This commit is contained in:
warlock 2025-06-23 01:25:35 +05:30
parent 1028d24bc1
commit b697bb176a
18 changed files with 3815 additions and 153 deletions

View File

@ -0,0 +1,51 @@
import { NextResponse } from 'next/server';
import { createCheckoutSession } from '@/app/lib/stripe';
export async function POST(req: Request) {
try {
console.log('Creating checkout session...');
// For now, we'll skip authentication check until Firebase Admin is set up
// TODO: Add proper authentication check with Firebase Admin
const { boxId, boxName, amount, currency } = await req.json();
console.log('Checkout session request:', { boxId, boxName, amount, currency });
if (!amount || amount <= 0) {
console.error('Invalid amount:', amount);
return NextResponse.json(
{ error: 'Invalid amount' },
{ status: 400 }
);
}
if (!boxId || !boxName) {
console.error('Missing box information:', { boxId, boxName });
return NextResponse.json(
{ error: 'Missing box information' },
{ status: 400 }
);
}
console.log('Calling createCheckoutSession with:', { boxId, boxName, amount, currency });
const session = await createCheckoutSession(
boxId,
boxName,
amount,
currency
);
console.log('Checkout session created successfully:', { sessionId: session.id });
return NextResponse.json({
sessionId: session.id,
});
} catch (error) {
console.error('Error creating checkout session:', error);
return NextResponse.json(
{ error: error instanceof Error ? error.message : 'Error creating checkout session' },
{ status: 500 }
);
}
}

View File

@ -0,0 +1,42 @@
import { NextResponse } from 'next/server';
import { createPaymentIntent } from '@/app/lib/stripe';
export async function POST(req: Request) {
try {
console.log('Creating payment intent...');
// For now, we'll skip authentication check until Firebase Admin is set up
// TODO: Add proper authentication check with Firebase Admin
const { amount, currency } = await req.json();
console.log('Payment intent request:', { amount, currency });
if (!amount || amount <= 0) {
console.error('Invalid amount:', amount);
return NextResponse.json(
{ error: 'Invalid amount' },
{ status: 400 }
);
}
console.log('Calling createPaymentIntent with:', { amount, currency });
const { clientSecret, paymentIntentId } = await createPaymentIntent(
amount,
currency
);
console.log('Payment intent created successfully:', { paymentIntentId });
return NextResponse.json({
clientSecret,
paymentIntentId,
});
} catch (error) {
console.error('Error creating payment intent:', error);
return NextResponse.json(
{ error: error instanceof Error ? error.message : 'Error creating payment intent' },
{ status: 500 }
);
}
}

167
app/auth/login/page.tsx Normal file
View File

@ -0,0 +1,167 @@
'use client';
import { useState } from 'react';
import { useRouter } from 'next/navigation';
import Link from 'next/link';
import { useAuth } from '../../context/AuthContext';
export default function LoginPage() {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [error, setError] = useState('');
const [loading, setLoading] = useState(false);
const router = useRouter();
const { signIn, signInWithGoogle } = useAuth();
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
try {
setError('');
setLoading(true);
await signIn(email, password);
router.push('/'); // Redirect to home page after successful login
} catch (error: any) {
setError(error.message || 'Failed to sign in');
} finally {
setLoading(false);
}
};
const handleGoogleSignIn = async () => {
try {
setError('');
setLoading(true);
await signInWithGoogle();
router.push('/');
} catch (error: any) {
setError(error.message || 'Failed to sign in with Google');
} finally {
setLoading(false);
}
};
return (
<div className="min-h-screen flex items-center justify-center bg-gradient-to-b from-background to-background/95 px-4">
<div className="max-w-md w-full space-y-8 bg-white/5 backdrop-blur-sm p-8 rounded-2xl shadow-xl">
<div>
<h2 className="mt-6 text-center text-3xl font-bold bg-clip-text text-transparent bg-gradient-to-r from-purple-600 to-pink-600">
Sign in to your account
</h2>
</div>
{error && (
<div className="bg-red-500/10 border border-red-500/20 text-red-500 px-4 py-3 rounded-lg text-sm">
{error}
</div>
)}
<form className="mt-8 space-y-6" onSubmit={handleSubmit}>
<div className="space-y-4">
<div>
<label htmlFor="email" className="block text-sm font-medium text-foreground/80">
Email address
</label>
<input
id="email"
name="email"
type="email"
autoComplete="email"
required
value={email}
onChange={(e) => setEmail(e.target.value)}
className="mt-1 block w-full px-4 py-3 bg-background/50 border border-foreground/10 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent transition-colors"
placeholder="Enter your email"
/>
</div>
<div>
<label htmlFor="password" className="block text-sm font-medium text-foreground/80">
Password
</label>
<input
id="password"
name="password"
type="password"
autoComplete="current-password"
required
value={password}
onChange={(e) => setPassword(e.target.value)}
className="mt-1 block w-full px-4 py-3 bg-background/50 border border-foreground/10 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent transition-colors"
placeholder="Enter your password"
/>
</div>
</div>
<div className="flex items-center justify-between">
<div className="text-sm">
<Link
href="/auth/reset-password"
className="text-purple-600 hover:text-purple-500 transition-colors"
>
Forgot your password?
</Link>
</div>
</div>
<div>
<button
type="submit"
disabled={loading}
className="w-full flex justify-center py-3 px-4 border border-transparent rounded-lg shadow-sm text-sm font-medium text-white bg-gradient-to-r from-purple-600 to-pink-600 hover:opacity-90 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-purple-500 transition-opacity disabled:opacity-50 disabled:cursor-not-allowed"
>
{loading ? 'Signing in...' : 'Sign in'}
</button>
</div>
<div className="relative">
<div className="absolute inset-0 flex items-center">
<div className="w-full border-t border-foreground/10"></div>
</div>
<div className="relative flex justify-center text-sm">
<span className="px-2 bg-background text-foreground/60">Or continue with</span>
</div>
</div>
<div>
<button
type="button"
onClick={handleGoogleSignIn}
disabled={loading}
className="w-full flex justify-center items-center gap-2 py-3 px-4 border border-foreground/10 rounded-lg shadow-sm text-sm font-medium text-foreground bg-background/50 hover:bg-background/80 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-purple-500 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
>
<svg className="w-5 h-5" viewBox="0 0 24 24">
<path
fill="currentColor"
d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"
/>
<path
fill="currentColor"
d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"
/>
<path
fill="currentColor"
d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"
/>
<path
fill="currentColor"
d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"
/>
</svg>
Sign in with Google
</button>
</div>
</form>
<div className="text-center text-sm">
<span className="text-foreground/60">Don't have an account? </span>
<Link
href="/auth/signup"
className="text-purple-600 hover:text-purple-500 transition-colors"
>
Sign up
</Link>
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,93 @@
'use client';
import { useState } from 'react';
import Link from 'next/link';
import { useAuth } from '../../context/AuthContext';
export default function ResetPasswordPage() {
const [email, setEmail] = useState('');
const [error, setError] = useState('');
const [message, setMessage] = useState('');
const [loading, setLoading] = useState(false);
const { resetPassword } = useAuth();
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
try {
setError('');
setMessage('');
setLoading(true);
await resetPassword(email);
setMessage('Check your email for password reset instructions');
} catch (error: any) {
setError(error.message || 'Failed to reset password');
} finally {
setLoading(false);
}
};
return (
<div className="min-h-screen flex items-center justify-center bg-gradient-to-b from-background to-background/95 px-4">
<div className="max-w-md w-full space-y-8 bg-white/5 backdrop-blur-sm p-8 rounded-2xl shadow-xl">
<div>
<h2 className="mt-6 text-center text-3xl font-bold bg-clip-text text-transparent bg-gradient-to-r from-purple-600 to-pink-600">
Reset your password
</h2>
<p className="mt-2 text-center text-sm text-foreground/60">
Enter your email address and we'll send you a link to reset your password.
</p>
</div>
{error && (
<div className="bg-red-500/10 border border-red-500/20 text-red-500 px-4 py-3 rounded-lg text-sm">
{error}
</div>
)}
{message && (
<div className="bg-green-500/10 border border-green-500/20 text-green-500 px-4 py-3 rounded-lg text-sm">
{message}
</div>
)}
<form className="mt-8 space-y-6" onSubmit={handleSubmit}>
<div>
<label htmlFor="email" className="block text-sm font-medium text-foreground/80">
Email address
</label>
<input
id="email"
name="email"
type="email"
autoComplete="email"
required
value={email}
onChange={(e) => setEmail(e.target.value)}
className="mt-1 block w-full px-4 py-3 bg-background/50 border border-foreground/10 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent transition-colors"
placeholder="Enter your email"
/>
</div>
<div>
<button
type="submit"
disabled={loading}
className="w-full flex justify-center py-3 px-4 border border-transparent rounded-lg shadow-sm text-sm font-medium text-white bg-gradient-to-r from-purple-600 to-pink-600 hover:opacity-90 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-purple-500 transition-opacity disabled:opacity-50 disabled:cursor-not-allowed"
>
{loading ? 'Sending reset link...' : 'Send reset link'}
</button>
</div>
</form>
<div className="text-center text-sm">
<Link
href="/auth/login"
className="text-purple-600 hover:text-purple-500 transition-colors"
>
Back to sign in
</Link>
</div>
</div>
</div>
);
}

179
app/auth/signup/page.tsx Normal file
View File

@ -0,0 +1,179 @@
'use client';
import { useState } from 'react';
import { useRouter } from 'next/navigation';
import Link from 'next/link';
import { useAuth } from '../../context/AuthContext';
export default function SignUpPage() {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [confirmPassword, setConfirmPassword] = useState('');
const [error, setError] = useState('');
const [loading, setLoading] = useState(false);
const router = useRouter();
const { signUp, signInWithGoogle } = useAuth();
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (password !== confirmPassword) {
return setError('Passwords do not match');
}
try {
setError('');
setLoading(true);
await signUp(email, password);
router.push('/'); // Redirect to home page after successful signup
} catch (error: any) {
setError(error.message || 'Failed to create an account');
} finally {
setLoading(false);
}
};
const handleGoogleSignIn = async () => {
try {
setError('');
setLoading(true);
await signInWithGoogle();
router.push('/');
} catch (error: any) {
setError(error.message || 'Failed to sign up with Google');
} finally {
setLoading(false);
}
};
return (
<div className="min-h-screen flex items-center justify-center bg-gradient-to-b from-background to-background/95 px-4">
<div className="max-w-md w-full space-y-8 bg-white/5 backdrop-blur-sm p-8 rounded-2xl shadow-xl">
<div>
<h2 className="mt-6 text-center text-3xl font-bold bg-clip-text text-transparent bg-gradient-to-r from-purple-600 to-pink-600">
Create your account
</h2>
</div>
{error && (
<div className="bg-red-500/10 border border-red-500/20 text-red-500 px-4 py-3 rounded-lg text-sm">
{error}
</div>
)}
<form className="mt-8 space-y-6" onSubmit={handleSubmit}>
<div className="space-y-4">
<div>
<label htmlFor="email" className="block text-sm font-medium text-foreground/80">
Email address
</label>
<input
id="email"
name="email"
type="email"
autoComplete="email"
required
value={email}
onChange={(e) => setEmail(e.target.value)}
className="mt-1 block w-full px-4 py-3 bg-background/50 border border-foreground/10 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent transition-colors"
placeholder="Enter your email"
/>
</div>
<div>
<label htmlFor="password" className="block text-sm font-medium text-foreground/80">
Password
</label>
<input
id="password"
name="password"
type="password"
autoComplete="new-password"
required
value={password}
onChange={(e) => setPassword(e.target.value)}
className="mt-1 block w-full px-4 py-3 bg-background/50 border border-foreground/10 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent transition-colors"
placeholder="Create a password"
/>
</div>
<div>
<label htmlFor="confirm-password" className="block text-sm font-medium text-foreground/80">
Confirm Password
</label>
<input
id="confirm-password"
name="confirm-password"
type="password"
autoComplete="new-password"
required
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
className="mt-1 block w-full px-4 py-3 bg-background/50 border border-foreground/10 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent transition-colors"
placeholder="Confirm your password"
/>
</div>
</div>
<div>
<button
type="submit"
disabled={loading}
className="w-full flex justify-center py-3 px-4 border border-transparent rounded-lg shadow-sm text-sm font-medium text-white bg-gradient-to-r from-purple-600 to-pink-600 hover:opacity-90 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-purple-500 transition-opacity disabled:opacity-50 disabled:cursor-not-allowed"
>
{loading ? 'Creating account...' : 'Create account'}
</button>
</div>
<div className="relative">
<div className="absolute inset-0 flex items-center">
<div className="w-full border-t border-foreground/10"></div>
</div>
<div className="relative flex justify-center text-sm">
<span className="px-2 bg-background text-foreground/60">Or continue with</span>
</div>
</div>
<div>
<button
type="button"
onClick={handleGoogleSignIn}
disabled={loading}
className="w-full flex justify-center items-center gap-2 py-3 px-4 border border-foreground/10 rounded-lg shadow-sm text-sm font-medium text-foreground bg-background/50 hover:bg-background/80 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-purple-500 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
>
<svg className="w-5 h-5" viewBox="0 0 24 24">
<path
fill="currentColor"
d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"
/>
<path
fill="currentColor"
d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"
/>
<path
fill="currentColor"
d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"
/>
<path
fill="currentColor"
d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"
/>
</svg>
Sign up with Google
</button>
</div>
</form>
<div className="text-center text-sm">
<span className="text-foreground/60">Already have an account? </span>
<Link
href="/auth/login"
className="text-purple-600 hover:text-purple-500 transition-colors"
>
Sign in
</Link>
</div>
</div>
</div>
);
}

235
app/boxes/page.tsx Normal file
View File

@ -0,0 +1,235 @@
'use client';
import { useState } from 'react';
import { useAuth } from '../context/AuthContext';
import Header from '../components/Header';
import PaymentModal from '../components/PaymentModal';
// Mock data for boxes - replace with actual data from your backend
const MOCK_BOXES = [
{
id: '1',
tier: 'Small',
price: 100,
color: 'from-blue-500 to-blue-600',
remaining: 2500,
total: 3000,
image: '/box-small.png',
description: 'Perfect for beginners. Contains common to rare items.',
},
{
id: '2',
tier: 'Medium',
price: 1000,
color: 'from-purple-500 to-purple-600',
remaining: 1500,
total: 2000,
image: '/box-medium.png',
description: 'For serious collectors. Contains rare to epic items.',
},
{
id: '3',
tier: 'Big',
price: 10000,
color: 'from-pink-500 to-pink-600',
remaining: 500,
total: 1000,
image: '/box-big.png',
description: 'Premium boxes with epic to legendary items.',
},
];
type SortOption = 'price-asc' | 'price-desc' | 'remaining-asc' | 'remaining-desc';
type FilterTier = 'all' | 'small' | 'medium' | 'big';
export default function BoxesPage() {
const { user } = useAuth();
const [sortBy, setSortBy] = useState<SortOption>('price-asc');
const [filterTier, setFilterTier] = useState<FilterTier>('all');
const [selectedBox, setSelectedBox] = useState<typeof MOCK_BOXES[0] | null>(null);
const [isPaymentModalOpen, setIsPaymentModalOpen] = useState(false);
const filteredAndSortedBoxes = MOCK_BOXES
.filter(box => filterTier === 'all' || box.tier.toLowerCase() === filterTier)
.sort((a, b) => {
switch (sortBy) {
case 'price-asc':
return a.price - b.price;
case 'price-desc':
return b.price - a.price;
case 'remaining-asc':
return a.remaining - b.remaining;
case 'remaining-desc':
return b.remaining - a.remaining;
default:
return 0;
}
});
const handleOpenBox = (box: typeof MOCK_BOXES[0]) => {
if (!user) {
// Redirect to login or show login modal
return;
}
setSelectedBox(box);
setIsPaymentModalOpen(true);
};
const handleClosePaymentModal = () => {
setIsPaymentModalOpen(false);
setSelectedBox(null);
};
return (
<>
<Header />
<div className="min-h-screen bg-gradient-to-b from-background to-background/95 pt-16">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
{/* Page Header */}
<div className="mb-8">
<h1 className="text-3xl font-bold mb-2">Mystery Boxes</h1>
<p className="text-foreground/60">
Choose your box and discover amazing prizes
</p>
</div>
{/* Filters and Sort */}
<div className="flex flex-col sm:flex-row gap-4 mb-8">
{/* Tier Filter */}
<div className="flex gap-2">
<button
onClick={() => setFilterTier('all')}
className={`px-4 py-2 rounded-lg text-sm font-medium transition-colors ${
filterTier === 'all'
? 'bg-purple-600 text-white'
: 'bg-foreground/5 hover:bg-foreground/10'
}`}
>
All
</button>
<button
onClick={() => setFilterTier('small')}
className={`px-4 py-2 rounded-lg text-sm font-medium transition-colors ${
filterTier === 'small'
? 'bg-blue-600 text-white'
: 'bg-foreground/5 hover:bg-foreground/10'
}`}
>
Small
</button>
<button
onClick={() => setFilterTier('medium')}
className={`px-4 py-2 rounded-lg text-sm font-medium transition-colors ${
filterTier === 'medium'
? 'bg-purple-600 text-white'
: 'bg-foreground/5 hover:bg-foreground/10'
}`}
>
Medium
</button>
<button
onClick={() => setFilterTier('big')}
className={`px-4 py-2 rounded-lg text-sm font-medium transition-colors ${
filterTier === 'big'
? 'bg-pink-600 text-white'
: 'bg-foreground/5 hover:bg-foreground/10'
}`}
>
Big
</button>
</div>
{/* Sort Dropdown */}
<select
value={sortBy}
onChange={(e) => setSortBy(e.target.value as SortOption)}
className="ml-auto px-4 py-2 rounded-lg bg-foreground/5 border border-foreground/10 text-sm focus:outline-none focus:ring-2 focus:ring-purple-500"
>
<option value="price-asc">Price: Low to High</option>
<option value="price-desc">Price: High to Low</option>
<option value="remaining-desc">Most Available</option>
<option value="remaining-asc">Least Available</option>
</select>
</div>
{/* Boxes Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{filteredAndSortedBoxes.map((box) => (
<div
key={box.id}
className="relative group rounded-2xl overflow-hidden bg-gradient-to-br p-[1px] hover:scale-[1.02] transition-transform"
>
<div className={`absolute inset-0 bg-gradient-to-br ${box.color} opacity-20`} />
<div className="relative bg-background/95 p-6 rounded-2xl">
{/* Box Image */}
<div className="aspect-square mb-4 rounded-lg bg-foreground/5 flex items-center justify-center">
<div className="text-4xl">🎁</div>
</div>
{/* Box Info */}
<div className="space-y-4">
<div>
<h3 className="text-2xl font-bold mb-1">{box.tier} Box</h3>
<p className="text-foreground/60 text-sm">{box.description}</p>
</div>
{/* Progress Bar */}
<div className="space-y-1">
<div className="flex justify-between text-sm">
<span className="text-foreground/60">Remaining</span>
<span className="font-medium">{box.remaining}/{box.total}</span>
</div>
<div className="h-2 bg-foreground/5 rounded-full overflow-hidden">
<div
className={`h-full bg-gradient-to-r ${box.color} transition-all`}
style={{ width: `${(box.remaining / box.total) * 100}%` }}
/>
</div>
</div>
{/* Price and Action */}
<div className="flex items-center justify-between pt-2">
<div className="flex items-center gap-2">
<span className="text-lg font-semibold">${box.price}</span>
<span className="text-foreground/60 text-sm">USD</span>
</div>
<button
onClick={() => handleOpenBox(box)}
className={`px-4 py-2 rounded-full text-sm font-medium transition-opacity ${
user
? 'bg-gradient-to-r from-purple-600 to-pink-600 text-white hover:opacity-90'
: 'bg-foreground/10 text-foreground/60 cursor-not-allowed'
}`}
disabled={!user}
>
{user ? 'Open Box' : 'Sign in to Open'}
</button>
</div>
</div>
</div>
</div>
))}
</div>
{/* Empty State */}
{filteredAndSortedBoxes.length === 0 && (
<div className="text-center py-12">
<div className="text-4xl mb-4">🔍</div>
<h3 className="text-xl font-semibold mb-2">No boxes found</h3>
<p className="text-foreground/60">
Try adjusting your filters to see more boxes
</p>
</div>
)}
</div>
</div>
{/* Payment Modal */}
<PaymentModal
isOpen={isPaymentModalOpen}
onClose={handleClosePaymentModal}
box={selectedBox}
/>
</>
);
}

147
app/boxes/success/page.tsx Normal file
View File

@ -0,0 +1,147 @@
'use client';
import { useState, useEffect } from 'react';
import { useSearchParams } from 'next/navigation';
import { useAuth } from '../../context/AuthContext';
import Header from '../../components/Header';
// Mock prize data - replace with actual data from your backend
const MOCK_PRIZES = [
{ name: 'Rare NFT', rarity: 'rare', value: 500 },
{ name: 'Epic Collectible', rarity: 'epic', value: 1000 },
{ name: 'Legendary Item', rarity: 'legendary', value: 5000 },
{ name: 'Common Token', rarity: 'common', value: 50 },
];
export default function SuccessPage() {
const { user } = useAuth();
const searchParams = useSearchParams();
const [isOpening, setIsOpening] = useState(true);
const [prize, setPrize] = useState<typeof MOCK_PRIZES[0] | null>(null);
const [showPrize, setShowPrize] = useState(false);
const boxId = searchParams.get('box_id');
const sessionId = searchParams.get('session_id');
useEffect(() => {
// Simulate box opening animation
const timer = setTimeout(() => {
setIsOpening(false);
// Randomly select a prize (replace with actual logic)
const randomPrize = MOCK_PRIZES[Math.floor(Math.random() * MOCK_PRIZES.length)];
setPrize(randomPrize);
setShowPrize(true);
}, 3000);
return () => clearTimeout(timer);
}, []);
if (!user) {
return (
<>
<Header />
<div className="min-h-screen bg-gradient-to-b from-background to-background/95 pt-16">
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-8 text-center">
<h1 className="text-2xl font-bold mb-4">Access Denied</h1>
<p className="text-foreground/60">Please sign in to view this page.</p>
</div>
</div>
</>
);
}
return (
<>
<Header />
<div className="min-h-screen bg-gradient-to-b from-background to-background/95 pt-16">
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
{/* Success Header */}
<div className="text-center mb-8">
<div className="text-6xl mb-4">🎉</div>
<h1 className="text-3xl font-bold mb-2">Payment Successful!</h1>
<p className="text-foreground/60">
Your mystery box is being opened...
</p>
</div>
{/* Box Opening Animation */}
{isOpening && (
<div className="text-center py-12">
<div className="inline-block animate-bounce">
<div className="text-8xl mb-4">🎁</div>
</div>
<p className="text-lg text-foreground/60">Opening your box...</p>
</div>
)}
{/* Prize Reveal */}
{showPrize && prize && (
<div className="text-center">
<div className="bg-gradient-to-br from-purple-500/10 to-pink-500/10 rounded-2xl p-8 mb-8">
<div className="text-6xl mb-4"></div>
<h2 className="text-2xl font-bold mb-2">Congratulations!</h2>
<p className="text-foreground/60 mb-6">
You found something amazing!
</p>
{/* Prize Card */}
<div className="bg-background/50 rounded-xl p-6 max-w-sm mx-auto">
<div className="text-4xl mb-4">🎯</div>
<h3 className="text-xl font-semibold mb-2">{prize.name}</h3>
<div className="flex items-center justify-center gap-2 mb-4">
<span className={`px-3 py-1 rounded-full text-xs font-medium ${
prize.rarity === 'common' ? 'bg-gray-500/20 text-gray-300' :
prize.rarity === 'rare' ? 'bg-blue-500/20 text-blue-300' :
prize.rarity === 'epic' ? 'bg-purple-500/20 text-purple-300' :
'bg-yellow-500/20 text-yellow-300'
}`}>
{prize.rarity.toUpperCase()}
</span>
</div>
<p className="text-foreground/60 text-sm">
Estimated Value: ${prize.value}
</p>
</div>
</div>
{/* Action Buttons */}
<div className="flex flex-col sm:flex-row gap-4 justify-center">
<button
onClick={() => window.location.href = '/inventory'}
className="px-6 py-3 rounded-lg bg-gradient-to-r from-purple-600 to-pink-600 text-white font-semibold hover:opacity-90 transition-opacity"
>
View in Inventory
</button>
<button
onClick={() => window.location.href = '/boxes'}
className="px-6 py-3 rounded-lg border border-foreground/20 hover:bg-foreground/5 transition-colors"
>
Open Another Box
</button>
</div>
</div>
)}
{/* Transaction Details */}
<div className="mt-12 bg-foreground/5 rounded-xl p-6">
<h3 className="text-lg font-semibold mb-4">Transaction Details</h3>
<div className="space-y-2 text-sm">
<div className="flex justify-between">
<span className="text-foreground/60">Session ID:</span>
<span className="font-mono">{sessionId}</span>
</div>
<div className="flex justify-between">
<span className="text-foreground/60">Box ID:</span>
<span className="font-mono">{boxId}</span>
</div>
<div className="flex justify-between">
<span className="text-foreground/60">User:</span>
<span>{user.email}</span>
</div>
</div>
</div>
</div>
</div>
</>
);
}

105
app/components/Header.tsx Normal file
View File

@ -0,0 +1,105 @@
'use client';
import { useAuth } from '../context/AuthContext';
import Link from 'next/link';
import { useState } from 'react';
export default function Header() {
const { user, logout } = useAuth();
const [isProfileMenuOpen, setIsProfileMenuOpen] = useState(false);
const handleLogout = async () => {
try {
await logout();
setIsProfileMenuOpen(false);
} catch (error) {
console.error('Failed to log out:', error);
}
};
return (
<header className="fixed top-0 left-0 right-0 z-50 bg-background/80 backdrop-blur-sm border-b border-foreground/10">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex justify-between items-center h-16">
{/* Logo */}
<Link href="/" className="flex items-center">
<span className="text-xl font-bold bg-clip-text text-transparent bg-gradient-to-r from-purple-600 to-pink-600">
Mystery Box
</span>
</Link>
{/* Navigation */}
<nav className="hidden md:flex items-center space-x-8">
<Link href="/boxes" className="text-foreground/80 hover:text-foreground transition-colors">
Boxes
</Link>
<Link href="/inventory" className="text-foreground/80 hover:text-foreground transition-colors">
Inventory
</Link>
<Link href="/trading" className="text-foreground/80 hover:text-foreground transition-colors">
Trading
</Link>
</nav>
{/* Auth Buttons / Profile */}
<div className="flex items-center space-x-4">
{user ? (
<div className="relative">
<button
onClick={() => setIsProfileMenuOpen(!isProfileMenuOpen)}
className="flex items-center space-x-2 text-foreground/80 hover:text-foreground transition-colors"
>
<div className="w-8 h-8 rounded-full bg-gradient-to-r from-purple-600 to-pink-600 flex items-center justify-center text-white text-sm font-medium">
{user.email?.[0].toUpperCase()}
</div>
<span className="hidden md:inline-block">{user.email}</span>
</button>
{/* Profile Dropdown */}
{isProfileMenuOpen && (
<div className="absolute right-0 mt-2 w-48 rounded-lg bg-background border border-foreground/10 shadow-lg py-1">
<Link
href="/profile"
className="block px-4 py-2 text-sm text-foreground/80 hover:bg-foreground/5 transition-colors"
onClick={() => setIsProfileMenuOpen(false)}
>
Profile
</Link>
<Link
href="/settings"
className="block px-4 py-2 text-sm text-foreground/80 hover:bg-foreground/5 transition-colors"
onClick={() => setIsProfileMenuOpen(false)}
>
Settings
</Link>
<button
onClick={handleLogout}
className="block w-full text-left px-4 py-2 text-sm text-red-500 hover:bg-foreground/5 transition-colors"
>
Sign out
</button>
</div>
)}
</div>
) : (
<div className="flex items-center space-x-4">
<Link
href="/auth/login"
className="text-foreground/80 hover:text-foreground transition-colors"
>
Sign in
</Link>
<Link
href="/auth/signup"
className="px-4 py-2 rounded-lg bg-gradient-to-r from-purple-600 to-pink-600 text-white text-sm font-medium hover:opacity-90 transition-opacity"
>
Sign up
</Link>
</div>
)}
</div>
</div>
</div>
</header>
);
}

View File

@ -0,0 +1,153 @@
'use client';
import { useState, useEffect } from 'react';
import { useAuth } from '../context/AuthContext';
import { getStripe } from '../lib/stripe';
interface PaymentModalProps {
isOpen: boolean;
onClose: () => void;
box: {
id: string;
tier: string;
price: number;
description: string;
} | null;
}
export default function PaymentModal({ isOpen, onClose, box }: PaymentModalProps) {
const { user } = useAuth();
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const handlePayment = async () => {
if (!user || !box) return;
setIsLoading(true);
setError(null);
try {
console.log('Creating checkout session for:', box);
// Create checkout session
const response = await fetch('/api/create-checkout-session', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
boxId: box.id,
boxName: box.tier,
amount: box.price,
currency: 'usd',
}),
});
console.log('Checkout session response status:', response.status);
if (!response.ok) {
const errorData = await response.json();
console.error('Checkout session error:', errorData);
throw new Error(errorData.error || 'Failed to create checkout session');
}
const { sessionId } = await response.json();
console.log('Checkout session created:', { sessionId });
if (!sessionId) {
throw new Error('No session ID received from server');
}
// Redirect to Stripe Checkout
const stripe = await getStripe();
if (!stripe) {
throw new Error('Stripe failed to load');
}
console.log('Redirecting to Stripe Checkout...');
const { error: stripeError } = await stripe.redirectToCheckout({
sessionId,
});
if (stripeError) {
console.error('Stripe error:', stripeError);
throw new Error(stripeError.message);
}
} catch (err) {
console.error('Payment error:', err);
setError(err instanceof Error ? err.message : 'Payment failed');
} finally {
setIsLoading(false);
}
};
if (!isOpen || !box) return null;
return (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
<div className="bg-background rounded-2xl p-6 max-w-md w-full">
{/* Header */}
<div className="flex items-center justify-between mb-6">
<h2 className="text-2xl font-bold">Open {box.tier} Box</h2>
<button
onClick={onClose}
className="text-foreground/60 hover:text-foreground transition-colors"
>
</button>
</div>
{/* Box Details */}
<div className="mb-6">
<div className="flex items-center gap-4 mb-4">
<div className="w-16 h-16 bg-gradient-to-br from-purple-500 to-pink-500 rounded-lg flex items-center justify-center">
<span className="text-2xl">🎁</span>
</div>
<div>
<h3 className="text-xl font-semibold">{box.tier} Box</h3>
<p className="text-foreground/60 text-sm">{box.description}</p>
</div>
</div>
<div className="bg-foreground/5 rounded-lg p-4">
<div className="flex justify-between items-center">
<span className="text-foreground/60">Price:</span>
<span className="text-xl font-bold">${box.price}</span>
</div>
</div>
</div>
{/* Error Message */}
{error && (
<div className="mb-4 p-3 bg-red-500/10 border border-red-500/20 rounded-lg">
<p className="text-red-500 text-sm">{error}</p>
</div>
)}
{/* Action Buttons */}
<div className="flex gap-3">
<button
onClick={onClose}
className="flex-1 px-4 py-3 rounded-lg border border-foreground/20 hover:bg-foreground/5 transition-colors"
disabled={isLoading}
>
Cancel
</button>
<button
onClick={handlePayment}
disabled={isLoading}
className="flex-1 px-4 py-3 rounded-lg bg-gradient-to-r from-purple-600 to-pink-600 text-white font-semibold hover:opacity-90 transition-opacity disabled:opacity-50"
>
{isLoading ? 'Processing...' : 'Pay & Open Box'}
</button>
</div>
{/* Security Notice */}
<p className="text-xs text-foreground/40 text-center mt-4">
Your payment is secured by Stripe. We never store your card details.
</p>
</div>
</div>
);
}

105
app/context/AuthContext.tsx Normal file
View File

@ -0,0 +1,105 @@
'use client';
import { createContext, useContext, useEffect, useState } from 'react';
import {
User,
signInWithEmailAndPassword,
createUserWithEmailAndPassword,
signOut,
onAuthStateChanged,
GoogleAuthProvider,
signInWithPopup,
sendPasswordResetEmail,
} from 'firebase/auth';
import { auth } from '../lib/firebase';
interface AuthContextType {
user: User | null;
loading: boolean;
signIn: (email: string, password: string) => Promise<void>;
signUp: (email: string, password: string) => Promise<void>;
signInWithGoogle: () => Promise<void>;
logout: () => Promise<void>;
resetPassword: (email: string) => Promise<void>;
}
const AuthContext = createContext<AuthContextType>({} as AuthContextType);
export function AuthProvider({ children }: { children: React.ReactNode }) {
const [user, setUser] = useState<User | null>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
const unsubscribe = onAuthStateChanged(auth, (user) => {
setUser(user);
setLoading(false);
});
return () => unsubscribe();
}, []);
const signIn = async (email: string, password: string) => {
try {
await signInWithEmailAndPassword(auth, email, password);
} catch (error) {
throw error;
}
};
const signUp = async (email: string, password: string) => {
try {
await createUserWithEmailAndPassword(auth, email, password);
} catch (error) {
throw error;
}
};
const signInWithGoogle = async () => {
try {
const provider = new GoogleAuthProvider();
await signInWithPopup(auth, provider);
} catch (error) {
throw error;
}
};
const logout = async () => {
try {
await signOut(auth);
} catch (error) {
throw error;
}
};
const resetPassword = async (email: string) => {
try {
await sendPasswordResetEmail(auth, email);
} catch (error) {
throw error;
}
};
const value = {
user,
loading,
signIn,
signUp,
signInWithGoogle,
logout,
resetPassword,
};
return (
<AuthContext.Provider value={value}>
{!loading && children}
</AuthContext.Provider>
);
}
export const useAuth = () => {
const context = useContext(AuthContext);
if (!context) {
throw new Error('useAuth must be used within an AuthProvider');
}
return context;
};

View File

@ -1,20 +1,13 @@
import type { Metadata } from "next"; import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google"; import { Inter } from "next/font/google";
import "./globals.css"; import "./globals.css";
import { AuthProvider } from "./context/AuthContext";
const geistSans = Geist({ const inter = Inter({ subsets: ["latin"] });
variable: "--font-geist-sans",
subsets: ["latin"],
});
const geistMono = Geist_Mono({
variable: "--font-geist-mono",
subsets: ["latin"],
});
export const metadata: Metadata = { export const metadata: Metadata = {
title: "Create Next App", title: "Mystery Box Platform",
description: "Generated by create next app", description: "Discover amazing prizes in our mystery boxes",
}; };
export default function RootLayout({ export default function RootLayout({
@ -24,10 +17,10 @@ export default function RootLayout({
}>) { }>) {
return ( return (
<html lang="en"> <html lang="en">
<body <body className={inter.className}>
className={`${geistSans.variable} ${geistMono.variable} antialiased`} <AuthProvider>
> {children}
{children} </AuthProvider>
</body> </body>
</html> </html>
); );

17
app/lib/firebase-admin.ts Normal file
View File

@ -0,0 +1,17 @@
import { initializeApp, getApps, cert } from 'firebase-admin/app';
import { getAuth } from 'firebase-admin/auth';
// Initialize Firebase Admin
const apps = getApps();
if (!apps.length) {
initializeApp({
credential: cert({
projectId: process.env.FIREBASE_PROJECT_ID,
clientEmail: process.env.FIREBASE_CLIENT_EMAIL,
privateKey: process.env.FIREBASE_PRIVATE_KEY?.replace(/\\n/g, '\n'),
}),
});
}
export const auth = getAuth();

18
app/lib/firebase.ts Normal file
View File

@ -0,0 +1,18 @@
import { initializeApp, getApps } from 'firebase/app';
import { getAuth } from 'firebase/auth';
const firebaseConfig = {
// Replace these with your Firebase config values
apiKey: process.env.NEXT_PUBLIC_FIREBASE_API_KEY,
authDomain: process.env.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN,
projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID,
storageBucket: process.env.NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET,
messagingSenderId: process.env.NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID,
appId: process.env.NEXT_PUBLIC_FIREBASE_APP_ID,
};
// Initialize Firebase
const app = getApps().length === 0 ? initializeApp(firebaseConfig) : getApps()[0];
const auth = getAuth(app);
export { app, auth };

98
app/lib/stripe.ts Normal file
View File

@ -0,0 +1,98 @@
import { loadStripe } from '@stripe/stripe-js';
// Initialize Stripe on the client
export const getStripe = () => {
const stripePromise = loadStripe(process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY!);
return stripePromise;
};
// Helper function to create a payment intent (server-side only)
export async function createPaymentIntent(amount: number, currency: string = 'usd') {
try {
console.log('Creating Stripe payment intent with:', { amount, currency });
// This function should only be called from server-side code
const { default: Stripe } = await import('stripe');
const stripeSecretKey = process.env.STRIPE_SECRET_KEY;
if (!stripeSecretKey) {
throw new Error('STRIPE_SECRET_KEY is not configured');
}
const stripe = new Stripe(stripeSecretKey, {
apiVersion: '2025-05-28.basil',
typescript: true,
});
console.log('Stripe instance created, creating payment intent...');
const paymentIntent = await stripe.paymentIntents.create({
amount: Math.round(amount * 100), // Convert to cents
currency,
automatic_payment_methods: {
enabled: true,
},
});
console.log('Payment intent created:', {
id: paymentIntent.id,
amount: paymentIntent.amount,
currency: paymentIntent.currency,
status: paymentIntent.status
});
return {
clientSecret: paymentIntent.client_secret,
paymentIntentId: paymentIntent.id,
};
} catch (error) {
console.error('Error creating payment intent:', error);
throw error;
}
}
// Helper function to create a checkout session (server-side only)
export async function createCheckoutSession(
boxId: string,
boxName: string,
amount: number,
currency: string = 'usd'
) {
try {
// This function should only be called from server-side code
const { default: Stripe } = await import('stripe');
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
apiVersion: '2025-05-28.basil',
typescript: true,
});
const session = await stripe.checkout.sessions.create({
payment_method_types: ['card'],
line_items: [
{
price_data: {
currency,
product_data: {
name: boxName,
description: `Mystery Box Purchase - ${boxName}`,
},
unit_amount: Math.round(amount * 100), // Convert to cents
},
quantity: 1,
},
],
mode: 'payment',
success_url: `${process.env.NEXT_PUBLIC_BASE_URL}/boxes/success?box_id=${boxId}`,
cancel_url: `${process.env.NEXT_PUBLIC_BASE_URL}/boxes?canceled=true`,
metadata: {
boxId,
},
});
return session;
} catch (error) {
console.error('Error creating checkout session:', error);
throw error;
}
}

View File

@ -1,103 +1,153 @@
'use client';
import Image from "next/image"; import Image from "next/image";
import Link from "next/link";
import Header from "./components/Header";
import { useAuth } from "./context/AuthContext";
export default function Home() { export default function Home() {
return ( const { user } = useAuth();
<div className="grid grid-rows-[20px_1fr_20px] items-center justify-items-center min-h-screen p-8 pb-20 gap-16 sm:p-20 font-[family-name:var(--font-geist-sans)]">
<main className="flex flex-col gap-[32px] row-start-2 items-center sm:items-start">
<Image
className="dark:invert"
src="/next.svg"
alt="Next.js logo"
width={180}
height={38}
priority
/>
<ol className="list-inside list-decimal text-sm/6 text-center sm:text-left font-[family-name:var(--font-geist-mono)]">
<li className="mb-2 tracking-[-.01em]">
Get started by editing{" "}
<code className="bg-black/[.05] dark:bg-white/[.06] px-1 py-0.5 rounded font-[family-name:var(--font-geist-mono)] font-semibold">
app/page.tsx
</code>
.
</li>
<li className="tracking-[-.01em]">
Save and see your changes instantly.
</li>
</ol>
<div className="flex gap-4 items-center flex-col sm:flex-row"> return (
<a <>
className="rounded-full border border-solid border-transparent transition-colors flex items-center justify-center bg-foreground text-background gap-2 hover:bg-[#383838] dark:hover:bg-[#ccc] font-medium text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 sm:w-auto" <Header />
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app" <div className="min-h-screen bg-gradient-to-b from-background to-background/95 pt-16">
target="_blank" {/* Hero Section */}
rel="noopener noreferrer" <section className="relative min-h-[calc(100vh-4rem)] flex items-center justify-center px-4 sm:px-6 lg:px-8">
> <div className="absolute inset-0 bg-[url('/hero-bg.jpg')] bg-cover bg-center opacity-10" />
<Image <div className="relative z-10 text-center max-w-4xl mx-auto">
className="dark:invert" <h1 className="text-4xl sm:text-6xl font-bold mb-6 bg-clip-text text-transparent bg-gradient-to-r from-purple-600 to-pink-600">
src="/vercel.svg" {user ? 'Welcome Back!' : 'Discover Amazing Prizes'}
alt="Vercel logomark" </h1>
width={20} <p className="text-lg sm:text-xl mb-8 text-foreground/80">
height={20} {user
/> ? 'Ready to open some mystery boxes? Browse our collection and start your adventure!'
Deploy now : 'Unlock exclusive mystery boxes and win incredible prizes. Trade, collect, and experience the thrill of discovery.'}
</a> </p>
<a <div className="flex flex-col sm:flex-row gap-4 justify-center">
className="rounded-full border border-solid border-black/[.08] dark:border-white/[.145] transition-colors flex items-center justify-center hover:bg-[#f2f2f2] dark:hover:bg-[#1a1a1a] hover:border-transparent font-medium text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 w-full sm:w-auto md:w-[158px]" {user ? (
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app" <Link
target="_blank" href="/boxes"
rel="noopener noreferrer" className="px-8 py-3 rounded-full bg-gradient-to-r from-purple-600 to-pink-600 text-white font-semibold hover:opacity-90 transition-opacity"
> >
Read our docs Open Boxes
</a> </Link>
</div> ) : (
</main> <>
<footer className="row-start-3 flex gap-[24px] flex-wrap items-center justify-center"> <Link
<a href="/auth/signup"
className="flex items-center gap-2 hover:underline hover:underline-offset-4" className="px-8 py-3 rounded-full bg-gradient-to-r from-purple-600 to-pink-600 text-white font-semibold hover:opacity-90 transition-opacity"
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app" >
target="_blank" Get Started
rel="noopener noreferrer" </Link>
> <Link
<Image href="/boxes"
aria-hidden className="px-8 py-3 rounded-full border border-foreground/20 hover:bg-foreground/5 transition-colors"
src="/file.svg" >
alt="File icon" View Boxes
width={16} </Link>
height={16} </>
/> )}
Learn </div>
</a> </div>
<a </section>
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app" {/* Featured Boxes Section */}
target="_blank" <section className="py-20 px-4 sm:px-6 lg:px-8">
rel="noopener noreferrer" <div className="max-w-7xl mx-auto">
> <h2 className="text-3xl font-bold text-center mb-12">Featured Mystery Boxes</h2>
<Image <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-8">
aria-hidden {[
src="/window.svg" { tier: "Small", price: "100", color: "from-blue-500 to-blue-600" },
alt="Window icon" { tier: "Medium", price: "1,000", color: "from-purple-500 to-purple-600" },
width={16} { tier: "Big", price: "10,000", color: "from-pink-500 to-pink-600" },
height={16} ].map((box) => (
/> <div
Examples key={box.tier}
</a> className="relative group rounded-2xl overflow-hidden bg-gradient-to-br p-[1px] hover:scale-105 transition-transform"
<a >
className="flex items-center gap-2 hover:underline hover:underline-offset-4" <div className={`absolute inset-0 bg-gradient-to-br ${box.color} opacity-20`} />
href="https://nextjs.org?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app" <div className="relative bg-background/95 p-6 rounded-2xl">
target="_blank" <h3 className="text-2xl font-bold mb-2">{box.tier} Box</h3>
rel="noopener noreferrer" <p className="text-foreground/60 mb-4">Unlock amazing prizes</p>
> <div className="flex items-center justify-between">
<Image <span className="text-lg font-semibold">{box.price} Tokens</span>
aria-hidden <button className="px-4 py-2 rounded-full bg-gradient-to-r from-purple-600 to-pink-600 text-white text-sm font-medium hover:opacity-90 transition-opacity">
src="/globe.svg" Open Box
alt="Globe icon" </button>
width={16} </div>
height={16} </div>
/> </div>
Go to nextjs.org ))}
</a> </div>
</footer> </div>
</div> </section>
{/* How It Works Section */}
<section className="py-20 px-4 sm:px-6 lg:px-8 bg-foreground/[0.02]">
<div className="max-w-7xl mx-auto">
<h2 className="text-3xl font-bold text-center mb-12">How It Works</h2>
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
{[
{
title: "Get Tokens",
description: "Purchase tokens using various payment methods including cards and crypto",
icon: "💳",
},
{
title: "Open Boxes",
description: "Choose your mystery box tier and experience the thrill of discovery",
icon: "🎁",
},
{
title: "Trade & Collect",
description: "Trade your prizes with other users or add them to your collection",
icon: "🔄",
},
].map((step) => (
<div key={step.title} className="text-center p-6 rounded-xl bg-background/50">
<div className="text-4xl mb-4">{step.icon}</div>
<h3 className="text-xl font-semibold mb-2">{step.title}</h3>
<p className="text-foreground/60">{step.description}</p>
</div>
))}
</div>
</div>
</section>
{/* Recent Wins Section */}
<section className="py-20 px-4 sm:px-6 lg:px-8">
<div className="max-w-7xl mx-auto">
<h2 className="text-3xl font-bold text-center mb-12">Recent Wins</h2>
<div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 gap-4">
{[1, 2, 3, 4].map((i) => (
<div key={i} className="aspect-square rounded-xl bg-foreground/[0.02] p-4 flex items-center justify-center">
<div className="text-center">
<div className="text-2xl mb-2">🎉</div>
<p className="text-sm text-foreground/60">Amazing Prize #{i}</p>
</div>
</div>
))}
</div>
</div>
</section>
{/* CTA Section */}
<section className="py-20 px-4 sm:px-6 lg:px-8 bg-gradient-to-r from-purple-600/10 to-pink-600/10">
<div className="max-w-4xl mx-auto text-center">
<h2 className="text-3xl font-bold mb-6">Ready to Start Your Journey?</h2>
<p className="text-lg text-foreground/80 mb-8">
Join thousands of users already discovering amazing prizes in our mystery boxes.
</p>
<Link
href="/auth/signup"
className="inline-block px-8 py-3 rounded-full bg-gradient-to-r from-purple-600 to-pink-600 text-white font-semibold hover:opacity-90 transition-opacity"
>
Create Account
</Link>
</div>
</section>
</div>
</>
); );
} }

2272
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -9,19 +9,25 @@
"lint": "next lint" "lint": "next lint"
}, },
"dependencies": { "dependencies": {
"@firebase/auth": "^1.10.7",
"@stripe/stripe-js": "^7.3.1",
"firebase": "^11.9.1",
"firebase-admin": "^13.4.0",
"next": "15.3.3",
"postcss": "^8.5.6",
"react": "^19.0.0", "react": "^19.0.0",
"react-dom": "^19.0.0", "react-dom": "^19.0.0",
"next": "15.3.3" "stripe": "^18.2.1"
}, },
"devDependencies": { "devDependencies": {
"typescript": "^5", "@eslint/eslintrc": "^3",
"@tailwindcss/postcss": "^4.1.10",
"@types/node": "^20", "@types/node": "^20",
"@types/react": "^19", "@types/react": "^19",
"@types/react-dom": "^19", "@types/react-dom": "^19",
"@tailwindcss/postcss": "^4",
"tailwindcss": "^4",
"eslint": "^9", "eslint": "^9",
"eslint-config-next": "15.3.3", "eslint-config-next": "15.3.3",
"@eslint/eslintrc": "^3" "tailwindcss": "^4.1.10",
"typescript": "^5"
} }
} }

1
public/hero-bg.jpg Normal file
View File

@ -0,0 +1 @@
data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEASABIAAD/4gHYSUNDX1BST0ZJTEUAAQEAAAHIAAAAAAQwAABtbnRyUkdCIFhZWiAH4AABAAEAAAAAAABhY3NwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAA9tYAAQAAAADTLQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAlkZXNjAAAA8AAAACRyWFlaAAABFAAAABRnWFlaAAABKAAAABRiWFlaAAABPAAAABR3dHB0AAABUAAAABRyVFJDAAABZAAAAChnVFJDAAABZAAAAChiVFJDAAABZAAAAChjcHJ0AAABjAAAADxtbHVjAAAAAAAAAAEAAAAMZW5VUwAAAAgAAAAcAHMAUgBHAEJYWVogAAAAAAAAb6IAADj1AAADkFhZWiAAAAAAAABimQAAt4UAABjaWFlaIAAAAAAAACSgAAAPhAAAts9YWVogAAAAAAAA9tYAAQAAAADTLXBhcmEAAAAAAAQAAAACZmYAAPKnAAANWQAAE9AAAApbAAAAAAAAAABtbHVjAAAAAAAAAAEAAAAMZW5VUwAAACAAAAAcAEcAbwBvAGcAbABlACAASQBuAGMALgAgADIAMAAxADb/2wBDABQODxIPDRQSEBIXFRQdHx4eHRoaHSQtJSEkMjU1LS0yMi4qLjgyPj4+Oj5CQkJCQkJCQkJCQkJCQkJCQkJCQkL/2wBDAR4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh7/wAARCAAIAAoDASIAAhEBAxEB/8QAFQABAQAAAAAAAAAAAAAAAAAAAAb/xAAUEAEAAAAAAAAAAAAAAAAAAAAA/8QAFQEBAQAAAAAAAAAAAAAAAAAAAAX/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwCdABmX/9k=