diff --git a/src/app/admin/layout.tsx b/src/app/admin/layout.tsx index 26ae2d4..69e2eaf 100644 --- a/src/app/admin/layout.tsx +++ b/src/app/admin/layout.tsx @@ -1,5 +1,6 @@ import * as React from 'react'; +import Link from 'next/link'; import { Sidebar, SidebarContent, @@ -15,15 +16,34 @@ import { SidebarTrigger, SidebarFooter, } from '@/components/ui/sidebar'; -import { LayoutDashboard, LogOut, Settings, Mail } from 'lucide-react'; -import { signOut } from '@/lib/auth'; +import { LayoutDashboard, LogOut, Settings, Mail, User as UserIcon } from 'lucide-react'; +import { signOut, getSession } from '@/lib/auth'; import { Button } from '@/components/ui/button'; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from '@/components/ui/dropdown-menu'; +import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'; +import type { User } from '@/lib/types'; -export default function AdminLayout({ + +export default async function AdminLayout({ children, }: { children: React.ReactNode; }) { + const session = (await getSession()) as User | null; + const userInitials = session?.name + ? session.name + .split(' ') + .map((n) => n[0]) + .join('') + : ''; + return ( @@ -47,6 +67,12 @@ export default function AdminLayout({ Settings + + + + User + + @@ -73,11 +99,39 @@ export default function AdminLayout({ - - - Dashboard + + + + Dashboard + + + + + + + {userInitials} + + Toggle user menu + + + + My Account + + + Settings + + + + + + Sign Out + + + + + - {children} + {children} ); diff --git a/src/app/admin/settings/user/page.tsx b/src/app/admin/settings/user/page.tsx new file mode 100644 index 0000000..0ac37c5 --- /dev/null +++ b/src/app/admin/settings/user/page.tsx @@ -0,0 +1,122 @@ + +"use client"; + +import { useForm, type SubmitHandler } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { z } from "zod"; +import { Button } from "@/components/ui/button"; +import { + Card, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { useToast } from "@/hooks/use-toast"; +import { useEffect, useTransition } from "react"; +import { getUser, updateUser } from "@/lib/actions/user"; +import { User } from "@/lib/types"; + +const userProfileSchema = z.object({ + name: z.string().min(1, "Name is required"), + email: z.string().email("Invalid email address"), + password: z.string().min(6, "Password must be at least 6 characters").optional().or(z.literal('')), + confirmPassword: z.string().optional(), +}).refine((data) => data.password === data.confirmPassword, { + message: "Passwords don't match", + path: ["confirmPassword"], +}); + +type UserProfileFormValues = z.infer; + +export default function UserProfilePage() { + const { toast } = useToast(); + const [isSaving, startSavingTransition] = useTransition(); + + const { + register, + handleSubmit, + reset, + formState: { errors }, + } = useForm({ + resolver: zodResolver(userProfileSchema), + }); + + useEffect(() => { + async function fetchUser() { + const user = await getUser(); + if (user) { + reset({ name: user.name, email: user.email }); + } + } + fetchUser(); + }, [reset]); + + const onSubmit: SubmitHandler = async (data) => { + startSavingTransition(async () => { + const result = await updateUser({ + name: data.name, + email: data.email, + password: data.password || undefined, + }); + + if (result.success) { + toast({ + title: "Profile Updated", + description: "Your profile has been updated successfully.", + }); + // Clear password fields after successful submission + reset({ ...data, password: '', confirmPassword: '' }); + } else { + toast({ + title: "Update Failed", + description: result.error || "An unknown error occurred.", + variant: "destructive", + }); + } + }); + }; + + return ( + + + + My Account + + Update your account details. Leave password fields blank to keep the current password. + + + + + Name + + {errors.name && {errors.name.message}} + + + Email + + {errors.email && {errors.email.message}} + + + New Password + + {errors.password && {errors.password.message}} + + + Confirm New Password + + {errors.confirmPassword && {errors.confirmPassword.message}} + + + + + {isSaving ? 'Saving...' : 'Save Changes'} + + + + + ); +} diff --git a/src/lib/actions/user.ts b/src/lib/actions/user.ts new file mode 100644 index 0000000..71d11e0 --- /dev/null +++ b/src/lib/actions/user.ts @@ -0,0 +1,64 @@ + +'use server'; + +import db from '@/lib/db'; +import { User } from '@/lib/types'; +import { z } from 'zod'; +import { getSession } from '../auth'; + +const UserUpdateSchema = z.object({ + name: z.string().min(1, 'Name is required'), + email: z.string().email('Invalid email address'), + password: z.string().optional(), +}); + +export async function getUser(): Promise { + const session = await getSession(); + if (!session?.userId) { + return null; + } + try { + const stmt = db.prepare('SELECT id, name, email FROM users WHERE id = ?'); + const user = stmt.get(session.userId) as User | undefined; + return user ?? null; + } catch (error) { + console.error('Failed to get user:', error); + return null; + } +} + +export async function updateUser( + data: z.infer +): Promise<{ success: boolean; error?: string }> { + const session = await getSession(); + if (!session?.userId) { + return { success: false, error: 'Not authenticated' }; + } + + const validated = UserUpdateSchema.safeParse(data); + if (!validated.success) { + return { success: false, error: 'Invalid data' }; + } + + const { name, email, password } = validated.data; + + try { + if (password) { + // In a real application, hash the password + const stmt = db.prepare( + 'UPDATE users SET name = ?, email = ?, password = ? WHERE id = ?' + ); + stmt.run(name, email, password, session.userId); + } else { + const stmt = db.prepare('UPDATE users SET name = ?, email = ? WHERE id = ?'); + stmt.run(name, email, session.userId); + } + return { success: true }; + } catch (error: any) { + console.error('Failed to update user:', error); + if (error.code === 'SQLITE_CONSTRAINT_UNIQUE') { + return { success: false, error: 'Email already in use.' }; + } + return { success: false, error: 'Failed to update user profile.' }; + } +}
{errors.name.message}
{errors.email.message}
{errors.password.message}
{errors.confirmPassword.message}