Skip to content

Commit ba87c6d

Browse files
authored
Merge pull request #1 from SX110903/secure-app-frontend
feat: build Next.js 16 frontend for Secure App
2 parents d69c2b9 + e727531 commit ba87c6d

34 files changed

Lines changed: 6849 additions & 0 deletions
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
"use client"
2+
3+
import { useState } from "react"
4+
import Link from "next/link"
5+
import { useForm } from "react-hook-form"
6+
import { zodResolver } from "@hookform/resolvers/zod"
7+
import { z } from "zod"
8+
import { Button } from "@/components/ui/button"
9+
import { Input } from "@/components/ui/input"
10+
import { Label } from "@/components/ui/label"
11+
import {
12+
Card,
13+
CardContent,
14+
CardDescription,
15+
CardFooter,
16+
CardHeader,
17+
CardTitle,
18+
} from "@/components/ui/card"
19+
import { Shield, Loader2, ArrowLeft, Mail } from "lucide-react"
20+
21+
const forgotSchema = z.object({
22+
email: z.string().email("Enter a valid email address"),
23+
})
24+
25+
type ForgotForm = z.infer<typeof forgotSchema>
26+
27+
export default function ForgotPasswordPage() {
28+
const [submitted, setSubmitted] = useState(false)
29+
30+
const {
31+
register,
32+
handleSubmit,
33+
formState: { errors, isSubmitting },
34+
} = useForm<ForgotForm>({
35+
resolver: zodResolver(forgotSchema),
36+
})
37+
38+
const onSubmit = async (_data: ForgotForm) => {
39+
// Simulate API call - backend would handle sending email
40+
await new Promise((resolve) => setTimeout(resolve, 1000))
41+
setSubmitted(true)
42+
}
43+
44+
return (
45+
<>
46+
<div className="flex items-center gap-2 mb-8 lg:hidden">
47+
<Shield className="w-6 h-6 text-primary" />
48+
<span className="text-xl font-bold">Secure App</span>
49+
</div>
50+
51+
<Card className="border-border/50 shadow-sm">
52+
{submitted ? (
53+
<>
54+
<CardHeader className="text-center pb-4">
55+
<div className="mx-auto flex h-12 w-12 items-center justify-center rounded-full bg-primary/10 mb-4">
56+
<Mail className="h-6 w-6 text-primary" />
57+
</div>
58+
<CardTitle className="text-2xl font-bold">
59+
Check your email
60+
</CardTitle>
61+
<CardDescription>
62+
If an account exists with that email, we sent password reset
63+
instructions.
64+
</CardDescription>
65+
</CardHeader>
66+
<CardFooter className="justify-center">
67+
<Link
68+
href="/login"
69+
className="text-sm text-primary font-medium hover:underline inline-flex items-center gap-1"
70+
>
71+
<ArrowLeft className="w-4 h-4" />
72+
Back to sign in
73+
</Link>
74+
</CardFooter>
75+
</>
76+
) : (
77+
<>
78+
<CardHeader className="pb-4">
79+
<CardTitle className="text-2xl font-bold">
80+
Forgot password
81+
</CardTitle>
82+
<CardDescription>
83+
{"Enter your email and we'll send you a reset link"}
84+
</CardDescription>
85+
</CardHeader>
86+
<CardContent>
87+
<form
88+
onSubmit={handleSubmit(onSubmit)}
89+
className="flex flex-col gap-4"
90+
>
91+
<div className="flex flex-col gap-2">
92+
<Label htmlFor="email">Email</Label>
93+
<Input
94+
id="email"
95+
type="email"
96+
placeholder="you@example.com"
97+
autoComplete="email"
98+
{...register("email")}
99+
/>
100+
{errors.email && (
101+
<p className="text-sm text-destructive">
102+
{errors.email.message}
103+
</p>
104+
)}
105+
</div>
106+
107+
<Button type="submit" disabled={isSubmitting} className="mt-2">
108+
{isSubmitting ? (
109+
<>
110+
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
111+
Sending...
112+
</>
113+
) : (
114+
"Send reset link"
115+
)}
116+
</Button>
117+
</form>
118+
</CardContent>
119+
<CardFooter className="justify-center">
120+
<Link
121+
href="/login"
122+
className="text-sm text-muted-foreground hover:text-foreground inline-flex items-center gap-1"
123+
>
124+
<ArrowLeft className="w-4 h-4" />
125+
Back to sign in
126+
</Link>
127+
</CardFooter>
128+
</>
129+
)}
130+
</Card>
131+
</>
132+
)
133+
}

app/(auth)/layout.tsx

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
"use client"
2+
3+
import { useEffect } from "react"
4+
import { useRouter } from "next/navigation"
5+
import { useAuth } from "@/lib/auth"
6+
import { Shield } from "lucide-react"
7+
8+
export default function AuthLayout({
9+
children,
10+
}: {
11+
children: React.ReactNode
12+
}) {
13+
const { isAuthenticated, isLoading } = useAuth()
14+
const router = useRouter()
15+
16+
useEffect(() => {
17+
if (!isLoading && isAuthenticated) {
18+
router.replace("/dashboard")
19+
}
20+
}, [isAuthenticated, isLoading, router])
21+
22+
if (isLoading) {
23+
return (
24+
<div className="flex min-h-screen items-center justify-center bg-background">
25+
<div className="animate-pulse text-muted-foreground">Loading...</div>
26+
</div>
27+
)
28+
}
29+
30+
if (isAuthenticated) return null
31+
32+
return (
33+
<div className="flex min-h-screen">
34+
{/* Left panel - branding */}
35+
<div className="hidden lg:flex lg:w-1/2 flex-col items-center justify-center bg-sidebar text-sidebar-foreground p-12">
36+
<div className="flex flex-col items-center gap-6 max-w-md text-center">
37+
<div className="flex items-center justify-center w-16 h-16 rounded-2xl bg-primary/10">
38+
<Shield className="w-8 h-8 text-primary" />
39+
</div>
40+
<h1 className="text-3xl font-bold text-sidebar-accent-foreground tracking-tight text-balance">
41+
Secure App
42+
</h1>
43+
<p className="text-sidebar-foreground/70 leading-relaxed text-balance">
44+
Enterprise-grade security management platform with role-based access
45+
control, comprehensive user administration, and granular permission
46+
management.
47+
</p>
48+
<div className="flex flex-col gap-3 mt-4 text-sm text-sidebar-foreground/50">
49+
<div className="flex items-center gap-2">
50+
<div className="w-1.5 h-1.5 rounded-full bg-primary" />
51+
<span>JWT-based authentication</span>
52+
</div>
53+
<div className="flex items-center gap-2">
54+
<div className="w-1.5 h-1.5 rounded-full bg-primary" />
55+
<span>Role-based access control (RBAC)</span>
56+
</div>
57+
<div className="flex items-center gap-2">
58+
<div className="w-1.5 h-1.5 rounded-full bg-primary" />
59+
<span>Granular permission system</span>
60+
</div>
61+
</div>
62+
</div>
63+
</div>
64+
65+
{/* Right panel - form */}
66+
<div className="flex flex-1 flex-col items-center justify-center p-6 lg:p-12">
67+
<div className="w-full max-w-md">{children}</div>
68+
</div>
69+
</div>
70+
)
71+
}

app/(auth)/login/page.tsx

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
"use client"
2+
3+
import { useState } from "react"
4+
import { useRouter } from "next/navigation"
5+
import Link from "next/link"
6+
import { useForm } from "react-hook-form"
7+
import { zodResolver } from "@hookform/resolvers/zod"
8+
import { z } from "zod"
9+
import { toast } from "sonner"
10+
import { useAuth } from "@/lib/auth"
11+
import { ApiRequestError } from "@/lib/api"
12+
import { Button } from "@/components/ui/button"
13+
import { Input } from "@/components/ui/input"
14+
import { Label } from "@/components/ui/label"
15+
import {
16+
Card,
17+
CardContent,
18+
CardDescription,
19+
CardFooter,
20+
CardHeader,
21+
CardTitle,
22+
} from "@/components/ui/card"
23+
import { Shield, Loader2, Eye, EyeOff } from "lucide-react"
24+
25+
const loginSchema = z.object({
26+
email: z.string().email("Enter a valid email address"),
27+
password: z.string().min(1, "Password is required"),
28+
})
29+
30+
type LoginForm = z.infer<typeof loginSchema>
31+
32+
export default function LoginPage() {
33+
const { login } = useAuth()
34+
const router = useRouter()
35+
const [showPassword, setShowPassword] = useState(false)
36+
37+
const {
38+
register,
39+
handleSubmit,
40+
formState: { errors, isSubmitting },
41+
} = useForm<LoginForm>({
42+
resolver: zodResolver(loginSchema),
43+
})
44+
45+
const onSubmit = async (data: LoginForm) => {
46+
try {
47+
await login(data.email, data.password)
48+
toast.success("Login successful")
49+
router.push("/dashboard")
50+
} catch (error) {
51+
if (error instanceof ApiRequestError) {
52+
toast.error(error.message)
53+
} else {
54+
toast.error("An unexpected error occurred")
55+
}
56+
}
57+
}
58+
59+
return (
60+
<>
61+
<div className="flex items-center gap-2 mb-8 lg:hidden">
62+
<Shield className="w-6 h-6 text-primary" />
63+
<span className="text-xl font-bold">Secure App</span>
64+
</div>
65+
66+
<Card className="border-border/50 shadow-sm">
67+
<CardHeader className="pb-4">
68+
<CardTitle className="text-2xl font-bold">Welcome back</CardTitle>
69+
<CardDescription>
70+
Sign in to your account to continue
71+
</CardDescription>
72+
</CardHeader>
73+
<CardContent>
74+
<form onSubmit={handleSubmit(onSubmit)} className="flex flex-col gap-4">
75+
<div className="flex flex-col gap-2">
76+
<Label htmlFor="email">Email</Label>
77+
<Input
78+
id="email"
79+
type="email"
80+
placeholder="admin@example.com"
81+
autoComplete="email"
82+
{...register("email")}
83+
/>
84+
{errors.email && (
85+
<p className="text-sm text-destructive">{errors.email.message}</p>
86+
)}
87+
</div>
88+
89+
<div className="flex flex-col gap-2">
90+
<div className="flex items-center justify-between">
91+
<Label htmlFor="password">Password</Label>
92+
<Link
93+
href="/forgot-password"
94+
className="text-xs text-primary hover:underline"
95+
>
96+
Forgot password?
97+
</Link>
98+
</div>
99+
<div className="relative">
100+
<Input
101+
id="password"
102+
type={showPassword ? "text" : "password"}
103+
placeholder="Enter your password"
104+
autoComplete="current-password"
105+
className="pr-10"
106+
{...register("password")}
107+
/>
108+
<button
109+
type="button"
110+
onClick={() => setShowPassword(!showPassword)}
111+
className="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
112+
aria-label={showPassword ? "Hide password" : "Show password"}
113+
>
114+
{showPassword ? (
115+
<EyeOff className="w-4 h-4" />
116+
) : (
117+
<Eye className="w-4 h-4" />
118+
)}
119+
</button>
120+
</div>
121+
{errors.password && (
122+
<p className="text-sm text-destructive">
123+
{errors.password.message}
124+
</p>
125+
)}
126+
</div>
127+
128+
<Button type="submit" disabled={isSubmitting} className="mt-2">
129+
{isSubmitting ? (
130+
<>
131+
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
132+
Signing in...
133+
</>
134+
) : (
135+
"Sign in"
136+
)}
137+
</Button>
138+
</form>
139+
</CardContent>
140+
<CardFooter className="justify-center">
141+
<p className="text-sm text-muted-foreground">
142+
{"Don't have an account? "}
143+
<Link
144+
href="/register"
145+
className="text-primary font-medium hover:underline"
146+
>
147+
Create account
148+
</Link>
149+
</p>
150+
</CardFooter>
151+
</Card>
152+
</>
153+
)
154+
}

0 commit comments

Comments
 (0)