diff --git a/src/app/dashboard/generate-from-photo/page.tsx b/src/app/dashboard/generate-from-photo/page.tsx index b2be7da..03d17ff 100644 --- a/src/app/dashboard/generate-from-photo/page.tsx +++ b/src/app/dashboard/generate-from-photo/page.tsx @@ -10,6 +10,7 @@ import { Button } from '@/components/ui/button'; import { Loader2, Save } from 'lucide-react'; import { useRouter } from 'next/navigation'; import { compressBase64Image, getBase64Size, formatBytes } from '@/utils/imageUtils'; +import ApiKeysWarning from '@/components/cards/ApiKeysWarning'; export default function GenerateFromPhotoPage() { const { user } = useAuth(); @@ -99,6 +100,14 @@ export default function GenerateFromPhotoPage() {

+ {/* API Keys Warning - only show OpenAI key warning for this page */} +
+ +
+ {generatedCard && ( diff --git a/src/app/dashboard/generate/page.tsx b/src/app/dashboard/generate/page.tsx index 9420a41..cb0e293 100644 --- a/src/app/dashboard/generate/page.tsx +++ b/src/app/dashboard/generate/page.tsx @@ -8,6 +8,7 @@ import { addCardToCollection } from '@/lib/firestore'; import { useToast } from '@/hooks/use-toast'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { compressBase64Image, getBase64Size, formatBytes } from '@/utils/imageUtils'; +import ApiKeysWarning from '@/components/cards/ApiKeysWarning'; export default function GenerateCardPage() { const { user } = useAuth(); @@ -93,6 +94,14 @@ export default function GenerateCardPage() {

+ {/* API Keys Warning - only show Gemini key warning for this page */} +
+ +
+ {generatedCard && ( diff --git a/src/app/dashboard/page.tsx b/src/app/dashboard/page.tsx index 956683f..2e5859e 100644 --- a/src/app/dashboard/page.tsx +++ b/src/app/dashboard/page.tsx @@ -5,6 +5,7 @@ import Link from 'next/link'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; import { Button } from '@/components/ui/button'; import { ScanLine, Sparkles, BookOpen, Camera } from 'lucide-react'; +import ApiKeysWarning from '@/components/cards/ApiKeysWarning'; export default function DashboardPage() { const router = useRouter(); @@ -18,6 +19,11 @@ export default function DashboardPage() {

+ {/* API Keys Warning */} +
+ +
+
diff --git a/src/app/dashboard/scan/page.tsx b/src/app/dashboard/scan/page.tsx index 51d14b9..097a428 100644 --- a/src/app/dashboard/scan/page.tsx +++ b/src/app/dashboard/scan/page.tsx @@ -1,4 +1,5 @@ import CardScanner from '@/components/cards/CardScanner'; +import ApiKeysWarning from '@/components/cards/ApiKeysWarning'; export default function ScanCardPage() { return ( @@ -9,6 +10,15 @@ export default function ScanCardPage() { Upload an image of your Pokémon card to automatically identify and add it to your collection

+ + {/* API Keys Warning - only show Gemini key warning for scanning */} +
+ +
+ ); diff --git a/src/components/cards/ApiKeysWarning.tsx b/src/components/cards/ApiKeysWarning.tsx new file mode 100644 index 0000000..76838b9 --- /dev/null +++ b/src/components/cards/ApiKeysWarning.tsx @@ -0,0 +1,81 @@ +'use client'; + +import { useState } from 'react'; +import Link from 'next/link'; +import { useApiKeys } from '@/hooks/useApiKeys'; +import { Alert, AlertDescription } from '@/components/ui/alert'; +import { Button } from '@/components/ui/button'; +import { AlertTriangle, Settings, X } from 'lucide-react'; + +interface ApiKeysWarningProps { + className?: string; + showDismiss?: boolean; + requiredKeys?: ('geminiApiKey' | 'openaiApiKey')[]; +} + +export default function ApiKeysWarning({ + className = "", + showDismiss = true, + requiredKeys = ['geminiApiKey', 'openaiApiKey'] +}: ApiKeysWarningProps) { + const { hasGeminiKey, hasOpenAIKey, loading } = useApiKeys(); + const [dismissed, setDismissed] = useState(false); + + if (loading || dismissed) { + return null; + } + + // Check which keys are missing + const missingKeys: string[] = []; + if (requiredKeys.includes('geminiApiKey') && !hasGeminiKey) { + missingKeys.push('Gemini API'); + } + if (requiredKeys.includes('openaiApiKey') && !hasOpenAIKey) { + missingKeys.push('OpenAI API'); + } + + // If no keys are missing, don't show the warning + if (missingKeys.length === 0) { + return null; + } + + const allKeysMissing = missingKeys.length === requiredKeys.length; + + return ( + + + +
+ + {allKeysMissing ? 'API Keys Required' : 'Some API Keys Missing'} + +

+ {allKeysMissing + ? `Configure your ${missingKeys.join(' and ')} key${missingKeys.length > 1 ? 's' : ''} to enable AI-powered features.` + : `Missing ${missingKeys.join(' and ')} key${missingKeys.length > 1 ? 's' : ''}. Some features may be unavailable.` + } +

+
+ +
+
+ {showDismiss && ( + + )} +
+
+ ); +} diff --git a/src/components/cards/CardGenerator.tsx b/src/components/cards/CardGenerator.tsx index a79d9de..5f4775a 100644 --- a/src/components/cards/CardGenerator.tsx +++ b/src/components/cards/CardGenerator.tsx @@ -16,6 +16,7 @@ import { Loader2, Download } from 'lucide-react'; import { downloadCardFromBase64 } from '@/utils/downloadUtils'; import { useToast } from '@/hooks/use-toast'; import { useAuth } from '@/hooks/useAuth'; +import { useApiKeys } from '@/hooks/useApiKeys'; export interface GeneratePokemonCardInput { pokemonName: string; @@ -79,6 +80,7 @@ interface CardGeneratorProps { export function CardGenerator({ onCardGenerated, initialValues }: CardGeneratorProps) { const { user } = useAuth(); + const { hasGeminiKey } = useApiKeys(); const { toast } = useToast(); const [isGenerating, setIsGenerating] = useState(false); const [generatedCard, setGeneratedCard] = useState<{ imageBase64: string; prompt: string } | null>(null); @@ -138,6 +140,11 @@ export function CardGenerator({ onCardGenerated, initialValues }: CardGeneratorP return; } + if (!hasGeminiKey) { + setError('Gemini API key is required to generate cards. Please configure it in Settings.'); + return; + } + setIsGenerating(true); setError(''); setGeneratedCard(null); @@ -394,17 +401,31 @@ export function CardGenerator({ onCardGenerated, initialValues }: CardGeneratorP - + {!hasGeminiKey && ( +
+

+ Please configure your Gemini API key in Settings to generate cards. +

+
+ )} + {error && (

{error}

diff --git a/src/components/cards/CardItem.tsx b/src/components/cards/CardItem.tsx index 595c317..55c4bbc 100644 --- a/src/components/cards/CardItem.tsx +++ b/src/components/cards/CardItem.tsx @@ -5,13 +5,14 @@ import Link from 'next/link'; import { Button } from '@/components/ui/button'; import { Card, CardContent, CardFooter, CardHeader, CardTitle, CardDescription } from '@/components/ui/card'; import { Badge } from '@/components/ui/badge'; -import { Pencil, Trash2, Sparkles, Download, Play, Pause, Video, Clock, AlertCircle, RotateCcw, FileDown, Eye } from 'lucide-react'; +import { Pencil, Trash2, Sparkles, Download, Play, Pause, Video, Clock, AlertCircle, RotateCcw, FileDown, Eye, Settings } from 'lucide-react'; import type { PokemonCard } from '@/types'; import { downloadCardImage, downloadCardVideo } from '@/utils/downloadUtils'; import { useToast } from '@/hooks/use-toast'; import ShareButton from '@/components/ui/share-button'; import { useState } from 'react'; import { generateVideoForExistingCard } from '@/lib/firestore'; +import { useApiKeys } from '@/hooks/useApiKeys'; import { AlertDialog, AlertDialogAction, @@ -33,6 +34,7 @@ interface CardItemProps { export default function CardItem({ card, onDelete, onUpdate, isDeleting }: CardItemProps) { const { toast } = useToast(); + const { hasGeminiKey } = useApiKeys(); const [showVideo, setShowVideo] = useState(false); const [isGeneratingVideo, setIsGeneratingVideo] = useState(false); @@ -288,29 +290,101 @@ export default function CardItem({ card, onDelete, onUpdate, isDeleting }: CardI {/* Video Generation Controls */} {!card.videoUrl && card.videoGenerationStatus !== 'generating' && ( - + hasGeminiKey ? ( + + ) : ( + + + + + + + + + Gemini API Key Required + + + To generate videos for your cards, you need to configure your Gemini API key in the settings. + This feature uses Google's Veo model to create animated videos from your card images. + + + + Cancel + + + + Configure API Keys + + + + + + ) )} {card.videoGenerationStatus === 'failed' && ( - + hasGeminiKey ? ( + + ) : ( + + + + + + + + + Gemini API Key Required + + + To retry video generation for your card, you need to configure your Gemini API key in the settings. + This feature uses Google's Veo model to create animated videos from your card images. + + + + Cancel + + + + Configure API Keys + + + + + + ) )} {/* Download Video Button - Show when video is ready */} diff --git a/src/components/cards/CardScanner.tsx b/src/components/cards/CardScanner.tsx index 457f17e..56e5a70 100644 --- a/src/components/cards/CardScanner.tsx +++ b/src/components/cards/CardScanner.tsx @@ -10,11 +10,13 @@ import { Loader2, Upload, AlertCircle, CheckCircle } from 'lucide-react'; import Image from 'next/image'; import CardForm, { type CardFormInputs } from './CardForm'; import { useAuth } from '@/hooks/useAuth'; +import { useApiKeys } from '@/hooks/useApiKeys'; import { addCardToCollection } from '@/lib/firestore'; import { useRouter } from 'next/navigation'; import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '../ui/card'; export default function CardScanner() { + const { hasGeminiKey } = useApiKeys(); const [isScanning, setIsScanning] = useState(false); const [scanResult, setScanResult] = useState(null); const [error, setError] = useState(null); @@ -53,6 +55,12 @@ export default function CardScanner() { return; } + if (!hasGeminiKey) { + setError('Gemini API key is required to scan Pokemon cards. Please configure it in Settings.'); + toast({ title: 'API Key Required', description: 'Gemini API key is required to scan Pokemon cards. Please configure it in Settings.', variant: 'destructive' }); + return; + } + setIsScanning(true); setError(null); setScanResult(null); @@ -179,24 +187,36 @@ export default function CardScanner() {
{imageDataUrl && ( - + + {!hasGeminiKey && ( +
+

+ Please configure your Gemini API key in Settings to scan Pokemon cards. +

+
)} - + )} )} diff --git a/src/components/cards/PhotoCardGenerator.tsx b/src/components/cards/PhotoCardGenerator.tsx index 23e02e8..06787c3 100644 --- a/src/components/cards/PhotoCardGenerator.tsx +++ b/src/components/cards/PhotoCardGenerator.tsx @@ -16,6 +16,7 @@ import Image from 'next/image'; import { useToast } from '@/hooks/use-toast'; import { downloadCardFromBase64 } from '@/utils/downloadUtils'; import { useAuth } from '@/hooks/useAuth'; +import { useApiKeys } from '@/hooks/useApiKeys'; export interface GenerateFromPhotoInput { photoDataUri: string; @@ -75,6 +76,7 @@ interface PhotoCardGeneratorProps { export function PhotoCardGenerator({ onCardGenerated, initialValues }: PhotoCardGeneratorProps) { const { user } = useAuth(); + const { hasOpenAIKey } = useApiKeys(); const [isGenerating, setIsGenerating] = useState(false); const [generatedCard, setGeneratedCard] = useState<{ imageBase64: string; prompt: string } | null>(null); const [error, setError] = useState(''); @@ -162,6 +164,11 @@ export function PhotoCardGenerator({ onCardGenerated, initialValues }: PhotoCard return; } + if (!hasOpenAIKey) { + setError('OpenAI API key is required to generate cards from photos. Please configure it in Settings.'); + return; + } + if (!data.photoDataUri) { toast({ title: 'Photo Required', @@ -432,17 +439,31 @@ export function PhotoCardGenerator({ onCardGenerated, initialValues }: PhotoCard - + {!hasOpenAIKey && ( +
+

+ Please configure your OpenAI API key in Settings to generate cards from photos. +

+
+ )} + {error && (

{error}

diff --git a/src/hooks/useApiKeys.ts b/src/hooks/useApiKeys.ts new file mode 100644 index 0000000..153b8fc --- /dev/null +++ b/src/hooks/useApiKeys.ts @@ -0,0 +1,60 @@ +'use client'; + +import { useState, useEffect } from 'react'; +import { useAuth } from '@/hooks/useAuth'; +import { getUserApiKeys } from '@/lib/firestore'; +import type { UserApiKeys } from '@/types'; + +interface UseApiKeysResult { + apiKeys: UserApiKeys | null; + loading: boolean; + hasGeminiKey: boolean; + hasOpenAIKey: boolean; + hasAnyKey: boolean; + refreshApiKeys: () => Promise; +} + +export function useApiKeys(): UseApiKeysResult { + const { user } = useAuth(); + const [apiKeys, setApiKeys] = useState(null); + const [loading, setLoading] = useState(true); + + const loadApiKeys = async () => { + if (!user) { + setLoading(false); + return; + } + + try { + const keys = await getUserApiKeys(user.uid); + setApiKeys(keys); + } catch (error) { + console.error('Error loading API keys:', error); + setApiKeys(null); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + loadApiKeys(); + }, [user]); + + const refreshApiKeys = async () => { + setLoading(true); + await loadApiKeys(); + }; + + const hasGeminiKey = !!(apiKeys?.geminiApiKey); + const hasOpenAIKey = !!(apiKeys?.openaiApiKey); + const hasAnyKey = hasGeminiKey || hasOpenAIKey; + + return { + apiKeys, + loading, + hasGeminiKey, + hasOpenAIKey, + hasAnyKey, + refreshApiKeys, + }; +}