277 lines
8.4 KiB
TypeScript
277 lines
8.4 KiB
TypeScript
"use client";
|
|
|
|
import { useEffect, useState } from "react";
|
|
import { CountrySelect } from "@/components/CountrySelect";
|
|
|
|
type Address = {
|
|
id: string;
|
|
first_name: string;
|
|
last_name: string;
|
|
address_1: string;
|
|
address_2?: string;
|
|
city: string;
|
|
province?: string;
|
|
postal_code: string;
|
|
country_code: string;
|
|
phone?: string;
|
|
};
|
|
|
|
const inputClass =
|
|
"no-ring w-full rounded-xl border border-lightline px-6 py-3 shadow-lg text-lighttext transition-all duration-200 ease-in-out placeholder:text-lightsec hover:border-lightline-hover focus:border-lightline-focus dark:border-darkline dark:text-darktext dark:placeholder:text-darksec dark:hover:border-darkline-hover dark:focus:border-darkline-focus";
|
|
|
|
const emptyForm = {
|
|
first_name: "",
|
|
last_name: "",
|
|
address_1: "",
|
|
address_2: "",
|
|
city: "",
|
|
province: "",
|
|
postal_code: "",
|
|
country_code: "nl",
|
|
phone: "",
|
|
};
|
|
|
|
export function AddressesTab() {
|
|
const [address, setAddress] = useState<Address | null>(null);
|
|
const [loading, setLoading] = useState(true);
|
|
const [editing, setEditing] = useState(false);
|
|
const [form, setForm] = useState(emptyForm);
|
|
const [submitting, setSubmitting] = useState(false);
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
useEffect(() => {
|
|
fetchAddress();
|
|
}, []);
|
|
|
|
async function fetchAddress() {
|
|
try {
|
|
const res = await fetch("/api/account/addresses");
|
|
if (res.ok) {
|
|
const data = await res.json();
|
|
const addresses: Address[] = data.addresses ?? [];
|
|
setAddress(addresses[0] ?? null);
|
|
}
|
|
} catch {
|
|
// Silently fail
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}
|
|
|
|
function startEdit() {
|
|
if (address) {
|
|
setForm({
|
|
first_name: address.first_name ?? "",
|
|
last_name: address.last_name ?? "",
|
|
address_1: address.address_1 ?? "",
|
|
address_2: address.address_2 ?? "",
|
|
city: address.city ?? "",
|
|
province: address.province ?? "",
|
|
postal_code: address.postal_code ?? "",
|
|
country_code: address.country_code ?? "nl",
|
|
phone: address.phone ?? "",
|
|
});
|
|
} else {
|
|
setForm(emptyForm);
|
|
}
|
|
setEditing(true);
|
|
setError(null);
|
|
}
|
|
|
|
function cancelForm() {
|
|
setEditing(false);
|
|
setForm(emptyForm);
|
|
setError(null);
|
|
}
|
|
|
|
async function handleSubmit(e: React.FormEvent) {
|
|
e.preventDefault();
|
|
setSubmitting(true);
|
|
setError(null);
|
|
|
|
const url = address ? `/api/account/addresses/${address.id}` : "/api/account/addresses";
|
|
|
|
try {
|
|
const res = await fetch(url, {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify(form),
|
|
});
|
|
|
|
if (!res.ok) {
|
|
const data = await res.json().catch(() => ({}));
|
|
throw new Error(data.error ?? "Failed to save address");
|
|
}
|
|
|
|
cancelForm();
|
|
await fetchAddress();
|
|
} catch (err) {
|
|
setError(err instanceof Error ? err.message : "Something went wrong");
|
|
} finally {
|
|
setSubmitting(false);
|
|
}
|
|
}
|
|
|
|
function updateField(field: string, value: string) {
|
|
setForm((prev) => ({ ...prev, [field]: value }));
|
|
}
|
|
|
|
if (loading) {
|
|
return <p className="py-12 text-center text-lightsec dark:text-darksec">Loading address…</p>;
|
|
}
|
|
|
|
// Display mode: show the saved address (or empty state) with an Edit / Add button
|
|
if (!editing) {
|
|
if (address) {
|
|
return (
|
|
<div>
|
|
<p className="font-silkasb">
|
|
{address.first_name} {address.last_name}
|
|
</p>
|
|
<p className="text-sm text-lightsec dark:text-darksec">
|
|
{address.address_1}
|
|
{address.address_2 ? `, ${address.address_2}` : ""}
|
|
</p>
|
|
<p className="text-sm text-lightsec dark:text-darksec">
|
|
{address.city}
|
|
{address.province ? `, ${address.province}` : ""}, {address.postal_code},{" "}
|
|
{address.country_code.toUpperCase()}
|
|
</p>
|
|
{address.phone && (
|
|
<p className="text-sm text-lightsec dark:text-darksec">{address.phone}</p>
|
|
)}
|
|
<div className="mt-3">
|
|
<button
|
|
type="button"
|
|
onClick={startEdit}
|
|
className="text-sm text-trptkblue underline transition-opacity hover:opacity-70 dark:text-white"
|
|
>
|
|
Edit
|
|
</button>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div>
|
|
<p className="py-8 text-center text-lightsec dark:text-darksec">
|
|
No address saved. Add one to speed up checkout.
|
|
</p>
|
|
<button
|
|
type="button"
|
|
onClick={startEdit}
|
|
className="w-full rounded-xl border border-dashed border-lightline px-4 py-4 text-sm text-lightsec transition-all duration-200 hover:border-lightline-hover hover:text-trptkblue dark:border-darkline dark:text-darksec dark:hover:border-darkline-hover dark:hover:text-white"
|
|
>
|
|
+ Add Address
|
|
</button>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// Edit / Add form
|
|
return (
|
|
<form
|
|
onSubmit={handleSubmit}
|
|
className="rounded-2xl border border-lightline p-6 dark:border-darkline"
|
|
>
|
|
<h3 className="mb-4 font-silkasb text-sm">{address ? "Edit Address" : "Add Address"}</h3>
|
|
|
|
<div className="flex flex-col gap-3">
|
|
<div className="grid gap-3 sm:grid-cols-2">
|
|
<input
|
|
type="text"
|
|
required
|
|
placeholder="First name"
|
|
value={form.first_name}
|
|
onChange={(e) => updateField("first_name", e.target.value)}
|
|
className={inputClass}
|
|
/>
|
|
<input
|
|
type="text"
|
|
required
|
|
placeholder="Last name"
|
|
value={form.last_name}
|
|
onChange={(e) => updateField("last_name", e.target.value)}
|
|
className={inputClass}
|
|
/>
|
|
</div>
|
|
<input
|
|
type="text"
|
|
required
|
|
placeholder="Address"
|
|
value={form.address_1}
|
|
onChange={(e) => updateField("address_1", e.target.value)}
|
|
className={inputClass}
|
|
/>
|
|
<input
|
|
type="text"
|
|
placeholder="Apartment, suite, etc. (optional)"
|
|
value={form.address_2}
|
|
onChange={(e) => updateField("address_2", e.target.value)}
|
|
className={inputClass}
|
|
/>
|
|
<div className="grid gap-3 sm:grid-cols-3">
|
|
<input
|
|
type="text"
|
|
required
|
|
placeholder="City"
|
|
value={form.city}
|
|
onChange={(e) => updateField("city", e.target.value)}
|
|
className={inputClass}
|
|
/>
|
|
<input
|
|
type="text"
|
|
placeholder="Province / State"
|
|
value={form.province}
|
|
onChange={(e) => updateField("province", e.target.value)}
|
|
className={inputClass}
|
|
/>
|
|
<input
|
|
type="text"
|
|
required
|
|
placeholder="Postal code"
|
|
value={form.postal_code}
|
|
onChange={(e) => updateField("postal_code", e.target.value)}
|
|
className={inputClass}
|
|
/>
|
|
</div>
|
|
<CountrySelect
|
|
value={form.country_code}
|
|
onChange={(code) => updateField("country_code", code)}
|
|
className={inputClass}
|
|
/>
|
|
<input
|
|
type="tel"
|
|
placeholder="Phone (optional)"
|
|
value={form.phone}
|
|
onChange={(e) => updateField("phone", e.target.value)}
|
|
className={inputClass}
|
|
/>
|
|
</div>
|
|
|
|
{error && (
|
|
<div className="mt-3 rounded-xl border border-red-300 bg-red-50 px-4 py-3 text-sm text-red-700 dark:border-red-800 dark:bg-red-950 dark:text-red-300">
|
|
{error}
|
|
</div>
|
|
)}
|
|
|
|
<div className="mt-4 flex gap-3">
|
|
<button
|
|
type="submit"
|
|
disabled={submitting}
|
|
className="rounded-xl bg-trptkblue px-5 py-2.5 font-silkasb text-sm text-white shadow-lg transition-all duration-200 hover:opacity-90 disabled:pointer-events-none disabled:opacity-50 dark:bg-white dark:text-lighttext"
|
|
>
|
|
{submitting ? "Saving\u2026" : "Save Address"}
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onClick={cancelForm}
|
|
className="rounded-xl border border-lightline px-5 py-2.5 text-sm transition-all duration-200 hover:border-lightline-hover hover:text-trptkblue dark:border-darkline dark:hover:border-darkline-hover dark:hover:text-white"
|
|
>
|
|
Cancel
|
|
</button>
|
|
</div>
|
|
</form>
|
|
);
|
|
}
|