Harsha1845's picture
Update src/App.tsx
67405d9 verified
import React, { useState, useRef, useEffect } from 'react';
import { Product, AppView } from './types';
import { PRODUCTS } from './constants';
import { generateTryOn } from './services/geminiService';
import Navigation from './components/Navigation';
import ProductCard from './components/ProductCard';
import CameraView from './components/CameraView';
import ResultView from './components/ResultView';
import { Sparkles, Timer } from 'lucide-react';
const STORAGE_KEY = 'stylegenie_last_req_v2';
const COOLDOWN_SECONDS = 120; // 2 minutes is safe for public shared keys
function App() {
const [currentView, setCurrentView] = useState<AppView>(AppView.CATALOG);
const [selectedProduct, setSelectedProduct] = useState<Product | null>(null);
const [userImage, setUserImage] = useState<string | null>(null);
const [generatedResult, setGeneratedResult] = useState<string | null>(null);
const [isGenerating, setIsGenerating] = useState(false);
const [generationError, setGenerationError] = useState<string | null>(null);
const [retryAfter, setRetryAfter] = useState<number | null>(null);
const [globalCooldown, setGlobalCooldown] = useState(0);
// Sync cooldown from localStorage to survive refreshes
useEffect(() => {
const updateCooldown = () => {
const last = localStorage.getItem(STORAGE_KEY);
if (last) {
const diff = (Date.now() - parseInt(last)) / 1000;
if (diff < COOLDOWN_SECONDS) {
setGlobalCooldown(Math.ceil(COOLDOWN_SECONDS - diff));
} else {
setGlobalCooldown(0);
}
}
};
updateCooldown();
const interval = setInterval(updateCooldown, 1000);
return () => clearInterval(interval);
}, []);
const handleSelectProduct = (product: Product) => {
if (globalCooldown > 0) return;
setSelectedProduct(product);
setCurrentView(AppView.CAMERA);
};
const handleBackToCatalog = () => {
setCurrentView(AppView.CATALOG);
setSelectedProduct(null);
setUserImage(null);
setGeneratedResult(null);
setGenerationError(null);
setRetryAfter(null);
};
const handleCapture = async (imageSrc: string) => {
setUserImage(imageSrc);
setCurrentView(AppView.RESULT);
if (selectedProduct) {
await processTryOn(imageSrc, selectedProduct);
}
};
const processTryOn = async (imageSrc: string, product: Product) => {
// Final check for cooldown
if (globalCooldown > 0) {
setRetryAfter(globalCooldown);
return;
}
setIsGenerating(true);
setGenerationError(null);
setRetryAfter(null);
// Set lock immediately to prevent race conditions
localStorage.setItem(STORAGE_KEY, Date.now().toString());
try {
const resultImage = await generateTryOn(imageSrc, product);
setGeneratedResult(resultImage);
} catch (error: any) {
const msg = error.message || "";
if (msg.includes("FREE_LIMIT_HIT") || msg.includes("429")) {
setRetryAfter(COOLDOWN_SECONDS);
} else {
setGenerationError(msg);
}
} finally {
setIsGenerating(false);
}
};
const handleRetake = () => {
setCurrentView(AppView.CAMERA);
setGeneratedResult(null);
setGenerationError(null);
setRetryAfter(null);
};
return (
<div className="min-h-screen bg-slate-50 font-sans text-slate-900">
<Navigation
showBack={currentView !== AppView.CATALOG}
onBack={handleBackToCatalog}
cooldown={globalCooldown}
/>
{currentView === AppView.CATALOG && (
<main className="max-w-7xl mx-auto p-4 md:p-6 lg:p-8">
<div className="mb-8 text-center max-w-2xl mx-auto">
<div className="inline-flex items-center gap-2 px-3 py-1 bg-brand-50 rounded-full border border-brand-100 mb-4">
<Sparkles className="w-4 h-4 text-brand-500" />
<span className="text-[10px] font-bold text-brand-600 uppercase tracking-widest">Optimized Free Mode</span>
</div>
<h1 className="text-3xl md:text-4xl font-bold text-gray-900 mb-4">StyleGenie Try-On</h1>
{globalCooldown > 0 ? (
<div className="flex flex-col items-center gap-2 mt-4 p-4 bg-amber-50 rounded-2xl border border-amber-100 animate-in fade-in zoom-in-95">
<Timer className="w-6 h-6 text-amber-500 animate-pulse" />
<p className="text-amber-700 font-bold">AI is Resting: {globalCooldown}s</p>
<p className="text-amber-600 text-xs">Free tier allows 1 generation every 2 minutes.</p>
</div>
) : (
<p className="text-gray-500 text-lg">Select an item below to see the magic happen.</p>
)}
</div>
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4 md:gap-6">
{PRODUCTS.map(product => (
<ProductCard
key={product.id}
product={product}
onTryOn={handleSelectProduct}
disabled={globalCooldown > 0}
/>
))}
</div>
</main>
)}
{currentView === AppView.CAMERA && selectedProduct && (
<CameraView
product={selectedProduct}
onCapture={handleCapture}
onClose={handleBackToCatalog}
/>
)}
{currentView === AppView.RESULT && selectedProduct && userImage && (
<ResultView
product={selectedProduct}
originalImage={userImage}
generatedImage={generatedResult}
loading={isGenerating}
error={generationError}
retryAfter={retryAfter}
onRetake={handleRetake}
onRetry={() => processTryOn(userImage, selectedProduct)}
/>
)}
</div>
);
}
export default App;