| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243 |
- 'use client';
- import { useState, useEffect, Suspense } from 'react';
- import { useRouter, useSearchParams } from 'next/navigation';
- import { useAuth } from '@/lib/auth-context';
- import { invitationsApi } from '@/lib/api';
- import { Button } from '@/components/ui/button';
- function LoginForm() {
- const router = useRouter();
- const searchParams = useSearchParams();
- const inviteToken = searchParams.get('invite_token');
- const { login, acceptedProjects, clearAcceptedProjects } = useAuth();
- const [email, setEmail] = useState('');
- const [password, setPassword] = useState('');
- const [error, setError] = useState('');
- const [loading, setLoading] = useState(false);
- const [justJoined, setJustJoined] = useState(false);
- // Pre-fill email if coming from invite link
- useEffect(() => {
- if (inviteToken) {
- invitationsApi.verify(inviteToken)
- .then(({ invitation }) => {
- if (!email) setEmail(invitation.email);
- })
- .catch(() => {});
- }
- }, [inviteToken]); // eslint-disable-line react-hooks/exhaustive-deps
- useEffect(() => {
- if (acceptedProjects.length > 0 && !justJoined) {
- setJustJoined(true);
- }
- }, [acceptedProjects, justJoined]);
- const handleSubmit = async (e: React.FormEvent) => {
- e.preventDefault();
- setError('');
- setLoading(true);
- try {
- await login(email, password);
- if (inviteToken) {
- router.push(`/invite/${inviteToken}`);
- } else {
- router.push('/projects');
- }
- } catch (err: unknown) {
- setError(err instanceof Error ? err.message : 'Invalid email or password');
- } finally {
- setLoading(false);
- }
- };
- const handleGoToProject = (projectId: string) => {
- clearAcceptedProjects();
- router.push(`/projects/${projectId}`);
- };
- return (
- <div className="min-h-screen flex items-center justify-center relative overflow-hidden"
- style={{ background: 'var(--bg)' }}>
- {/* Ambient background blobs */}
- <div className="absolute inset-0 overflow-hidden pointer-events-none">
- <div className="absolute -top-40 -left-40 w-[600px] h-[600px] rounded-full opacity-[0.07]"
- style={{ background: 'radial-gradient(circle, #6366F1 0%, transparent 70%)' }} />
- <div className="absolute top-1/2 -right-40 w-[500px] h-[500px] rounded-full opacity-[0.05]"
- style={{ background: 'radial-gradient(circle, #818CF8 0%, transparent 70%)' }} />
- <div className="absolute -bottom-40 left-1/3 w-[400px] h-[400px] rounded-full opacity-[0.04]"
- style={{ background: 'radial-gradient(circle, #22C55E 0%, transparent 70%)' }} />
- {/* Grid pattern */}
- <div className="absolute inset-0 opacity-[0.025]"
- style={{
- backgroundImage: 'linear-gradient(rgba(255,255,255,0.5) 1px, transparent 1px), linear-gradient(90deg, rgba(255,255,255,0.5) 1px, transparent 1px)',
- backgroundSize: '48px 48px',
- }} />
- </div>
- {/* Logo */}
- <div className="absolute top-8 left-1/2 -translate-x-1/2 flex items-center gap-2.5 animate-fade-in">
- <div className="w-9 h-9 rounded-xl flex items-center justify-center"
- style={{ background: '#6366F1', boxShadow: '0 0 24px rgba(99,102,241,0.5)' }}>
- <svg className="w-5 h-5 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
- <path strokeLinecap="round" strokeLinejoin="round" d="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z" />
- <path strokeLinecap="round" strokeLinejoin="round" d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
- </svg>
- </div>
- <span className="text-lg font-semibold tracking-tight" style={{ color: 'var(--text)' }}>VidReview</span>
- </div>
- {/* Card */}
- <div className="relative w-full max-w-[400px] mx-6 animate-scale-in">
- <div className="rounded-2xl p-8"
- style={{
- background: 'rgba(22,24,34,0.90)',
- backdropFilter: 'blur(20px)',
- border: '1px solid rgba(255,255,255,0.08)',
- boxShadow: 'var(--shadow-modal)',
- }}>
- {/* Accepted projects notification */}
- {justJoined && acceptedProjects.length > 0 && (
- <div className="mb-5 rounded-lg px-4 py-3 text-sm animate-scale-in"
- style={{ background: 'rgba(34,197,94,0.12)', border: '1px solid rgba(34,197,94,0.25)', color: '#86EFAC' }}>
- <p className="font-medium mb-2">You're now a member of:</p>
- {acceptedProjects.map(p => (
- <button
- key={p.projectId}
- onClick={() => handleGoToProject(p.projectId)}
- className="block text-left hover:underline"
- style={{ color: '#86EFAC' }}
- >
- → {p.projectName}
- </button>
- ))}
- </div>
- )}
- {/* Heading */}
- <div className="mb-7">
- <h1 className="text-xl font-semibold tracking-tight" style={{ color: 'var(--text)' }}>
- Welcome back
- </h1>
- <p className="text-sm mt-1" style={{ color: 'var(--text-muted)' }}>
- Sign in to your workspace
- </p>
- </div>
- {/* Error alert */}
- {error && (
- <div className="mb-5 rounded-lg px-4 py-3 text-sm flex items-start gap-2.5 animate-scale-in"
- style={{ background: 'rgba(239,68,68,0.12)', border: '1px solid rgba(239,68,68,0.25)', color: '#FCA5A5' }}>
- <svg className="w-4 h-4 mt-0.5 shrink-0 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
- <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
- d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
- </svg>
- {error}
- </div>
- )}
- <form onSubmit={handleSubmit} className="space-y-4">
- <div>
- <label htmlFor="email" className="block text-sm font-medium mb-1.5" style={{ color: 'var(--text)' }}>
- Email address
- </label>
- <input
- id="email"
- type="email"
- className="input"
- value={email}
- onChange={e => setEmail(e.target.value)}
- placeholder="you@company.com"
- required
- autoComplete="email"
- autoFocus
- />
- </div>
- <div>
- <div className="flex items-center justify-between mb-1.5">
- <label htmlFor="password" className="text-sm font-medium" style={{ color: 'var(--text)' }}>
- Password
- </label>
- <a href="#"
- className="text-xs transition-colors hover:underline"
- style={{ color: '#818CF8' }}>
- Forgot password?
- </a>
- </div>
- <input
- id="password"
- type="password"
- className="input"
- value={password}
- onChange={e => setPassword(e.target.value)}
- placeholder="••••••••"
- required
- autoComplete="current-password"
- />
- </div>
- <Button
- type="submit"
- loading={loading}
- className="btn btn-primary btn-lg w-full mt-2"
- >
- {loading ? 'Signing in…' : 'Sign in'}
- </Button>
- </form>
- {/* Divider */}
- <div className="flex items-center gap-3 my-6">
- <hr className="flex-1" style={{ borderColor: 'rgba(255,255,255,0.07)' }} />
- <span className="text-xs px-2" style={{ color: 'var(--text-subtle)' }}>or</span>
- <hr className="flex-1" style={{ borderColor: 'rgba(255,255,255,0.07)' }} />
- </div>
- {/* Register link */}
- <p className="text-center text-sm" style={{ color: 'var(--text-muted)' }}>
- No account yet?{' '}
- <a
- href={inviteToken ? `/register?invite_token=${inviteToken}` : '/register'}
- className="font-medium transition-colors hover:underline"
- style={{ color: '#818CF8' }}
- >
- Create workspace
- </a>
- </p>
- </div>
- {/* Demo hint */}
- <p className="text-center text-xs mt-5" style={{ color: 'var(--text-subtle)' }}>
- Demo: <span className="font-mono" style={{ color: 'var(--text-muted)' }}>admin@vidreview.local</span>
- {' / '}
- <span className="font-mono" style={{ color: 'var(--text-muted)' }}>admin123</span>
- </p>
- </div>
- {/* Footer */}
- <p className="absolute bottom-6 text-center text-xs" style={{ color: 'var(--text-subtle)' }}>
- © 2026 VidReview — Video collaboration for creative teams
- </p>
- </div>
- );
- }
- export default function LoginPage() {
- return (
- <Suspense fallback={
- <div className="min-h-screen flex items-center justify-center" style={{ background: 'var(--bg)' }}>
- <div className="flex items-center gap-3" style={{ color: 'var(--text-muted)' }}>
- <div className="w-5 h-5 rounded-full animate-spin"
- style={{ borderColor: '#6366F1', borderTopColor: 'transparent' }} />
- <span className="text-sm">Loading…</span>
- </div>
- </div>
- }>
- <LoginForm />
- </Suspense>
- );
- }
|