242 lines
6.4 KiB
Svelte
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>
|