Skip to content

Commit efc8999

Browse files
committed
feat: Enhance navbar with login/logout functionality and account menu; add token management and logout confirmation modal
1 parent a0312a2 commit efc8999

8 files changed

Lines changed: 1398 additions & 23 deletions

File tree

app/(auth)/login/page.tsx

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import React from 'react';
2+
import Login from '@/components/auth/login';
3+
4+
export default function LoginPage() {
5+
return (
6+
<div className='min-h-screen flex flex-col gap-4'>
7+
<Login />
8+
</div>
9+
)
10+
}

app/dashboard/page.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import Dashboard from '@/components/dashboard/dashboard';
2+
3+
export default function DashboardPage() {
4+
return <Dashboard />;
5+
}

components/auth/login.tsx

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
"use client";
2+
import React, { useState } from 'react';
3+
import { Input } from '@/components/ui/input';
4+
import { Label } from '@/components/ui/label';
5+
import { Button } from '@/components/ui/button';
6+
import { useRouter } from 'next/navigation';
7+
8+
const Login = () => {
9+
const [teamName, setTeamName] = useState('');
10+
const [password, setPassword] = useState('');
11+
const [error, setError] = useState('');
12+
const [isLoading, setIsLoading] = useState(false);
13+
const [showPassword, setShowPassword] = useState(false);
14+
const router = useRouter();
15+
16+
17+
const handleLogin = async () => {
18+
if (!teamName || !password) {
19+
setError('Please enter your team name and password.');
20+
setTimeout(() => setError(''), 4000);
21+
return;
22+
}
23+
24+
try {
25+
setIsLoading(true);
26+
const response = await fetch('/api/auth/login', {
27+
method: 'POST',
28+
body: JSON.stringify({ username: teamName, password: password }),
29+
headers: {
30+
'Content-Type': 'application/json',
31+
},
32+
});
33+
34+
const data = await response.json();
35+
if (response.ok && data.status) {
36+
localStorage.setItem('token', data.token);
37+
// Persist team name for navbar account menu
38+
localStorage.setItem('team_name', teamName);
39+
router.push('/dashboard');
40+
} else {
41+
setError(data.message || 'Login failed. Please try again.');
42+
setTimeout(() => setError(''), 5000);
43+
}
44+
} catch (e) {
45+
setError('Something went wrong. Please try again.');
46+
setTimeout(() => setError(''), 5000);
47+
} finally {
48+
setIsLoading(false);
49+
}
50+
}
51+
52+
53+
54+
return (
55+
<div className='min-h-screen w-full bg-gradient-to-b from-[#0B0B0F] to-[#11111A] flex items-center justify-center px-4'>
56+
<div className='w-full max-w-md bg-black/40 backdrop-blur-xl border border-white/10 rounded-2xl shadow-2xl p-6 md:p-8'>
57+
<div className='mb-6 text-center'>
58+
<h1 className='text-2xl font-semibold tracking-tight text-white'>Welcome</h1>
59+
<p className='text-sm text-gray-300 mt-1'>Sign in to continue to your dashboard</p>
60+
</div>
61+
62+
{error && (
63+
<div className='mb-4 rounded-md border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-700 dark:border-red-900/50 dark:bg-red-950/60 dark:text-red-300'>
64+
{error}
65+
</div>
66+
)}
67+
68+
<form
69+
className='grid gap-4'
70+
onSubmit={(e) => {
71+
e.preventDefault();
72+
if (!isLoading) handleLogin();
73+
}}
74+
>
75+
<div className='grid gap-2'>
76+
<Label htmlFor='teamName' className='text-slate-200'>Team name</Label>
77+
<Input
78+
id='teamName'
79+
type='text'
80+
placeholder='Enter your team name'
81+
value={teamName}
82+
onChange={(e) => setTeamName(e.target.value)}
83+
autoComplete='username'
84+
/>
85+
</div>
86+
87+
<div className='grid gap-2'>
88+
<Label htmlFor='password' className='text-slate-200'>Password</Label>
89+
<div className='relative'>
90+
<Input
91+
id='password'
92+
type={showPassword ? 'text' : 'password'}
93+
placeholder='Enter your password'
94+
value={password}
95+
onChange={(e) => setPassword(e.target.value)}
96+
autoComplete='current-password'
97+
className='pr-12'
98+
/>
99+
<button
100+
type='button'
101+
aria-label={showPassword ? 'Hide password' : 'Show password'}
102+
className='absolute inset-y-0 right-2 my-auto h-8 rounded px-2 text-xs text-slate-200 hover:bg-white/10 active:bg-white/15'
103+
onClick={() => setShowPassword((s) => !s)}
104+
>
105+
{showPassword ? 'Hide' : 'Show'}
106+
</button>
107+
</div>
108+
</div>
109+
110+
<Button type='submit' className='w-full mt-2' disabled={isLoading}>
111+
{isLoading ? 'Signing in...' : 'Sign in'}
112+
</Button>
113+
</form>
114+
115+
<p className='mt-6 text-center text-xs text-gray-400'>
116+
Please check your email inbox for the password.
117+
</p>
118+
</div>
119+
</div>
120+
)
121+
}
122+
123+
export default Login

0 commit comments

Comments
 (0)