mirror of
https://github.com/xavidop/cardex.git
synced 2026-09-04 09:13:28 +00:00
feat: refactors
This commit is contained in:
@@ -1,5 +1,22 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { ERROR_MESSAGES, CACHE_CONTROL } from '@/constants';
|
||||
|
||||
/**
|
||||
* Validates if a URL is from Firebase Storage
|
||||
* @param url - URL to validate
|
||||
* @returns true if URL is from Firebase Storage (production or emulator)
|
||||
*/
|
||||
function isValidFirebaseStorageUrl(url: string): boolean {
|
||||
return url.includes('firebasestorage.googleapis.com') ||
|
||||
url.includes('localhost:9199') ||
|
||||
url.includes('127.0.0.1:9199');
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/download
|
||||
* Proxy endpoint to download files from Firebase Storage to avoid CORS issues
|
||||
* Only allows Firebase Storage URLs for security
|
||||
*/
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const url = request.nextUrl.searchParams.get('url');
|
||||
@@ -11,11 +28,8 @@ export async function GET(request: NextRequest) {
|
||||
);
|
||||
}
|
||||
|
||||
// Validate that the URL is from Firebase Storage (production or emulator)
|
||||
const isFirebaseStorage = url.includes('firebasestorage.googleapis.com');
|
||||
const isFirebaseEmulator = url.includes('localhost:9199') || url.includes('127.0.0.1:9199');
|
||||
|
||||
if (!isFirebaseStorage && !isFirebaseEmulator) {
|
||||
// Validate URL is from Firebase Storage
|
||||
if (!isValidFirebaseStorageUrl(url)) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Only Firebase Storage URLs are allowed' },
|
||||
{ status: 403 }
|
||||
@@ -33,7 +47,6 @@ export async function GET(request: NextRequest) {
|
||||
throw new Error(`Failed to fetch: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
|
||||
// Get the content type from the original response
|
||||
const contentType = response.headers.get('content-type') || 'application/octet-stream';
|
||||
|
||||
// Stream the response
|
||||
@@ -41,7 +54,7 @@ export async function GET(request: NextRequest) {
|
||||
headers: {
|
||||
'Content-Type': contentType,
|
||||
'Content-Disposition': 'attachment',
|
||||
'Cache-Control': 'public, max-age=31536000',
|
||||
'Cache-Control': CACHE_CONTROL.ONE_YEAR,
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Methods': 'GET',
|
||||
'Access-Control-Allow-Headers': 'Content-Type',
|
||||
@@ -49,8 +62,13 @@ export async function GET(request: NextRequest) {
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Download proxy error:', error);
|
||||
|
||||
const errorMessage = error instanceof Error
|
||||
? error.message
|
||||
: ERROR_MESSAGES.IMAGE_DOWNLOAD_FAILED;
|
||||
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to download file' },
|
||||
{ error: errorMessage },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,34 +1,23 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { NextRequest } from 'next/server';
|
||||
import { generateTCGCardFromPhoto } from '@/ai/flows/generate-tcg-card-from-photo';
|
||||
import { handleApiRequest } from '@/lib/api-handler';
|
||||
|
||||
/**
|
||||
* Required fields for photo-based card generation
|
||||
*/
|
||||
const REQUIRED_FIELDS = [
|
||||
'userId',
|
||||
'photoDataUri',
|
||||
'characterName',
|
||||
'characterType',
|
||||
'styleDescription',
|
||||
'game'
|
||||
] as const;
|
||||
|
||||
/**
|
||||
* POST /api/generate-card-from-photo
|
||||
* Generates a TCG card from a photo using AI
|
||||
*/
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json();
|
||||
|
||||
// Validate required fields - including userId
|
||||
const requiredFields = ['userId', 'photoDataUri', 'characterName', 'characterType', 'styleDescription', 'game'];
|
||||
const missingFields = requiredFields.filter(field => !body[field]);
|
||||
|
||||
if (missingFields.length > 0) {
|
||||
return NextResponse.json(
|
||||
{ error: `Missing required fields: ${missingFields.join(', ')}` },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Call the AI flow
|
||||
const result = await generateTCGCardFromPhoto(body);
|
||||
|
||||
if (result.error) {
|
||||
return NextResponse.json({ error: result.error }, { status: 500 });
|
||||
}
|
||||
|
||||
return NextResponse.json(result);
|
||||
} catch (error) {
|
||||
console.error('API route error:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Internal server error' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
return handleApiRequest(request, REQUIRED_FIELDS, generateTCGCardFromPhoto);
|
||||
}
|
||||
|
||||
@@ -1,36 +1,24 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { NextRequest } from 'next/server';
|
||||
import { generateTCGCard } from '@/ai/flows/generate-tcg-card';
|
||||
import { handleApiRequest } from '@/lib/api-handler';
|
||||
|
||||
/**
|
||||
* Required fields for card generation
|
||||
*/
|
||||
const REQUIRED_FIELDS = [
|
||||
'userId',
|
||||
'game',
|
||||
'characterName',
|
||||
'characterType',
|
||||
'backgroundDescription',
|
||||
'characterDescription'
|
||||
] as const;
|
||||
|
||||
/**
|
||||
* POST /api/generate-card
|
||||
* Generates a TCG card using AI based on provided parameters
|
||||
*/
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json();
|
||||
|
||||
// New multi-game flow
|
||||
const requiredFields = ['userId', 'game', 'characterName', 'characterType', 'backgroundDescription', 'characterDescription'];
|
||||
const missingFields = requiredFields.filter(field => !body[field]);
|
||||
|
||||
if (missingFields.length > 0) {
|
||||
return NextResponse.json(
|
||||
{ error: `Missing required fields: ${missingFields.join(', ')}` },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Call the new AI flow
|
||||
const result = await generateTCGCard(body);
|
||||
|
||||
if (result.error) {
|
||||
return NextResponse.json({ error: result.error }, { status: 500 });
|
||||
}
|
||||
|
||||
return NextResponse.json(result);
|
||||
|
||||
} catch (error) {
|
||||
console.error('API route error:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Internal server error' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
return handleApiRequest(request, REQUIRED_FIELDS, generateTCGCard);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,13 +1,19 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { generateVideoForExistingCard } from '@/lib/firestore';
|
||||
import { ERROR_MESSAGES, SUCCESS_MESSAGES } from '@/constants';
|
||||
|
||||
/**
|
||||
* POST /api/generate-video
|
||||
* Starts video generation for an existing card
|
||||
* This is a background process - the video generation happens asynchronously
|
||||
*/
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const { userId, cardId } = await request.json();
|
||||
|
||||
if (!userId || !cardId) {
|
||||
return NextResponse.json(
|
||||
{ error: 'User ID and Card ID are required' },
|
||||
{ error: `${ERROR_MESSAGES.USER_ID_REQUIRED} and ${ERROR_MESSAGES.CARD_ID_REQUIRED}` },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
@@ -17,13 +23,15 @@ export async function POST(request: NextRequest) {
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: 'Video generation started successfully'
|
||||
message: SUCCESS_MESSAGES.VIDEO_GENERATED
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error in video generation API:', error);
|
||||
|
||||
const errorMessage = error instanceof Error ? error.message : 'Internal server error';
|
||||
const errorMessage = error instanceof Error
|
||||
? error.message
|
||||
: ERROR_MESSAGES.VIDEO_GENERATION_FAILED;
|
||||
|
||||
return NextResponse.json(
|
||||
{ error: errorMessage },
|
||||
|
||||
@@ -226,7 +226,7 @@ export default function EditCardPage() {
|
||||
isSubmitting={isSubmitting}
|
||||
submitButtonText="Save Changes"
|
||||
formTitle="Update Card Information"
|
||||
formDescription="Modify the details of your Pokémon card."
|
||||
formDescription="Modify the details of your TCG card."
|
||||
onCancel={() => router.push('/dashboard/collection')}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -216,7 +216,7 @@ export default function CollectionPage() {
|
||||
<div className="text-center py-10 border-2 border-dashed border-border rounded-lg">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1" strokeLinecap="round" strokeLinejoin="round" className="lucide lucide-archive-restore mx-auto mb-4 text-muted-foreground"><rect width="20" height="5" x="2" y="3" rx="1"/><path d="M4 8v11a2 2 0 0 0 2 2h12a2 2 0 0 0 2-V8"/><path d="M9.5 12.5 12 15l2.5-2.5"/><path d="M12 15V9"/></svg>
|
||||
<h2 className="text-xl font-semibold text-muted-foreground">Your collection is empty.</h2>
|
||||
<p className="text-muted-foreground mt-2">Start by scanning or generating your first Pokémon card!</p>
|
||||
<p className="text-muted-foreground mt-2">Start by scanning or generating your first TCG card!</p>
|
||||
<div className="flex gap-2 justify-center mt-6">
|
||||
<Button asChild variant="outline">
|
||||
<Link href="/dashboard/generate">
|
||||
|
||||
+2
-2
@@ -11,8 +11,8 @@ import Navbar from '@/components/layout/Navbar';
|
||||
const inter = Inter({ subsets: ['latin'], variable: '--font-inter' });
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Cardex - Pokémon Card Scanner',
|
||||
description: 'Scan, identify, and manage your Pokémon card collection with Cardex.',
|
||||
title: 'Cardex - TCG Card Scanner',
|
||||
description: 'Scan, identify, and manage your TCG card collection with Cardex.',
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
|
||||
+14
-72
@@ -1,80 +1,22 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useAuth } from '@/hooks/useAuth';
|
||||
import LoginForm from '@/components/auth/LoginForm';
|
||||
import GoogleSignInButton from '@/components/auth/GoogleSignInButton';
|
||||
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Separator } from '@/components/ui/separator';
|
||||
import Link from 'next/link';
|
||||
import { Loader2 } from 'lucide-react';
|
||||
import { AuthPageWrapper } from '@/components/auth/AuthPageWrapper';
|
||||
import { AuthPageLayout } from '@/components/auth/AuthPageLayout';
|
||||
|
||||
export default function LoginPage() {
|
||||
const { user, loading: authLoading } = useAuth();
|
||||
const router = useRouter();
|
||||
|
||||
useEffect(() => {
|
||||
if (!authLoading && user) {
|
||||
router.replace('/dashboard');
|
||||
}
|
||||
}, [user, authLoading, router]);
|
||||
|
||||
if (authLoading || (!authLoading && user)) {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-screen">
|
||||
<Loader2 className="h-12 w-12 animate-spin text-primary" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex min-h-full flex-col justify-center py-12 sm:px-6 lg:px-8">
|
||||
<div className="sm:mx-auto sm:w-full sm:max-w-md">
|
||||
<h1 className="mt-6 text-center text-3xl font-bold tracking-tight text-foreground font-headline">
|
||||
Sign in to Cardex
|
||||
</h1>
|
||||
<p className="mt-2 text-center text-sm text-muted-foreground">
|
||||
Access your Pokémon card collection.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="mt-8 sm:mx-auto sm:w-full sm:max-w-md">
|
||||
<Card className="shadow-xl">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-xl">Welcome back</CardTitle>
|
||||
<CardDescription>Enter your credentials to continue.</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<LoginForm />
|
||||
<div className="mt-6">
|
||||
<div className="relative">
|
||||
<div className="absolute inset-0 flex items-center">
|
||||
<Separator />
|
||||
</div>
|
||||
<div className="relative flex justify-center text-sm">
|
||||
<span className="bg-card px-2 text-muted-foreground">
|
||||
Or continue with
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-6">
|
||||
<GoogleSignInButton />
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
<CardFooter className="justify-center">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Don't have an account?{' '}
|
||||
<Link href="/signup" legacyBehavior>
|
||||
<a className="font-medium text-primary hover:text-primary/80">
|
||||
Sign up
|
||||
</a>
|
||||
</Link>
|
||||
</p>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
<AuthPageWrapper>
|
||||
<AuthPageLayout
|
||||
title="Sign in to Cardex"
|
||||
subtitle="Access your TCG card collection."
|
||||
cardTitle="Welcome back"
|
||||
cardDescription="Enter your credentials to continue."
|
||||
form={<LoginForm />}
|
||||
footerText="Don't have an account?"
|
||||
footerLinkText="Sign up"
|
||||
footerLinkHref="/signup"
|
||||
/>
|
||||
</AuthPageWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
+15
-72
@@ -1,80 +1,23 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useAuth } from '@/hooks/useAuth';
|
||||
import SignupForm from '@/components/auth/SignupForm';
|
||||
import GoogleSignInButton from '@/components/auth/GoogleSignInButton';
|
||||
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Separator } from '@/components/ui/separator';
|
||||
import Link from 'next/link';
|
||||
import { Loader2 } from 'lucide-react';
|
||||
import { AuthPageWrapper } from '@/components/auth/AuthPageWrapper';
|
||||
import { AuthPageLayout } from '@/components/auth/AuthPageLayout';
|
||||
|
||||
export default function SignupPage() {
|
||||
const { user, loading: authLoading } = useAuth();
|
||||
const router = useRouter();
|
||||
|
||||
useEffect(() => {
|
||||
if (!authLoading && user) {
|
||||
router.replace('/dashboard');
|
||||
}
|
||||
}, [user, authLoading, router]);
|
||||
|
||||
if (authLoading || (!authLoading && user)) {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-screen">
|
||||
<Loader2 className="h-12 w-12 animate-spin text-primary" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex min-h-full flex-col justify-center py-12 sm:px-6 lg:px-8">
|
||||
<div className="sm:mx-auto sm:w-full sm:max-w-md">
|
||||
<h1 className="mt-6 text-center text-3xl font-bold tracking-tight text-foreground font-headline">
|
||||
Create your Cardex account
|
||||
</h1>
|
||||
<p className="mt-2 text-center text-sm text-muted-foreground">
|
||||
Start managing your Pokémon card collection today.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="mt-8 sm:mx-auto sm:w-full sm:max-w-md">
|
||||
<Card className="shadow-xl">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-xl">Get Started</CardTitle>
|
||||
<CardDescription>It's quick and easy.</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<SignupForm />
|
||||
<div className="mt-6">
|
||||
<div className="relative">
|
||||
<div className="absolute inset-0 flex items-center">
|
||||
<Separator />
|
||||
</div>
|
||||
<div className="relative flex justify-center text-sm">
|
||||
<span className="bg-card px-2 text-muted-foreground">
|
||||
Or sign up with
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-6">
|
||||
<GoogleSignInButton />
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
<CardFooter className="justify-center">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Already have an account?{' '}
|
||||
<Link href="/login" legacyBehavior>
|
||||
<a className="font-medium text-primary hover:text-primary/80">
|
||||
Sign in
|
||||
</a>
|
||||
</Link>
|
||||
</p>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
<AuthPageWrapper>
|
||||
<AuthPageLayout
|
||||
title="Create your Cardex account"
|
||||
subtitle="Start managing your TCG card collection today."
|
||||
cardTitle="Get Started"
|
||||
cardDescription="It's quick and easy."
|
||||
form={<SignupForm />}
|
||||
footerText="Already have an account?"
|
||||
footerLinkText="Sign in"
|
||||
footerLinkHref="/login"
|
||||
googleButtonText="Or sign up with"
|
||||
/>
|
||||
</AuthPageWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user