mirror of
https://github.com/rgrgogu/new_starr.git
synced 2026-09-27 00:12:54 +08:00
421 lines
22 KiB
React
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; |