Files
starr-philproperties/src/modules/client/pages/EditProfile.jsx
T
kennethobsequio fbef7cb6e6 ready to test
Testing

Signed-off-by: Kenneth Obsequio <k80308392@gmail.com>
2026-06-22 10:07:20 +08:00

421 lines
22 KiB
React

import { useState, useEffect } from "react";
import { useNavigate } from "react-router-dom";
import { useProfile } from "@/contexts/ProfileProvider";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Separator } from "@/components/ui/separator";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { Camera, Loader2, Plus, Trash2, ArrowLeft } from "lucide-react";
import AvatarUploadDialog from "@/components/generic/AvatarUploadDialog";
// ─── Helpers ──────────────────────────────────────────────────────────────────
function getInitials(name = "") {
return name
.trim()
.split(/\s+/)
.map((n) => n[0]?.toUpperCase() ?? "")
.slice(0, 2)
.join("");
}
function emptyPhone() {
return { number: "", country_code: "+63", phone_type: "mobile", full_number: "" };
}
function emptyAddress() {
return {
address_type: "home",
street: "",
city: "",
state: "",
zip: "",
country: "Philippines",
full_address: "",
};
}
const PHONE_TYPES = ["mobile", "home", "work", "other"];
const ADDRESS_TYPES = ["home", "work", "billing", "other"];
// ─── Component ────────────────────────────────────────────────────────────────
const EditProfile = () => {
const navigate = useNavigate();
const { profile, profileLoading, updateProfile, getProfile, uploadAvatar, deleteAvatar, avatarLoading } = useProfile();
const [avatarDialogOpen, setAvatarDialogOpen] = useState(false);
// ── Form state ─────────────────────────────────────────────────────────────
const [givenName, setGivenName] = useState("");
const [middleName, setMiddleName] = useState("");
const [lastName, setLastName] = useState("");
const [extensionName, setExtensionName] = useState("");
const [occupation, setOccupation] = useState("");
const [phones, setPhones] = useState([emptyPhone()]);
const [addresses, setAddresses] = useState([emptyAddress()]);
const [avatarPreview, setAvatarPreview] = useState("");
useEffect(() => { getProfile(); }, []); // fetch on mount
useEffect(() => { // seed fields when profile loads
if (!profile) return;
const pi = profile.personal_info ?? {};
setGivenName(pi.name?.given_name ?? "");
setMiddleName(pi.name?.middle_name ?? "");
setLastName(pi.name?.last_name ?? "");
setExtensionName(pi.name?.extension_name ?? "");
setOccupation(pi.occupation ?? "");
setPhones(pi.phone_number?.length ? pi.phone_number : [emptyPhone()]);
setAddresses(pi.addresses?.length ? pi.addresses : [emptyAddress()]);
setAvatarPreview(pi.avatar?.url ?? "");
}, [profile]);
// ── Phone helpers ──────────────────────────────────────────────────────────
const updatePhone = (i, field, value) => {
setPhones((prev) => {
const next = [...prev];
next[i] = { ...next[i], [field]: value };
next[i].full_number = `${next[i].country_code}${next[i].number}`;
return next;
});
};
const addPhone = () => setPhones((p) => [...p, emptyPhone()]);
const removePhone = (i) => setPhones((p) => p.filter((_, idx) => idx !== i));
// ── Address helpers ────────────────────────────────────────────────────────
const updateAddress = (i, field, value) => {
setAddresses((prev) => {
const next = [...prev];
next[i] = { ...next[i], [field]: value };
const a = next[i];
next[i].full_address = [a.street, a.city, a.state, a.zip, a.country]
.filter(Boolean)
.join(", ");
return next;
});
};
const addAddress = () => setAddresses((a) => [...a, emptyAddress()]);
const removeAddress = (i) => setAddresses((a) => a.filter((_, idx) => idx !== i));
// ── Submit ─────────────────────────────────────────────────────────────────
const handleSubmit = async () => {
const full_name = [givenName, middleName, lastName, extensionName]
.filter(Boolean)
.join(" ");
const personal_info = {
name: {
given_name: givenName,
middle_name: middleName,
last_name: lastName,
extension_name: extensionName,
full_name,
},
occupation,
phone_number: phones.filter((p) => p.number.trim() !== ""),
addresses: addresses.filter((a) => a.street.trim() !== "" || a.city.trim() !== ""),
};
const result = await updateProfile(personal_info);
if (result?.success) navigate("/profile");
};
const fullName = [givenName, lastName].filter(Boolean).join(" ");
const initials = getInitials(fullName);
return (
<div className="mt-17 bg-muted min-h-screen">
<div className="p-6 lg:container lg:max-w-3xl lg:mx-auto space-y-4">
{/* Header */}
<div className="flex items-center gap-3">
<Button variant="ghost" size="icon" onClick={() => navigate("/profile")}>
<ArrowLeft className="size-4" />
</Button>
<div>
<h1 className="text-base font-semibold">Edit Profile</h1>
<p className="text-xs text-muted-foreground">Update your personal information</p>
</div>
</div>
{/* Avatar */}
<Card>
<CardContent className="py-4 px-6">
<div className="flex items-center gap-4">
<div className="relative">
<Avatar className="h-16 w-16">
<AvatarImage src={avatarPreview} />
<AvatarFallback className="text-base">{initials || "PH"}</AvatarFallback>
</Avatar>
<button
type="button"
className="absolute bottom-0 right-0 h-5 w-5 rounded-full bg-foreground flex items-center justify-center hover:opacity-80 transition-opacity"
onClick={() => setAvatarDialogOpen(true)}
>
<Camera className="h-3 w-3 text-background" />
</button>
</div>
<div>
<p className="text-sm font-medium">{fullName || "—"}</p>
<p className="text-xs text-muted-foreground">{profile?.email ?? ""}</p>
</div>
</div>
</CardContent>
</Card>
{/* Name */}
<Card>
<CardHeader className="pb-2">
<CardTitle className="text-sm font-medium">Name</CardTitle>
</CardHeader>
<CardContent className="space-y-3">
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
<div className="space-y-1.5">
<Label className="text-xs">Given Name</Label>
<Input
value={givenName}
onChange={(e) => setGivenName(e.target.value)}
placeholder="Juan"
/>
</div>
<div className="space-y-1.5">
<Label className="text-xs">Middle Name</Label>
<Input
value={middleName}
onChange={(e) => setMiddleName(e.target.value)}
placeholder="Santos"
/>
</div>
<div className="space-y-1.5">
<Label className="text-xs">Last Name</Label>
<Input
value={lastName}
onChange={(e) => setLastName(e.target.value)}
placeholder="Dela Cruz"
/>
</div>
<div className="space-y-1.5">
<Label className="text-xs">Extension Name</Label>
<Input
value={extensionName}
onChange={(e) => setExtensionName(e.target.value)}
placeholder="Jr., Sr., III"
/>
</div>
</div>
<div className="space-y-1.5">
<Label className="text-xs">Occupation</Label>
<Input
value={occupation}
onChange={(e) => setOccupation(e.target.value)}
placeholder="e.g. Real Estate Agent"
/>
</div>
</CardContent>
</Card>
{/* Phone numbers */}
<Card>
<CardHeader className="pb-2">
<div className="flex items-center justify-between">
<CardTitle className="text-sm font-medium">Phone Numbers</CardTitle>
<Button variant="outline" size="sm" onClick={addPhone}>
<Plus className="size-3.5" />
Add
</Button>
</div>
</CardHeader>
<CardContent className="space-y-4">
{phones.map((phone, i) => (
<div key={i}>
{i > 0 && <Separator className="mb-4" />}
<div className="space-y-3">
<div className="flex items-center justify-between">
<p className="text-xs text-muted-foreground">Phone {i + 1}</p>
{phones.length > 1 && (
<Button
variant="ghost"
size="icon"
className="h-6 w-6 text-destructive hover:text-destructive"
onClick={() => removePhone(i)}
>
<Trash2 className="size-3.5" />
</Button>
)}
</div>
<div className="grid grid-cols-1 sm:grid-cols-3 gap-3">
<div className="space-y-1.5">
<Label className="text-xs">Country Code</Label>
<Input
value={phone.country_code}
onChange={(e) => updatePhone(i, "country_code", e.target.value)}
placeholder="+63"
/>
</div>
<div className="space-y-1.5">
<Label className="text-xs">Number</Label>
<Input
value={phone.number}
onChange={(e) => updatePhone(i, "number", e.target.value)}
placeholder="9XX XXX XXXX"
/>
</div>
<div className="space-y-1.5">
<Label className="text-xs">Type</Label>
<Select
value={phone.phone_type}
onValueChange={(v) => updatePhone(i, "phone_type", v)}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
{PHONE_TYPES.map((t) => (
<SelectItem key={t} value={t}>
{t.charAt(0).toUpperCase() + t.slice(1)}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
</div>
</div>
))}
</CardContent>
</Card>
{/* Addresses */}
<Card>
<CardHeader className="pb-2">
<div className="flex items-center justify-between">
<CardTitle className="text-sm font-medium">Addresses</CardTitle>
<Button variant="outline" size="sm" onClick={addAddress}>
<Plus className="size-3.5" />
Add
</Button>
</div>
</CardHeader>
<CardContent className="space-y-4">
{addresses.map((addr, i) => (
<div key={i}>
{i > 0 && <Separator className="mb-4" />}
<div className="space-y-3">
<div className="flex items-center justify-between">
<p className="text-xs text-muted-foreground">Address {i + 1}</p>
{addresses.length > 1 && (
<Button
variant="ghost"
size="icon"
className="h-6 w-6 text-destructive hover:text-destructive"
onClick={() => removeAddress(i)}
>
<Trash2 className="size-3.5" />
</Button>
)}
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
<div className="space-y-1.5 sm:col-span-2">
<Label className="text-xs">Address Type</Label>
<Select
value={addr.address_type}
onValueChange={(v) => updateAddress(i, "address_type", v)}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
{ADDRESS_TYPES.map((t) => (
<SelectItem key={t} value={t}>
{t.charAt(0).toUpperCase() + t.slice(1)}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-1.5 sm:col-span-2">
<Label className="text-xs">Street</Label>
<Input
value={addr.street}
onChange={(e) => updateAddress(i, "street", e.target.value)}
placeholder="123 Rizal Street, Barangay Poblacion"
/>
</div>
<div className="space-y-1.5">
<Label className="text-xs">City / Municipality</Label>
<Input
value={addr.city}
onChange={(e) => updateAddress(i, "city", e.target.value)}
placeholder="Makati"
/>
</div>
<div className="space-y-1.5">
<Label className="text-xs">State / Region</Label>
<Input
value={addr.state}
onChange={(e) => updateAddress(i, "state", e.target.value)}
placeholder="Metro Manila"
/>
</div>
<div className="space-y-1.5">
<Label className="text-xs">ZIP Code</Label>
<Input
value={addr.zip}
onChange={(e) => updateAddress(i, "zip", e.target.value)}
placeholder="1200"
/>
</div>
<div className="space-y-1.5">
<Label className="text-xs">Country</Label>
<Input
value={addr.country}
onChange={(e) => updateAddress(i, "country", e.target.value)}
placeholder="Philippines"
/>
</div>
</div>
</div>
</div>
))}
</CardContent>
</Card>
{/* Actions */}
<div className="flex justify-end gap-2 pb-6">
<Button variant="outline" onClick={() => navigate("/profile")}>
Cancel
</Button>
<Button onClick={handleSubmit} disabled={profileLoading}>
{profileLoading && <Loader2 className="size-4 animate-spin" />}
Save changes
</Button>
</div>
</div>
<AvatarUploadDialog
open={avatarDialogOpen}
onClose={() => setAvatarDialogOpen(false)}
currentAvatarUrl={avatarPreview}
initials={initials || "PH"}
onUpload={uploadAvatar}
onDelete={deleteAvatar}
loading={avatarLoading}
/>
</div>
);
};
export default EditProfile;