383 lines
16 KiB
TypeScript
Raw Normal View History

"use client";
import { useEffect, useState } from "react";
import { LoadingCard, Skeleton } from "@/components/atoms/loading-skeleton";
import { AlertBanner } from "@/components/molecules/AlertBanner";
import { MapPinIcon, PencilIcon, CheckIcon, XMarkIcon, UserIcon } from "@heroicons/react/24/outline";
import { useAuthStore } from "@/features/auth/services/auth.store";
import { accountService } from "@/features/account/services/account.service";
import { useProfileEdit } from "@/features/account/hooks/useProfileEdit";
import { AddressForm } from "@/features/catalog/components/base/AddressForm";
import { Button } from "@/components/atoms/button";
import { useAddressEdit } from "@/features/account/hooks/useAddressEdit";
export default function ProfileContainer() {
const { user } = useAuthStore();
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [editingProfile, setEditingProfile] = useState(false);
const [editingAddress, setEditingAddress] = useState(false);
const profile = useProfileEdit({
firstName: user?.firstName || "",
lastName: user?.lastName || "",
phone: user?.phone || "",
});
const address = useAddressEdit({
street: "",
streetLine2: "",
city: "",
state: "",
postalCode: "",
country: "",
});
useEffect(() => {
void (async () => {
try {
setLoading(true);
const [addr, prof] = await Promise.all([
accountService.getAddress().catch(() => null),
accountService.getProfile().catch(() => null),
]);
if (addr) {
address.setValue("street", addr.street ?? "");
address.setValue("streetLine2", addr.streetLine2 ?? "");
address.setValue("city", addr.city ?? "");
address.setValue("state", addr.state ?? "");
address.setValue("postalCode", addr.postalCode ?? "");
address.setValue("country", addr.country ?? "");
}
if (prof) {
profile.setValue("firstName", prof.firstName || "");
profile.setValue("lastName", prof.lastName || "");
profile.setValue("phone", prof.phone || "");
useAuthStore.setState(state => ({
...state,
user: state.user
? {
...state.user,
firstName: prof.firstName || state.user.firstName,
lastName: prof.lastName || state.user.lastName,
phone: prof.phone || state.user.phone,
}
: (prof as unknown as typeof state.user),
}));
}
} catch (e) {
setError(e instanceof Error ? e.message : "Failed to load profile data");
} finally {
setLoading(false);
}
})();
}, [user?.id]);
if (loading) {
return (
<div className="py-6">
<div className="max-w-4xl mx-auto px-4 sm:px-6 md:px-8 space-y-8">
<div className="bg-white shadow-sm rounded-xl border border-gray-200">
<div className="px-6 py-5 border-b border-gray-200">
<div className="flex items-center justify-between">
<div className="flex items-center space-x-3">
<div className="h-6 w-6 bg-blue-200 rounded" />
<div className="h-6 w-40 bg-gray-200 rounded" />
</div>
<div className="h-8 w-20 bg-gray-200 rounded" />
</div>
</div>
<div className="p-6">
<div className="grid grid-cols-1 gap-8 sm:grid-cols-2">
{Array.from({ length: 4 }).map((_, i) => (
<div key={i} className="space-y-2">
<Skeleton className="h-4 w-24" />
<Skeleton className="h-10 w-full" />
</div>
))}
<div className="sm:col-span-2">
<Skeleton className="h-4 w-28 mb-3" />
<div className="bg-gray-50 rounded-lg p-4 border border-gray-200">
<div className="flex items-center justify-between">
<Skeleton className="h-5 w-48" />
<Skeleton className="h-5 w-24" />
</div>
<Skeleton className="h-3 w-64 mt-2" />
</div>
</div>
</div>
<div className="flex items-center justify-end space-x-3 pt-6 border-t border-gray-200 mt-6">
<Skeleton className="h-9 w-24" />
<Skeleton className="h-9 w-28" />
</div>
</div>
</div>
<div className="bg-white shadow-sm rounded-xl border border-gray-200">
<div className="px-6 py-5 border-b border-gray-200">
<div className="flex items-center justify-between">
<div className="flex items-center space-x-3">
<div className="h-6 w-6 bg-blue-200 rounded" />
<div className="h-6 w-48 bg-gray-200 rounded" />
</div>
<div className="h-8 w-20 bg-gray-200 rounded" />
</div>
</div>
<div className="p-6">
<div className="bg-gray-50 rounded-lg p-4 border border-gray-200">
<div className="space-y-2">
<Skeleton className="h-4 w-60" />
<Skeleton className="h-4 w-48" />
<Skeleton className="h-4 w-52" />
<Skeleton className="h-4 w-32" />
</div>
</div>
<div className="flex items-center justify-end space-x-3 pt-6">
<Skeleton className="h-9 w-24" />
<Skeleton className="h-9 w-28" />
</div>
</div>
</div>
</div>
</div>
);
}
return (
<div className="py-6">
<div className="max-w-4xl mx-auto px-4 sm:px-6 md:px-8">
{error && (
<AlertBanner variant="error" title="Error" className="mb-6">
{error}
</AlertBanner>
)}
<div className="bg-white shadow-sm rounded-xl border border-gray-200">
<div className="px-6 py-5 border-b border-gray-200">
<div className="flex items-center justify-between">
<div className="flex items-center space-x-3">
<UserIcon className="h-6 w-6 text-blue-600" />
<h2 className="text-xl font-semibold text-gray-900">Personal Information</h2>
</div>
{!editingProfile && (
<Button variant="outline" size="sm" onClick={() => setEditingProfile(true)}>
<PencilIcon className="h-4 w-4 mr-2" />
Edit
</Button>
)}
</div>
</div>
<div className="p-6">
<div className="grid grid-cols-1 gap-8 sm:grid-cols-2">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">First Name</label>
{editingProfile ? (
<input
type="text"
value={profile.values.firstName}
onChange={e => profile.setValue("firstName", e.target.value)}
className="block w-full px-4 py-3 border border-gray-300 rounded-lg shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors"
/>
) : (
<p className="text-sm text-gray-900 py-2">
{user?.firstName || <span className="text-gray-500 italic">Not provided</span>}
</p>
)}
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">Last Name</label>
{editingProfile ? (
<input
type="text"
value={profile.values.lastName}
onChange={e => profile.setValue("lastName", e.target.value)}
className="block w-full px-4 py-3 border border-gray-300 rounded-lg shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors"
/>
) : (
<p className="text-sm text-gray-900 py-2">
{user?.lastName || <span className="text-gray-500 italic">Not provided</span>}
</p>
)}
</div>
<div className="sm:col-span-2">
<label className="block text-sm font-medium text-gray-700 mb-3">
Email Address
</label>
<div className="bg-gray-50 rounded-lg p-4 border border-gray-200">
<div className="flex items-center justify-between">
<p className="text-base text-gray-900 font-medium">{user?.email}</p>
</div>
<p className="text-xs text-gray-500 mt-2">
Email cannot be changed from the portal.
</p>
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">Phone Number</label>
{editingProfile ? (
<input
type="tel"
value={profile.values.phone}
onChange={e => profile.setValue("phone", e.target.value)}
placeholder="+81 XX-XXXX-XXXX"
className="block w-full px-4 py-3 border border-gray-300 rounded-lg shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors"
/>
) : (
<p className="text-sm text-gray-900 py-2">
{user?.phone || <span className="text-gray-500 italic">Not provided</span>}
</p>
)}
</div>
</div>
{editingProfile && (
<div className="flex items-center justify-end space-x-3 pt-6 border-t border-gray-200 mt-6">
<Button
variant="outline"
size="sm"
onClick={() => setEditingProfile(false)}
disabled={profile.isSubmitting}
>
<XMarkIcon className="h-4 w-4 mr-1" />
Cancel
</Button>
<Button
size="sm"
onClick={() => {
void profile.handleSubmit().then(() => {
setEditingProfile(false);
}).catch(() => {
// Error is handled by useZodForm
});
}}
disabled={profile.isSubmitting}
>
{profile.isSubmitting ? (
<>
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white mr-2"></div>
Saving...
</>
) : (
<>
<CheckIcon className="h-4 w-4 mr-1" />
Save Changes
</>
)}
</Button>
</div>
)}
</div>
</div>
<div className="bg-white shadow-sm rounded-xl border border-gray-200 mt-8">
<div className="px-6 py-5 border-b border-gray-200">
<div className="flex items-center justify-between">
<div className="flex items-center space-x-3">
<MapPinIcon className="h-6 w-6 text-blue-600" />
<h2 className="text-xl font-semibold text-gray-900">Address Information</h2>
</div>
{!editingAddress && (
<Button variant="outline" size="sm" onClick={() => setEditingAddress(true)}>
<PencilIcon className="h-4 w-4 mr-2" />
Edit
</Button>
)}
</div>
</div>
<div className="p-6">
{editingAddress ? (
<div className="space-y-6">
<AddressForm
initialAddress={{
street: address.values.street,
streetLine2: address.values.streetLine2,
city: address.values.city,
state: address.values.state,
postalCode: address.values.postalCode,
country: address.values.country,
}}
onChange={a => {
address.setValue("street", a.street);
address.setValue("streetLine2", a.streetLine2);
address.setValue("city", a.city);
address.setValue("state", a.state);
address.setValue("postalCode", a.postalCode);
address.setValue("country", a.country);
}}
title="Mailing Address"
/>
<div className="flex items-center justify-end space-x-3 pt-2">
<Button
variant="outline"
size="sm"
onClick={() => setEditingAddress(false)}
disabled={address.isSubmitting}
>
<XMarkIcon className="h-4 w-4 mr-2" />
Cancel
</Button>
<Button
size="sm"
onClick={() => {
void address.handleSubmit().then(() => {
setEditingAddress(false);
}).catch(() => {
// Error is handled by useZodForm
});
}}
disabled={address.isSubmitting}
>
{address.isSubmitting ? (
<>
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white mr-2"></div>
Saving...
</>
) : (
<>
<CheckIcon className="h-4 w-4 mr-2" />
Save Address
</>
)}
</Button>
</div>
{address.submitError && (
<AlertBanner variant="error" title="Address Error">
{address.submitError}
</AlertBanner>
)}
</div>
) : (
<div>
{address.values.street || address.values.city ? (
<div className="bg-gray-50 rounded-lg p-4">
<div className="text-gray-900 space-y-1">
{address.values.street && <p className="font-medium">{address.values.street}</p>}
{address.values.streetLine2 && <p>{address.values.streetLine2}</p>}
<p>
{[address.values.city, address.values.state, address.values.postalCode]
.filter(Boolean)
.join(", ")}
</p>
<p>{address.values.country}</p>
</div>
</div>
) : (
<div className="text-center py-8">
<MapPinIcon className="h-12 w-12 text-gray-400 mx-auto mb-4" />
<p className="text-gray-600 mb-4">No address on file</p>
<Button onClick={() => setEditingAddress(true)}>Add Address</Button>
</div>
)}
</div>
)}
</div>
</div>
</div>
</div>
);
}