init
This commit is contained in:
parent
1028d24bc1
commit
b697bb176a
51
app/api/create-checkout-session/route.ts
Normal file
51
app/api/create-checkout-session/route.ts
Normal 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 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
42
app/api/create-payment-intent/route.ts
Normal file
42
app/api/create-payment-intent/route.ts
Normal 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
167
app/auth/login/page.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
93
app/auth/reset-password/page.tsx
Normal file
93
app/auth/reset-password/page.tsx
Normal 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
179
app/auth/signup/page.tsx
Normal 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
235
app/boxes/page.tsx
Normal 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
147
app/boxes/success/page.tsx
Normal 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
105
app/components/Header.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
153
app/components/PaymentModal.tsx
Normal file
153
app/components/PaymentModal.tsx
Normal 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
105
app/context/AuthContext.tsx
Normal 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;
|
||||||
|
};
|
||||||
|
|
@ -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
17
app/lib/firebase-admin.ts
Normal 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
18
app/lib/firebase.ts
Normal 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
98
app/lib/stripe.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
244
app/page.tsx
244
app/page.tsx
|
|
@ -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
2272
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
16
package.json
16
package.json
|
|
@ -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
1
public/hero-bg.jpg
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEASABIAAD/4gHYSUNDX1BST0ZJTEUAAQEAAAHIAAAAAAQwAABtbnRyUkdCIFhZWiAH4AABAAEAAAAAAABhY3NwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAA9tYAAQAAAADTLQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAlkZXNjAAAA8AAAACRyWFlaAAABFAAAABRnWFlaAAABKAAAABRiWFlaAAABPAAAABR3dHB0AAABUAAAABRyVFJDAAABZAAAAChnVFJDAAABZAAAAChiVFJDAAABZAAAAChjcHJ0AAABjAAAADxtbHVjAAAAAAAAAAEAAAAMZW5VUwAAAAgAAAAcAHMAUgBHAEJYWVogAAAAAAAAb6IAADj1AAADkFhZWiAAAAAAAABimQAAt4UAABjaWFlaIAAAAAAAACSgAAAPhAAAts9YWVogAAAAAAAA9tYAAQAAAADTLXBhcmEAAAAAAAQAAAACZmYAAPKnAAANWQAAE9AAAApbAAAAAAAAAABtbHVjAAAAAAAAAAEAAAAMZW5VUwAAACAAAAAcAEcAbwBvAGcAbABlACAASQBuAGMALgAgADIAMAAxADb/2wBDABQODxIPDRQSEBIXFRQdHx4eHRoaHSQtJSEkMjU1LS0yMi4qLjgyPj4+Oj5CQkJCQkJCQkJCQkJCQkJCQkJCQkL/2wBDAR4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh7/wAARCAAIAAoDASIAAhEBAxEB/8QAFQABAQAAAAAAAAAAAAAAAAAAAAb/xAAUEAEAAAAAAAAAAAAAAAAAAAAA/8QAFQEBAQAAAAAAAAAAAAAAAAAAAAX/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwCdABmX/9k=
|
||||||
Loading…
Reference in New Issue
Block a user