Skip to content

Commit b727e7b

Browse files
CopilotSnozxyx
andcommitted
Add user authentication system and backend integration
Co-authored-by: Snozxyx <150821778+Snozxyx@users.noreply.github.com>
1 parent 93a19bb commit b727e7b

File tree

3,375 files changed

+812
-405903
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

3,375 files changed

+812
-405903
lines changed
Lines changed: 232 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,232 @@
1+
'use client';
2+
3+
import React, { useState } from 'react';
4+
import { motion, AnimatePresence } from 'framer-motion';
5+
import { Button } from '@/components/ui/button';
6+
import { Input } from '@/components/ui/input';
7+
import { Label } from '@/components/ui/label';
8+
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
9+
import { Alert, AlertDescription } from '@/components/ui/alert';
10+
import { Eye, EyeOff, User, Mail, Lock } from 'lucide-react';
11+
import { useAuth } from '@/lib/auth/auth-context';
12+
13+
interface AuthFormProps {
14+
isOpen: boolean;
15+
onClose: () => void;
16+
defaultMode?: 'login' | 'register';
17+
}
18+
19+
const AuthForm: React.FC<AuthFormProps> = ({ isOpen, onClose, defaultMode = 'login' }) => {
20+
const [mode, setMode] = useState<'login' | 'register'>(defaultMode);
21+
const [showPassword, setShowPassword] = useState(false);
22+
const [isLoading, setIsLoading] = useState(false);
23+
const [error, setError] = useState<string | null>(null);
24+
25+
const [formData, setFormData] = useState({
26+
username: '',
27+
email: '',
28+
password: '',
29+
displayName: '',
30+
});
31+
32+
const { login, register } = useAuth();
33+
34+
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
35+
const { name, value } = e.target;
36+
setFormData(prev => ({ ...prev, [name]: value }));
37+
setError(null);
38+
};
39+
40+
const handleSubmit = async (e: React.FormEvent) => {
41+
e.preventDefault();
42+
setIsLoading(true);
43+
setError(null);
44+
45+
try {
46+
let response;
47+
48+
if (mode === 'login') {
49+
response = await login(formData.email, formData.password);
50+
} else {
51+
response = await register({
52+
username: formData.username,
53+
email: formData.email,
54+
password: formData.password,
55+
displayName: formData.displayName || formData.username,
56+
});
57+
}
58+
59+
if (response.success) {
60+
onClose();
61+
setFormData({ username: '', email: '', password: '', displayName: '' });
62+
} else {
63+
setError(response.message || 'An error occurred');
64+
}
65+
} catch (error: unknown) {
66+
const errorMessage = error instanceof Error ? error.message : 'An error occurred';
67+
setError(errorMessage);
68+
} finally {
69+
setIsLoading(false);
70+
}
71+
};
72+
73+
const switchMode = () => {
74+
setMode(mode === 'login' ? 'register' : 'login');
75+
setError(null);
76+
setFormData({ username: '', email: '', password: '', displayName: '' });
77+
};
78+
79+
if (!isOpen) return null;
80+
81+
return (
82+
<AnimatePresence>
83+
<motion.div
84+
initial={{ opacity: 0 }}
85+
animate={{ opacity: 1 }}
86+
exit={{ opacity: 0 }}
87+
className="fixed inset-0 bg-black/80 backdrop-blur-sm z-50 flex items-center justify-center p-4"
88+
onClick={onClose}
89+
>
90+
<motion.div
91+
initial={{ opacity: 0, scale: 0.95, y: 20 }}
92+
animate={{ opacity: 1, scale: 1, y: 0 }}
93+
exit={{ opacity: 0, scale: 0.95, y: 20 }}
94+
className="w-full max-w-md"
95+
onClick={(e) => e.stopPropagation()}
96+
>
97+
<Card className="bg-gray-900/95 border-gray-700/50 backdrop-blur-lg">
98+
<CardHeader className="text-center">
99+
<CardTitle className="text-2xl font-bold text-white">
100+
{mode === 'login' ? 'Welcome Back' : 'Join Tatakai'}
101+
</CardTitle>
102+
<CardDescription className="text-gray-400">
103+
{mode === 'login'
104+
? 'Sign in to your account to continue watching'
105+
: 'Create an account to track your progress and favorites'
106+
}
107+
</CardDescription>
108+
</CardHeader>
109+
110+
<CardContent className="space-y-6">
111+
{error && (
112+
<Alert className="bg-red-500/10 border-red-500/30 text-red-400">
113+
<AlertDescription>{error}</AlertDescription>
114+
</Alert>
115+
)}
116+
117+
<form onSubmit={handleSubmit} className="space-y-4">
118+
{mode === 'register' && (
119+
<div className="space-y-2">
120+
<Label htmlFor="username" className="text-gray-300">Username</Label>
121+
<div className="relative">
122+
<User className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-4 h-4" />
123+
<Input
124+
id="username"
125+
name="username"
126+
type="text"
127+
placeholder="Choose a username"
128+
value={formData.username}
129+
onChange={handleInputChange}
130+
className="pl-10 bg-gray-800/50 border-gray-600 text-white placeholder-gray-400"
131+
required
132+
/>
133+
</div>
134+
</div>
135+
)}
136+
137+
<div className="space-y-2">
138+
<Label htmlFor="email" className="text-gray-300">Email</Label>
139+
<div className="relative">
140+
<Mail className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-4 h-4" />
141+
<Input
142+
id="email"
143+
name="email"
144+
type="email"
145+
placeholder="Enter your email"
146+
value={formData.email}
147+
onChange={handleInputChange}
148+
className="pl-10 bg-gray-800/50 border-gray-600 text-white placeholder-gray-400"
149+
required
150+
/>
151+
</div>
152+
</div>
153+
154+
<div className="space-y-2">
155+
<Label htmlFor="password" className="text-gray-300">Password</Label>
156+
<div className="relative">
157+
<Lock className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-4 h-4" />
158+
<Input
159+
id="password"
160+
name="password"
161+
type={showPassword ? 'text' : 'password'}
162+
placeholder="Enter your password"
163+
value={formData.password}
164+
onChange={handleInputChange}
165+
className="pl-10 pr-10 bg-gray-800/50 border-gray-600 text-white placeholder-gray-400"
166+
required
167+
minLength={6}
168+
/>
169+
<button
170+
type="button"
171+
onClick={() => setShowPassword(!showPassword)}
172+
className="absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-400 hover:text-gray-300"
173+
>
174+
{showPassword ? <EyeOff className="w-4 h-4" /> : <Eye className="w-4 h-4" />}
175+
</button>
176+
</div>
177+
</div>
178+
179+
{mode === 'register' && (
180+
<div className="space-y-2">
181+
<Label htmlFor="displayName" className="text-gray-300">Display Name (Optional)</Label>
182+
<div className="relative">
183+
<User className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-4 h-4" />
184+
<Input
185+
id="displayName"
186+
name="displayName"
187+
type="text"
188+
placeholder="How others will see you"
189+
value={formData.displayName}
190+
onChange={handleInputChange}
191+
className="pl-10 bg-gray-800/50 border-gray-600 text-white placeholder-gray-400"
192+
/>
193+
</div>
194+
</div>
195+
)}
196+
197+
<Button
198+
type="submit"
199+
className="w-full bg-rose-500 hover:bg-rose-600 text-white"
200+
disabled={isLoading}
201+
>
202+
{isLoading ? (
203+
<div className="flex items-center space-x-2">
204+
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white"></div>
205+
<span>{mode === 'login' ? 'Signing In...' : 'Creating Account...'}</span>
206+
</div>
207+
) : (
208+
mode === 'login' ? 'Sign In' : 'Create Account'
209+
)}
210+
</Button>
211+
</form>
212+
213+
<div className="text-center">
214+
<p className="text-gray-400">
215+
{mode === 'login' ? "Don't have an account?" : "Already have an account?"}
216+
<button
217+
onClick={switchMode}
218+
className="ml-2 text-rose-400 hover:text-rose-300 font-medium"
219+
>
220+
{mode === 'login' ? 'Sign Up' : 'Sign In'}
221+
</button>
222+
</p>
223+
</div>
224+
</CardContent>
225+
</Card>
226+
</motion.div>
227+
</motion.div>
228+
</AnimatePresence>
229+
);
230+
};
231+
232+
export default AuthForm;
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import * as React from "react"
2+
import { cn } from "@/lib/utils"
3+
4+
const Label = React.forwardRef<
5+
HTMLLabelElement,
6+
React.ComponentProps<"label">
7+
>(({ className, ...props }, ref) => (
8+
<label
9+
ref={ref}
10+
className={cn(
11+
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70",
12+
className
13+
)}
14+
{...props}
15+
/>
16+
))
17+
Label.displayName = "Label"
18+
19+
export { Label }
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
'use client';
2+
3+
import React, { createContext, useContext, useEffect, useState } from 'react';
4+
import BackendAPI, { type User, type AuthResponse } from './backend-api';
5+
6+
interface AuthContextType {
7+
user: User | null;
8+
isLoading: boolean;
9+
isAuthenticated: boolean;
10+
login: (email: string, password: string) => Promise<AuthResponse>;
11+
register: (userData: {
12+
username: string;
13+
email: string;
14+
password: string;
15+
displayName?: string;
16+
}) => Promise<AuthResponse>;
17+
logout: () => void;
18+
updateProfile: (profileData: Partial<User['profile']>) => Promise<void>;
19+
refreshUser: () => Promise<void>;
20+
}
21+
22+
const AuthContext = createContext<AuthContextType | undefined>(undefined);
23+
24+
export const useAuth = () => {
25+
const context = useContext(AuthContext);
26+
if (context === undefined) {
27+
throw new Error('useAuth must be used within an AuthProvider');
28+
}
29+
return context;
30+
};
31+
32+
export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
33+
const [user, setUser] = useState<User | null>(null);
34+
const [isLoading, setIsLoading] = useState(true);
35+
36+
// Check if user is authenticated on mount
37+
useEffect(() => {
38+
const token = localStorage.getItem('auth_token');
39+
if (token) {
40+
refreshUser();
41+
} else {
42+
setIsLoading(false);
43+
}
44+
}, []);
45+
46+
const refreshUser = async () => {
47+
try {
48+
setIsLoading(true);
49+
const response = await BackendAPI.getCurrentUser();
50+
if (response.success && response.data) {
51+
setUser(response.data.user);
52+
} else {
53+
// Token might be invalid, clear it
54+
localStorage.removeItem('auth_token');
55+
setUser(null);
56+
}
57+
} catch (error) {
58+
console.error('Failed to refresh user:', error);
59+
localStorage.removeItem('auth_token');
60+
setUser(null);
61+
} finally {
62+
setIsLoading(false);
63+
}
64+
};
65+
66+
const login = async (email: string, password: string): Promise<AuthResponse> => {
67+
try {
68+
const response = await BackendAPI.login({ email, password });
69+
70+
if (response.success && response.data) {
71+
localStorage.setItem('auth_token', response.data.token);
72+
setUser(response.data.user);
73+
}
74+
75+
return response;
76+
} catch (error) {
77+
console.error('Login error:', error);
78+
throw error;
79+
}
80+
};
81+
82+
const register = async (userData: {
83+
username: string;
84+
email: string;
85+
password: string;
86+
displayName?: string;
87+
}): Promise<AuthResponse> => {
88+
try {
89+
const response = await BackendAPI.register(userData);
90+
91+
if (response.success && response.data) {
92+
localStorage.setItem('auth_token', response.data.token);
93+
setUser(response.data.user);
94+
}
95+
96+
return response;
97+
} catch (error) {
98+
console.error('Registration error:', error);
99+
throw error;
100+
}
101+
};
102+
103+
const logout = () => {
104+
localStorage.removeItem('auth_token');
105+
setUser(null);
106+
};
107+
108+
const updateProfile = async (profileData: Partial<User['profile']>) => {
109+
try {
110+
const response = await BackendAPI.updateProfile(profileData);
111+
if (response.success && response.data) {
112+
setUser(response.data.user);
113+
}
114+
} catch (error) {
115+
console.error('Update profile error:', error);
116+
throw error;
117+
}
118+
};
119+
120+
const value = {
121+
user,
122+
isLoading,
123+
isAuthenticated: !!user,
124+
login,
125+
register,
126+
logout,
127+
updateProfile,
128+
refreshUser,
129+
};
130+
131+
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
132+
};

0 commit comments

Comments
 (0)