feat: web sprint 250-262 — plans/clients/tickets/audit/profile/settings/leads/reports #26
147
app/(app)/profile/page.tsx
Normal file
147
app/(app)/profile/page.tsx
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState } from "react";
|
||||||
|
import { useMutation } from "@tanstack/react-query";
|
||||||
|
import { useAuth } from "@/contexts/AuthContext";
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/Card";
|
||||||
|
import { Button } from "@/components/ui/Button";
|
||||||
|
import { Input } from "@/components/ui/Input";
|
||||||
|
import { User, Lock } from "lucide-react";
|
||||||
|
import api from "@/lib/api";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
|
||||||
|
export default function ProfilePage() {
|
||||||
|
const { user } = useAuth();
|
||||||
|
|
||||||
|
const [infoForm, setInfoForm] = useState({
|
||||||
|
firstName: (user as any)?.firstName ?? user?.name?.split(" ")[0] ?? "",
|
||||||
|
lastName: (user as any)?.lastName ?? user?.name?.split(" ").slice(1).join(" ") ?? "",
|
||||||
|
email: user?.email ?? "",
|
||||||
|
});
|
||||||
|
|
||||||
|
const [pwForm, setPwForm] = useState({
|
||||||
|
currentPassword: "",
|
||||||
|
newPassword: "",
|
||||||
|
confirmPassword: "",
|
||||||
|
});
|
||||||
|
|
||||||
|
const updateInfo = useMutation({
|
||||||
|
mutationFn: async () => {
|
||||||
|
await api.patch("/api/v1/auth/me", {
|
||||||
|
firstName: infoForm.firstName,
|
||||||
|
lastName: infoForm.lastName,
|
||||||
|
email: infoForm.email,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onSuccess: () => toast.success("Profile updated!"),
|
||||||
|
onError: (e: any) => toast.error(e.response?.data?.message ?? "Failed to update profile"),
|
||||||
|
});
|
||||||
|
|
||||||
|
const resetPassword = useMutation({
|
||||||
|
mutationFn: async () => {
|
||||||
|
if (pwForm.newPassword !== pwForm.confirmPassword) {
|
||||||
|
throw new Error("Passwords do not match");
|
||||||
|
}
|
||||||
|
await api.post("/api/v1/auth/change-password", {
|
||||||
|
currentPassword: pwForm.currentPassword,
|
||||||
|
newPassword: pwForm.newPassword,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
toast.success("Password changed successfully!");
|
||||||
|
setPwForm({ currentPassword: "", newPassword: "", confirmPassword: "" });
|
||||||
|
},
|
||||||
|
onError: (e: any) => toast.error(e.response?.data?.message ?? e.message ?? "Failed to change password"),
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6 max-w-xl">
|
||||||
|
<div>
|
||||||
|
<h1 className="text-2xl font-bold text-gray-900">My Profile</h1>
|
||||||
|
<p className="text-sm text-gray-500 mt-1">Manage your account settings</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Profile Info */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center gap-2">
|
||||||
|
<User className="h-4 w-4 text-blue-600" />
|
||||||
|
Personal Information
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
<div className="grid grid-cols-2 gap-3">
|
||||||
|
<Input
|
||||||
|
label="First Name"
|
||||||
|
value={infoForm.firstName}
|
||||||
|
onChange={e => setInfoForm(f => ({ ...f, firstName: e.target.value }))}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
label="Last Name"
|
||||||
|
value={infoForm.lastName}
|
||||||
|
onChange={e => setInfoForm(f => ({ ...f, lastName: e.target.value }))}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Input
|
||||||
|
label="Email"
|
||||||
|
type="email"
|
||||||
|
value={infoForm.email}
|
||||||
|
onChange={e => setInfoForm(f => ({ ...f, email: e.target.value }))}
|
||||||
|
/>
|
||||||
|
<div className="flex justify-end">
|
||||||
|
<Button
|
||||||
|
onClick={() => updateInfo.mutate()}
|
||||||
|
isLoading={updateInfo.isPending}
|
||||||
|
disabled={!infoForm.firstName || !infoForm.email}
|
||||||
|
>
|
||||||
|
Save Changes
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Change Password */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center gap-2">
|
||||||
|
<Lock className="h-4 w-4 text-blue-600" />
|
||||||
|
Change Password
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
<Input
|
||||||
|
label="Current Password"
|
||||||
|
type="password"
|
||||||
|
value={pwForm.currentPassword}
|
||||||
|
onChange={e => setPwForm(f => ({ ...f, currentPassword: e.target.value }))}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
label="New Password"
|
||||||
|
type="password"
|
||||||
|
value={pwForm.newPassword}
|
||||||
|
onChange={e => setPwForm(f => ({ ...f, newPassword: e.target.value }))}
|
||||||
|
hint="Minimum 8 characters"
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
label="Confirm New Password"
|
||||||
|
type="password"
|
||||||
|
value={pwForm.confirmPassword}
|
||||||
|
onChange={e => setPwForm(f => ({ ...f, confirmPassword: e.target.value }))}
|
||||||
|
/>
|
||||||
|
{pwForm.newPassword && pwForm.confirmPassword && pwForm.newPassword !== pwForm.confirmPassword && (
|
||||||
|
<p className="text-xs text-red-500">Passwords do not match</p>
|
||||||
|
)}
|
||||||
|
<div className="flex justify-end">
|
||||||
|
<Button
|
||||||
|
onClick={() => resetPassword.mutate()}
|
||||||
|
isLoading={resetPassword.isPending}
|
||||||
|
disabled={!pwForm.currentPassword || !pwForm.newPassword || !pwForm.confirmPassword || pwForm.newPassword !== pwForm.confirmPassword}
|
||||||
|
>
|
||||||
|
Change Password
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user