instasaas / App.tsx
Persano's picture
Upload 25 files
5ca475a verified
raw
history blame
10.3 kB
import React, { useState, useCallback, useEffect } from 'react';
import type { Session } from '@supabase/gotrue-js';
import { Header } from '@/components/Header';
import { PromptForm } from '@/components/PromptForm'; // Reimagined as CreationPanel
import { SqlViewer } from '@/components/SqlViewer'; // Reimagined as PreviewCanvas
import { GuideModal } from '@/components/GuideModal';
import { generateImage, generateAdCopy, generateFeatureDescriptions } from '@/services/geminiService';
import { supabase } from '@/services/supabaseClient';
import type { GenerateOptions, AdCopy, BrandConcept, PriceData, FeatureDetails } from '@/types';
import { RateLimitError } from '@/lib/errors';
import { compositionPresets } from '@/lib/compositions';
const App: React.FC = () => {
const [session, setSession] = useState<Session | null>(null);
const [cooldownUntil, setCooldownUntil] = useState<Date | null>(null);
// Generation state
const [generatedImagesB64, setGeneratedImagesB64] = useState<string[] | null>(null);
const [isLoading, setIsLoading] = useState<boolean>(false);
const [error, setError] = useState<string | null>(null);
// Post content state (for display and marketing tools)
const [textOverlay, setTextOverlay] = useState<string>('');
const [compositionId, setCompositionId] = useState<string>('random');
const [textPosition, setTextPosition] = useState<GenerateOptions['textPosition']>('center');
const [subtitleOutline, setSubtitleOutline] = useState<GenerateOptions['subtitleOutline']>('auto');
const [artStylesForFont, setArtStylesForFont] = useState<string[]>([]);
// Context for marketing tools
const [currentBasePrompt, setCurrentBasePrompt] = useState<string>('');
const [currentTheme, setCurrentTheme] = useState<string>('');
const [brandData, setBrandData] = useState<GenerateOptions['brandData']>({ name: '', slogan: '', weight: 25 });
const [priceData, setPriceData] = useState<PriceData>({ text: '', modelText: '', style: 'circle', position: 'none', color: 'red'});
const [featureDetails, setFeatureDetails] = useState<FeatureDetails[] | null>(null);
// Marketing suite state
const [adCopy, setAdCopy] = useState<AdCopy | null>(null);
const [isAdCopyLoading, setIsAdCopyLoading] = useState<boolean>(false);
const [adCopyError, setAdCopyError] = useState<string | null>(null);
// Modal state
const [isGuideOpen, setIsGuideOpen] = useState(false);
const isAuthEnabled = !!supabase;
const triggerCooldown = useCallback((durationMs: number = 60000) => {
setCooldownUntil(new Date(Date.now() + durationMs));
}, []);
useEffect(() => {
if (supabase) {
// Supabase v2: onAuthStateChange handles the initial session check and any subsequent changes.
const { data: authListener } = supabase.auth.onAuthStateChange((_event, session) => {
setSession(session);
});
return () => {
// Cleanup subscription on component unmount
authListener.subscription?.unsubscribe();
};
}
}, []);
const handleLogin = async () => {
if (!supabase) {
setError("A funcionalidade de login está desabilitada pois a configuração do serviço está ausente.");
console.warn("Login attempt failed: Supabase client is not initialized.");
return;
}
// Supabase v2: use signInWithOAuth
const { error } = await supabase.auth.signInWithOAuth({ provider: 'google' });
if (error) {
console.error('Error logging in with Google:', error);
setError('Falha ao fazer login com o Google.');
}
};
const handleLogout = async () => {
if (supabase) {
await supabase.auth.signOut();
}
};
const handleGenerate = useCallback(async (options: GenerateOptions) => {
setIsLoading(true);
setError(null);
setGeneratedImagesB64(null);
setTextOverlay('');
setAdCopy(null); // Reset ads on new generation
setAdCopyError(null); // Also reset ad error
setFeatureDetails(null); // Reset feature details
// BUG FIX: Resolve 'random' composition ID to a specific one to prevent layout shifts on re-render.
let finalCompositionId = options.compositionId;
if (options.compositionId === 'random') {
const availablePresets = compositionPresets.filter(p => p.id !== 'random');
if (availablePresets.length > 0) {
const randomPreset = availablePresets[Math.floor(Math.random() * availablePresets.length)];
finalCompositionId = randomPreset.id;
} else {
finalCompositionId = 'impacto-light'; // Fallback if all presets are somehow filtered out
}
}
// Persist UI state from options
setCompositionId(finalCompositionId);
setTextPosition(options.textPosition);
setSubtitleOutline(options.subtitleOutline);
setArtStylesForFont(options.artStyles);
setCurrentBasePrompt(options.basePrompt);
setCurrentTheme(options.theme);
setBrandData(options.brandData);
setPriceData(options.priceData);
try {
if (!process.env.API_KEY) {
throw new Error("A variável de ambiente API_KEY não foi definida.");
}
if (options.scenario === 'isometric_details' && options.concept) {
// Special dual-call flow for detailed isometric view
const concept = options.concept;
setTextOverlay(options.textOverlay);
const [imageResults, descriptionResults] = await Promise.all([
generateImage(options.imagePrompt, options.negativeImagePrompt, 1),
generateFeatureDescriptions(options.basePrompt, concept)
]);
setGeneratedImagesB64(imageResults);
setFeatureDetails(descriptionResults);
} else {
// Standard single-call flow
const safeNumberOfImages = Math.max(1, Math.min(options.numberOfImages, 4));
const imageResults = await generateImage(options.imagePrompt, options.negativeImagePrompt, safeNumberOfImages);
setGeneratedImagesB64(imageResults);
setTextOverlay(options.textOverlay);
}
} catch (e) {
if (e instanceof RateLimitError) {
triggerCooldown();
}
// The service now provides a complete, user-friendly message.
setError((e as Error).message);
console.error(e);
} finally {
setIsLoading(false);
}
}, [triggerCooldown]);
const handleGenerateAds = useCallback(async () => {
if (!currentBasePrompt || !textOverlay) return;
setIsAdCopyLoading(true);
setAdCopy(null);
setAdCopyError(null);
try {
const result = await generateAdCopy(currentBasePrompt, textOverlay, currentTheme, brandData);
setAdCopy(result);
} catch (e) {
if (e instanceof RateLimitError) {
triggerCooldown();
}
// The service now provides a complete, user-friendly message.
setAdCopyError((e as Error).message);
console.error("Failed to generate ad copy:", e);
} finally {
setIsAdCopyLoading(false);
}
}, [currentBasePrompt, textOverlay, currentTheme, brandData, triggerCooldown]);
return (
<div className="min-h-screen bg-gray-100 text-gray-800 font-sans flex flex-col">
<Header
session={session}
onLogin={handleLogin}
onLogout={handleLogout}
isAuthEnabled={isAuthEnabled}
onOpenGuide={() => setIsGuideOpen(true)}
/>
<div className="flex-grow container mx-auto p-4 sm:p-6 md:p-8">
<main className="flex flex-col lg:flex-row gap-8 h-full">
<div className="lg:w-2/5 flex flex-col">
<PromptForm
onGenerate={handleGenerate}
isLoading={isLoading}
cooldownUntil={cooldownUntil}
onCooldown={triggerCooldown}
/>
</div>
<div className="lg:w-3/5 flex flex-col">
<SqlViewer
imagesB64={generatedImagesB64}
textOverlay={textOverlay}
compositionId={compositionId}
textPosition={textPosition}
subtitleOutline={subtitleOutline}
artStyles={artStylesForFont}
isLoading={isLoading}
error={error}
adCopy={adCopy}
isAdCopyLoading={isAdCopyLoading}
adCopyError={adCopyError}
onGenerateAds={handleGenerateAds}
brandData={brandData}
priceData={priceData}
featureDetails={featureDetails}
// Setters for editing
setTextOverlay={setTextOverlay}
setCompositionId={setCompositionId}
setTextPosition={setTextPosition}
setSubtitleOutline={setSubtitleOutline}
setPriceData={setPriceData}
setFeatureDetails={setFeatureDetails}
/>
</div>
</main>
</div>
<footer className="text-center p-4 text-gray-500 border-t border-gray-200">
<p>Powered by Gemini, React, and Supabase</p>
</footer>
<GuideModal isOpen={isGuideOpen} onClose={() => setIsGuideOpen(false)} />
</div>
);
};
export default App;