feat: added warning for the api keys

This commit is contained in:
xavidop
2025-07-23 23:17:38 +02:00
parent a29f191d03
commit ada9184c0b
10 changed files with 351 additions and 40 deletions
@@ -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() {
</p>
</div>
{/* API Keys Warning - only show OpenAI key warning for this page */}
<div className="mb-6">
<ApiKeysWarning
requiredKeys={['openaiApiKey']}
showDismiss={false}
/>
</div>
<PhotoCardGenerator onCardGenerated={handleCardGenerated} />
{generatedCard && (
+9
View File
@@ -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() {
</p>
</div>
{/* API Keys Warning - only show Gemini key warning for this page */}
<div className="mb-6">
<ApiKeysWarning
requiredKeys={['geminiApiKey']}
showDismiss={false}
/>
</div>
<CardGenerator onCardGenerated={handleCardGenerated} />
{generatedCard && (
+6
View File
@@ -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() {
</p>
</div>
{/* API Keys Warning */}
<div className="mb-8">
<ApiKeysWarning />
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 max-w-6xl mx-auto">
<Card className="hover:shadow-lg transition-shadow">
<CardHeader>
+10
View File
@@ -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
</p>
</div>
{/* API Keys Warning - only show Gemini key warning for scanning */}
<div className="max-w-4xl mx-auto">
<ApiKeysWarning
requiredKeys={['geminiApiKey']}
showDismiss={false}
/>
</div>
<CardScanner />
</div>
);
+81
View File
@@ -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 (
<Alert className={`border-yellow-200 bg-yellow-50 dark:border-yellow-800 dark:bg-yellow-950 ${className}`}>
<AlertTriangle className="h-4 w-4 text-yellow-600 dark:text-yellow-400" />
<AlertDescription className="flex items-center justify-between w-full">
<div className="flex-1">
<span className="font-medium text-yellow-800 dark:text-yellow-200">
{allKeysMissing ? 'API Keys Required' : 'Some API Keys Missing'}
</span>
<p className="text-yellow-700 dark:text-yellow-300 mt-1">
{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.`
}
</p>
<div className="flex gap-2 mt-2">
<Button asChild size="sm" variant="default" className="bg-yellow-600 hover:bg-yellow-700 text-white">
<Link href="/dashboard/settings">
<Settings className="h-3 w-3 mr-1" />
Configure Keys
</Link>
</Button>
</div>
</div>
{showDismiss && (
<Button
variant="ghost"
size="sm"
className="text-yellow-600 hover:text-yellow-800 dark:text-yellow-400 dark:hover:text-yellow-200 h-8 w-8 p-0 ml-4"
onClick={() => setDismissed(true)}
>
<X className="h-4 w-4" />
<span className="sr-only">Dismiss</span>
</Button>
)}
</AlertDescription>
</Alert>
);
}
+22 -1
View File
@@ -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
</div>
</div>
<Button type="submit" className="w-full" disabled={isGenerating}>
<Button
type="submit"
className="w-full"
disabled={isGenerating || !hasGeminiKey}
>
{isGenerating ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Generating Card...
</>
) : !hasGeminiKey ? (
'Gemini API Key Required'
) : (
'Generate Pokemon Card'
)}
</Button>
{!hasGeminiKey && (
<div className="p-4 bg-yellow-50 border border-yellow-200 rounded-md">
<p className="text-sm text-yellow-700">
Please configure your Gemini API key in <strong>Settings</strong> to generate cards.
</p>
</div>
)}
{error && (
<div className="p-4 bg-red-50 border border-red-200 rounded-md">
<p className="text-sm text-red-600">{error}</p>
+95 -21
View File
@@ -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' && (
<Button
variant="outline"
size="sm"
onClick={handleGenerateVideo}
disabled={isGeneratingVideo}
className="w-full bg-gradient-to-r from-purple-50 to-pink-50 hover:from-purple-100 hover:to-pink-100 border-purple-200 text-purple-700"
>
<Video className="h-4 w-4 mr-2" />
{isGeneratingVideo ? 'Starting...' : 'Make Card Live'}
</Button>
hasGeminiKey ? (
<Button
variant="outline"
size="sm"
onClick={handleGenerateVideo}
disabled={isGeneratingVideo}
className="w-full bg-gradient-to-r from-purple-50 to-pink-50 hover:from-purple-100 hover:to-pink-100 border-purple-200 text-purple-700"
>
<Video className="h-4 w-4 mr-2" />
{isGeneratingVideo ? 'Starting...' : 'Make Card Live'}
</Button>
) : (
<AlertDialog>
<AlertDialogTrigger asChild>
<Button
variant="outline"
size="sm"
className="w-full bg-yellow-50 hover:bg-yellow-100 border-yellow-200 text-yellow-700"
>
<Video className="h-4 w-4 mr-2" />
Make Card Live
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle className="flex items-center gap-2">
<AlertCircle className="h-5 w-5 text-yellow-600" />
Gemini API Key Required
</AlertDialogTitle>
<AlertDialogDescription>
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.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction asChild>
<Link href="/dashboard/settings">
<Settings className="h-4 w-4 mr-2" />
Configure API Keys
</Link>
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
)
)}
{card.videoGenerationStatus === 'failed' && (
<Button
variant="outline"
size="sm"
onClick={handleGenerateVideo}
disabled={isGeneratingVideo}
className="w-full border-orange-200 bg-orange-50 hover:bg-orange-100 text-orange-700"
>
<RotateCcw className="h-4 w-4 mr-2" />
{isGeneratingVideo ? 'Starting...' : 'Retry Video Generation'}
</Button>
hasGeminiKey ? (
<Button
variant="outline"
size="sm"
onClick={handleGenerateVideo}
disabled={isGeneratingVideo}
className="w-full border-orange-200 bg-orange-50 hover:bg-orange-100 text-orange-700"
>
<RotateCcw className="h-4 w-4 mr-2" />
{isGeneratingVideo ? 'Starting...' : 'Retry Video Generation'}
</Button>
) : (
<AlertDialog>
<AlertDialogTrigger asChild>
<Button
variant="outline"
size="sm"
className="w-full bg-yellow-50 hover:bg-yellow-100 border-yellow-200 text-yellow-700"
>
<RotateCcw className="h-4 w-4 mr-2" />
Retry Video Generation
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle className="flex items-center gap-2">
<AlertCircle className="h-5 w-5 text-yellow-600" />
Gemini API Key Required
</AlertDialogTitle>
<AlertDialogDescription>
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.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction asChild>
<Link href="/dashboard/settings">
<Settings className="h-4 w-4 mr-2" />
Configure API Keys
</Link>
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
)
)}
{/* Download Video Button - Show when video is ready */}
+37 -17
View File
@@ -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<ScanPokemonCardOutput | null>(null);
const [error, setError] = useState<string | null>(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() {
</div>
{imageDataUrl && (
<Button
onClick={handleScan}
disabled={isScanning || !imageDataUrl}
className="w-full py-6 text-lg font-semibold"
size="lg"
>
{isScanning ? (
<>
<Loader2 className="mr-3 h-5 w-5 animate-spin" />
Analyzing Card...
</>
) : (
<>
<Upload className="mr-3 h-5 w-5" />
Scan Card
</>
<>
<Button
onClick={handleScan}
disabled={isScanning || !imageDataUrl || !hasGeminiKey}
className="w-full py-6 text-lg font-semibold"
size="lg"
>
{isScanning ? (
<>
<Loader2 className="mr-3 h-5 w-5 animate-spin" />
Analyzing Card...
</>
) : !hasGeminiKey ? (
'Gemini API Key Required'
) : (
<>
<Upload className="mr-3 h-5 w-5" />
Scan Card
</>
)}
</Button>
{!hasGeminiKey && (
<div className="p-4 bg-yellow-50 border border-yellow-200 rounded-md">
<p className="text-sm text-yellow-700">
Please configure your Gemini API key in <strong>Settings</strong> to scan Pokemon cards.
</p>
</div>
)}
</Button>
</>
)}
</div>
)}
+22 -1
View File
@@ -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<string>('');
@@ -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
</div>
</div>
<Button type="submit" className="w-full" disabled={isGenerating || !imagePreview}>
<Button
type="submit"
className="w-full"
disabled={isGenerating || !imagePreview || !hasOpenAIKey}
>
{isGenerating ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Generating Card from Photo...
</>
) : !hasOpenAIKey ? (
'OpenAI API Key Required'
) : (
'Generate Pokemon Card from Photo'
)}
</Button>
{!hasOpenAIKey && (
<div className="p-4 bg-yellow-50 border border-yellow-200 rounded-md">
<p className="text-sm text-yellow-700">
Please configure your OpenAI API key in <strong>Settings</strong> to generate cards from photos.
</p>
</div>
)}
{error && (
<div className="p-4 bg-red-50 border border-red-200 rounded-md">
<p className="text-sm text-red-600">{error}</p>
+60
View File
@@ -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<void>;
}
export function useApiKeys(): UseApiKeysResult {
const { user } = useAuth();
const [apiKeys, setApiKeys] = useState<UserApiKeys | null>(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,
};
}