Files
domain-wall/apps/frontend/src/lib/domains/customerinfo/view/customer-pii-form.svelte
2025-10-21 18:41:19 +03:00

242 lines
6.4 KiB
Svelte

<script lang="ts">
import LabelWrapper from "$lib/components/atoms/label-wrapper.svelte";
import Title from "$lib/components/atoms/title.svelte";
import Input from "$lib/components/ui/input/input.svelte";
import * as Select from "$lib/components/ui/select";
import * as Popover from "$lib/components/ui/popover";
import * as Command from "$lib/components/ui/command";
import { Button } from "$lib/components/ui/button";
import { COUNTRIES_SELECT } from "$lib/core/countries";
import { capitalize } from "$lib/core/string.utils";
import { PHONE_COUNTRY_CODES } from "@pkg/logic/core/data/phonecc";
import { cn } from "$lib/utils";
import { tick } from "svelte";
import ChevronsUpDownIcon from "~icons/lucide/chevrons-up-down";
import CheckIcon from "~icons/lucide/check";
import type { CustomerInfoModel } from "../data";
import { customerInfoVM } from "./customerinfo.vm.svelte";
let { info = $bindable() }: { info: CustomerInfoModel } = $props();
let phoneCodeOpen = $state(false);
let phoneCodeTriggerRef = $state<HTMLButtonElement>(null!);
function onSubmit(e: SubmitEvent) {
e.preventDefault();
customerInfoVM.validateCustomerInfo(info);
}
function debounceValidate() {
customerInfoVM.debounceValidate(info);
}
function closePhoneCodeAndFocus() {
phoneCodeOpen = false;
tick().then(() => {
phoneCodeTriggerRef?.focus();
});
}
</script>
<form action="#" class="flex w-full flex-col gap-4" onsubmit={onSubmit}>
<!-- Name Fields -->
<div class="flex flex-col gap-4 md:flex-row">
<LabelWrapper label="First Name" error={customerInfoVM.errors.firstName}>
<Input
placeholder="First Name"
bind:value={info.firstName}
required
oninput={() => debounceValidate()}
minlength={1}
maxlength={64}
/>
</LabelWrapper>
<LabelWrapper
label="Middle Name"
error={customerInfoVM.errors.middleName}
>
<Input
placeholder="Middle Name (Optional)"
bind:value={info.middleName}
oninput={() => debounceValidate()}
maxlength={64}
/>
</LabelWrapper>
<LabelWrapper label="Last Name" error={customerInfoVM.errors.lastName}>
<Input
placeholder="Last Name"
bind:value={info.lastName}
required
oninput={() => debounceValidate()}
minlength={1}
maxlength={64}
/>
</LabelWrapper>
</div>
<!-- Email Field -->
<LabelWrapper label="Email" error={customerInfoVM.errors.email}>
<Input
placeholder="Email"
bind:value={info.email}
type="email"
oninput={() => debounceValidate()}
required
maxlength={128}
/>
</LabelWrapper>
<!-- Phone Number Field -->
<LabelWrapper label="Phone Number" error={customerInfoVM.errors.phoneNumber}>
<div class="flex gap-2">
<Popover.Root bind:open={phoneCodeOpen}>
<Popover.Trigger bind:ref={phoneCodeTriggerRef}>
{#snippet child({ props })}
<Button
{...props}
variant="outline"
class="w-32 justify-between"
role="combobox"
aria-expanded={phoneCodeOpen}
>
{info.phoneCountryCode || "Select"}
<ChevronsUpDownIcon class="h-4 w-4 opacity-50" />
</Button>
{/snippet}
</Popover.Trigger>
<Popover.Content class="w-[300px] p-0">
<Command.Root>
<Command.Input placeholder="Search country..." class="h-9" />
<Command.List class="command-scrollbar max-h-[300px]">
<Command.Empty>No country found.</Command.Empty>
<Command.Group>
{#each PHONE_COUNTRY_CODES as { country, phoneCode } (country)}
<Command.Item
value={`${phoneCode} ${country}`}
onSelect={() => {
info.phoneCountryCode = phoneCode;
debounceValidate();
closePhoneCodeAndFocus();
}}
>
<CheckIcon
class={cn(
"h-4 w-4 flex-shrink-0",
info.phoneCountryCode !== phoneCode &&
"text-transparent",
)}
/>
<span class="flex flex-1 items-center justify-between gap-3">
<span class="font-semibold text-gray-900">{phoneCode}</span>
<span class="text-sm text-gray-600">{country}</span>
</span>
</Command.Item>
{/each}
</Command.Group>
</Command.List>
</Command.Root>
</Popover.Content>
</Popover.Root>
<Input
placeholder="Phone Number"
type="tel"
bind:value={info.phoneNumber}
required
oninput={() => debounceValidate()}
class="flex-1"
minlength={1}
maxlength={20}
/>
</div>
</LabelWrapper>
<!-- Address Section -->
<Title size="h5">Address Information</Title>
<div class="flex flex-col gap-4 md:flex-row">
<LabelWrapper label="Country" error={customerInfoVM.errors.country}>
<Select.Root
type="single"
required
onValueChange={(e) => {
info.country = e;
debounceValidate();
}}
name="country"
>
<Select.Trigger class="w-full">
{capitalize(
info.country.length > 0 ? info.country : "Select",
)}
</Select.Trigger>
<Select.Content>
{#each COUNTRIES_SELECT as country}
<Select.Item value={country.value}>
{country.label}
</Select.Item>
{/each}
</Select.Content>
</Select.Root>
</LabelWrapper>
<LabelWrapper label="State" error={customerInfoVM.errors.state}>
<Input
placeholder="State/Province"
bind:value={info.state}
required
oninput={() => debounceValidate()}
minlength={1}
maxlength={128}
/>
</LabelWrapper>
</div>
<div class="flex flex-col gap-4 md:flex-row">
<LabelWrapper label="City" error={customerInfoVM.errors.city}>
<Input
placeholder="City"
bind:value={info.city}
required
minlength={1}
maxlength={128}
oninput={() => debounceValidate()}
/>
</LabelWrapper>
<LabelWrapper label="Zip Code" error={customerInfoVM.errors.zipCode}>
<Input
placeholder="Zip/Postal Code"
bind:value={info.zipCode}
required
minlength={1}
maxlength={21}
oninput={() => debounceValidate()}
/>
</LabelWrapper>
</div>
<LabelWrapper label="Address" error={customerInfoVM.errors.address}>
<Input
placeholder="Street Address"
bind:value={info.address}
required
minlength={1}
oninput={() => debounceValidate()}
/>
</LabelWrapper>
<LabelWrapper
label="Address 2 (Optional)"
error={customerInfoVM.errors.address2}
>
<Input
placeholder="Apartment, suite, etc. (Optional)"
bind:value={info.address2}
oninput={() => debounceValidate()}
/>
</LabelWrapper>
</form>