Refactor Address Handling in Signup and Profile Components
- Updated address handling in Salesforce account and contact services to remove unnecessary address fields during signup, improving data management. - Refactored address input fields in the SignupForm and AddressForm components to ensure proper labeling and validation for Japanese address formats. - Enhanced address display logic in ProfileContainer and AddressConfirmation components for better user experience. - Standardized address field names and improved placeholder texts for clarity and consistency across the application.
This commit is contained in:
parent
cc0200bd5e
commit
1a3032bbd3
@ -175,14 +175,8 @@ export class SalesforceAccountService {
|
|||||||
): Promise<{ accountId: string; accountNumber: string }> {
|
): Promise<{ accountId: string; accountNumber: string }> {
|
||||||
this.logger.log("Creating new Salesforce Account", { email: data.email });
|
this.logger.log("Creating new Salesforce Account", { email: data.email });
|
||||||
|
|
||||||
const accountPayload = {
|
const accountPayload: Record<string, unknown> = {
|
||||||
Name: `${data.firstName} ${data.lastName}`,
|
Name: `${data.firstName} ${data.lastName}`,
|
||||||
BillingStreet:
|
|
||||||
data.address.address1 + (data.address.address2 ? `\n${data.address.address2}` : ""),
|
|
||||||
BillingCity: data.address.city,
|
|
||||||
BillingState: data.address.state,
|
|
||||||
BillingPostalCode: data.address.postcode,
|
|
||||||
BillingCountry: data.address.country,
|
|
||||||
Phone: data.phone,
|
Phone: data.phone,
|
||||||
// Portal tracking fields
|
// Portal tracking fields
|
||||||
[this.portalStatusField]: "Active",
|
[this.portalStatusField]: "Active",
|
||||||
@ -242,18 +236,12 @@ export class SalesforceAccountService {
|
|||||||
email: data.email,
|
email: data.email,
|
||||||
});
|
});
|
||||||
|
|
||||||
const contactPayload = {
|
const contactPayload: Record<string, unknown> = {
|
||||||
AccountId: data.accountId,
|
AccountId: data.accountId,
|
||||||
FirstName: data.firstName,
|
FirstName: data.firstName,
|
||||||
LastName: data.lastName,
|
LastName: data.lastName,
|
||||||
Email: data.email,
|
Email: data.email,
|
||||||
Phone: data.phone,
|
Phone: data.phone,
|
||||||
MailingStreet:
|
|
||||||
data.address.address1 + (data.address.address2 ? `\n${data.address.address2}` : ""),
|
|
||||||
MailingCity: data.address.city,
|
|
||||||
MailingState: data.address.state,
|
|
||||||
MailingPostalCode: data.address.postcode,
|
|
||||||
MailingCountry: data.address.country,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -351,14 +339,6 @@ export interface CreateSalesforceAccountRequest {
|
|||||||
lastName: string;
|
lastName: string;
|
||||||
email: string;
|
email: string;
|
||||||
phone: string;
|
phone: string;
|
||||||
address: {
|
|
||||||
address1: string;
|
|
||||||
address2?: string;
|
|
||||||
city: string;
|
|
||||||
state: string;
|
|
||||||
postcode: string;
|
|
||||||
country: string;
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -370,12 +350,4 @@ export interface CreateSalesforceContactRequest {
|
|||||||
lastName: string;
|
lastName: string;
|
||||||
email: string;
|
email: string;
|
||||||
phone: string;
|
phone: string;
|
||||||
address: {
|
|
||||||
address1: string;
|
|
||||||
address2?: string;
|
|
||||||
city: string;
|
|
||||||
state: string;
|
|
||||||
postcode: string;
|
|
||||||
country: string;
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -271,14 +271,7 @@ export class SignupWorkflowService {
|
|||||||
lastName,
|
lastName,
|
||||||
email: normalizedEmail,
|
email: normalizedEmail,
|
||||||
phone,
|
phone,
|
||||||
address: {
|
// Address not added to Salesforce during signup
|
||||||
address1: address.address1,
|
|
||||||
address2: address.address2 || undefined,
|
|
||||||
city: address.city,
|
|
||||||
state: address.state,
|
|
||||||
postcode: address.postcode,
|
|
||||||
country: address.country,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
await this.salesforceAccountService.createContact({
|
await this.salesforceAccountService.createContact({
|
||||||
@ -287,14 +280,7 @@ export class SignupWorkflowService {
|
|||||||
lastName,
|
lastName,
|
||||||
email: normalizedEmail,
|
email: normalizedEmail,
|
||||||
phone,
|
phone,
|
||||||
address: {
|
// Address not added to Salesforce during signup
|
||||||
address1: address.address1,
|
|
||||||
address2: address.address2 || undefined,
|
|
||||||
city: address.city,
|
|
||||||
state: address.state,
|
|
||||||
postcode: address.postcode,
|
|
||||||
country: address.country,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@ -78,14 +78,6 @@ export class SignupAccountResolverService {
|
|||||||
lastName,
|
lastName,
|
||||||
email: normalizedEmail,
|
email: normalizedEmail,
|
||||||
phone,
|
phone,
|
||||||
address: {
|
|
||||||
address1: address.address1,
|
|
||||||
address2: address.address2 || undefined,
|
|
||||||
city: address.city,
|
|
||||||
state: address.state,
|
|
||||||
postcode: address.postcode,
|
|
||||||
country: address.country,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
await this.salesforceAccountService.createContact({
|
await this.salesforceAccountService.createContact({
|
||||||
@ -94,14 +86,6 @@ export class SignupAccountResolverService {
|
|||||||
lastName,
|
lastName,
|
||||||
email: normalizedEmail,
|
email: normalizedEmail,
|
||||||
phone,
|
phone,
|
||||||
address: {
|
|
||||||
address1: address.address1,
|
|
||||||
address2: address.address2 || undefined,
|
|
||||||
city: address.city,
|
|
||||||
state: address.state,
|
|
||||||
postcode: address.postcode,
|
|
||||||
country: address.country,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const snapshot: SignupAccountSnapshot = {
|
const snapshot: SignupAccountSnapshot = {
|
||||||
|
|||||||
@ -105,7 +105,7 @@ export class SignupWhmcsService {
|
|||||||
companyname: params.company || "",
|
companyname: params.company || "",
|
||||||
phonenumber: params.phone,
|
phonenumber: params.phone,
|
||||||
address1: params.address.address1,
|
address1: params.address.address1,
|
||||||
address2: params.address.address2 || "",
|
address2: params.address.address2 ?? "",
|
||||||
city: params.address.city,
|
city: params.address.city,
|
||||||
state: params.address.state,
|
state: params.address.state,
|
||||||
postcode: params.address.postcode,
|
postcode: params.address.postcode,
|
||||||
|
|||||||
@ -88,8 +88,12 @@ export function AddressCard({
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="space-y-1.5 text-foreground">
|
<div className="space-y-1.5 text-foreground">
|
||||||
{address.address1 && <p className="font-semibold text-base">{address.address1}</p>}
|
{(address.address2 || address.address1) && (
|
||||||
{address.address2 && <p className="text-muted-foreground">{address.address2}</p>}
|
<p className="font-semibold text-base">{address.address2 || address.address1}</p>
|
||||||
|
)}
|
||||||
|
{address.address2 && address.address1 && (
|
||||||
|
<p className="text-muted-foreground">{address.address1}</p>
|
||||||
|
)}
|
||||||
{(address.city || address.state || address.postcode) && (
|
{(address.city || address.state || address.postcode) && (
|
||||||
<p className="text-muted-foreground">
|
<p className="text-muted-foreground">
|
||||||
{[address.city, address.state, address.postcode].filter(Boolean).join(", ")}
|
{[address.city, address.state, address.postcode].filter(Boolean).join(", ")}
|
||||||
|
|||||||
@ -460,11 +460,13 @@ export default function ProfileContainer() {
|
|||||||
{address.values.address1 || address.values.city ? (
|
{address.values.address1 || address.values.city ? (
|
||||||
<div className="bg-card rounded-lg p-5 border border-border shadow-sm">
|
<div className="bg-card rounded-lg p-5 border border-border shadow-sm">
|
||||||
<div className="text-foreground space-y-1.5">
|
<div className="text-foreground space-y-1.5">
|
||||||
{address.values.address1 && (
|
{(address.values.address2 || address.values.address1) && (
|
||||||
<p className="font-medium text-base">{address.values.address1}</p>
|
<p className="font-medium text-base">
|
||||||
|
{address.values.address2 || address.values.address1}
|
||||||
|
</p>
|
||||||
)}
|
)}
|
||||||
{address.values.address2 && (
|
{address.values.address2 && address.values.address1 && (
|
||||||
<p className="text-muted-foreground">{address.values.address2}</p>
|
<p className="text-muted-foreground">{address.values.address1}</p>
|
||||||
)}
|
)}
|
||||||
<p className="text-muted-foreground">
|
<p className="text-muted-foreground">
|
||||||
{[address.values.city, address.values.state, address.values.postcode]
|
{[address.values.city, address.values.state, address.values.postcode]
|
||||||
|
|||||||
@ -160,6 +160,15 @@ export function LoginForm({
|
|||||||
Sign up
|
Sign up
|
||||||
</Link>
|
</Link>
|
||||||
</p>
|
</p>
|
||||||
|
<p className="text-sm text-muted-foreground mt-1">
|
||||||
|
Existing customer?{" "}
|
||||||
|
<Link
|
||||||
|
href={`/auth/migrate${redirectQuery}`}
|
||||||
|
className="font-medium text-primary hover:text-primary-hover transition-colors duration-200"
|
||||||
|
>
|
||||||
|
Migrate your account
|
||||||
|
</Link>
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@ -5,16 +5,19 @@
|
|||||||
|
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState, useCallback } from "react";
|
import { useState, useCallback, useEffect, useRef } from "react";
|
||||||
|
import { flushSync } from "react-dom";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useSearchParams } from "next/navigation";
|
import { useSearchParams } from "next/navigation";
|
||||||
import { ErrorMessage } from "@/components/atoms";
|
import { ErrorMessage } from "@/components/atoms";
|
||||||
import { useSignupWithRedirect } from "../../hooks/use-auth";
|
import { useSignupWithRedirect } from "../../hooks/use-auth";
|
||||||
import { signupInputSchema, buildSignupRequest } from "@customer-portal/domain/auth";
|
import { signupInputSchema, buildSignupRequest } from "@customer-portal/domain/auth";
|
||||||
|
import { genderEnum } from "@customer-portal/domain/common";
|
||||||
import { addressFormSchema } from "@customer-portal/domain/customer";
|
import { addressFormSchema } from "@customer-portal/domain/customer";
|
||||||
import { useZodForm } from "@/shared/hooks";
|
import { useZodForm } from "@/shared/hooks";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { getSafeRedirect } from "@/features/auth/utils/route-protection";
|
import { getSafeRedirect } from "@/features/auth/utils/route-protection";
|
||||||
|
import { formatJapanesePostalCode } from "@/shared/constants";
|
||||||
|
|
||||||
import { MultiStepForm } from "./MultiStepForm";
|
import { MultiStepForm } from "./MultiStepForm";
|
||||||
import { AccountStep } from "./steps/AccountStep";
|
import { AccountStep } from "./steps/AccountStep";
|
||||||
@ -30,12 +33,20 @@ import { PasswordStep } from "./steps/PasswordStep";
|
|||||||
* - dateOfBirth: Required for signup (domain schema makes it optional)
|
* - dateOfBirth: Required for signup (domain schema makes it optional)
|
||||||
* - gender: Required for signup (domain schema makes it optional)
|
* - gender: Required for signup (domain schema makes it optional)
|
||||||
*/
|
*/
|
||||||
const genderSchema = z.enum(["male", "female", "other"]);
|
const genderSchema = genderEnum;
|
||||||
|
|
||||||
|
const signupAddressSchema = addressFormSchema.extend({
|
||||||
|
address2: z
|
||||||
|
.string()
|
||||||
|
.min(1, "Address line 2 is required")
|
||||||
|
.max(200, "Address line 2 is too long")
|
||||||
|
.trim(),
|
||||||
|
});
|
||||||
|
|
||||||
const signupFormBaseSchema = signupInputSchema.omit({ sfNumber: true }).extend({
|
const signupFormBaseSchema = signupInputSchema.omit({ sfNumber: true }).extend({
|
||||||
confirmPassword: z.string().min(1, "Please confirm your password"),
|
confirmPassword: z.string().min(1, "Please confirm your password"),
|
||||||
phoneCountryCode: z.string().regex(/^\+\d{1,4}$/, "Enter a valid country code (e.g., +81)"),
|
phoneCountryCode: z.string().regex(/^\+\d{1,4}$/, "Enter a valid country code (e.g., +81)"),
|
||||||
address: addressFormSchema,
|
address: signupAddressSchema,
|
||||||
dateOfBirth: z.string().min(1, "Date of birth is required"),
|
dateOfBirth: z.string().min(1, "Date of birth is required"),
|
||||||
gender: genderSchema,
|
gender: genderSchema,
|
||||||
});
|
});
|
||||||
@ -51,6 +62,7 @@ const signupFormSchema = signupFormBaseSchema
|
|||||||
});
|
});
|
||||||
|
|
||||||
type SignupFormData = z.infer<typeof signupFormSchema>;
|
type SignupFormData = z.infer<typeof signupFormSchema>;
|
||||||
|
type SignupAddress = SignupFormData["address"];
|
||||||
|
|
||||||
interface SignupFormProps {
|
interface SignupFormProps {
|
||||||
onSuccess?: () => void;
|
onSuccess?: () => void;
|
||||||
@ -122,6 +134,7 @@ export function SignupForm({
|
|||||||
initialEmail,
|
initialEmail,
|
||||||
showFooterLinks = true,
|
showFooterLinks = true,
|
||||||
}: SignupFormProps) {
|
}: SignupFormProps) {
|
||||||
|
const formRef = useRef<HTMLFormElement | null>(null);
|
||||||
const searchParams = useSearchParams();
|
const searchParams = useSearchParams();
|
||||||
const { signup, loading, error, clearError } = useSignupWithRedirect({ redirectTo });
|
const { signup, loading, error, clearError } = useSignupWithRedirect({ redirectTo });
|
||||||
const [step, setStep] = useState(0);
|
const [step, setStep] = useState(0);
|
||||||
@ -163,8 +176,10 @@ export function SignupForm({
|
|||||||
const formattedPhone = `+${countryDigits}.${phoneDigits}`;
|
const formattedPhone = `+${countryDigits}.${phoneDigits}`;
|
||||||
|
|
||||||
// Build request with normalized address and phone
|
// Build request with normalized address and phone
|
||||||
|
// Exclude UI-only fields (confirmPassword) from the request
|
||||||
|
const { confirmPassword: _confirmPassword, ...requestData } = data;
|
||||||
const request = buildSignupRequest({
|
const request = buildSignupRequest({
|
||||||
...data,
|
...requestData,
|
||||||
phone: formattedPhone,
|
phone: formattedPhone,
|
||||||
dateOfBirth: data.dateOfBirth || undefined,
|
dateOfBirth: data.dateOfBirth || undefined,
|
||||||
gender: data.gender || undefined,
|
gender: data.gender || undefined,
|
||||||
@ -193,6 +208,105 @@ export function SignupForm({
|
|||||||
isSubmitting,
|
isSubmitting,
|
||||||
} = form;
|
} = form;
|
||||||
|
|
||||||
|
const normalizeAutofillValue = useCallback(
|
||||||
|
(field: string, value: string) => {
|
||||||
|
switch (field) {
|
||||||
|
case "phoneCountryCode": {
|
||||||
|
let normalized = value.replace(/[^\d+]/g, "");
|
||||||
|
if (!normalized.startsWith("+")) normalized = "+" + normalized.replace(/\+/g, "");
|
||||||
|
return normalized.slice(0, 5);
|
||||||
|
}
|
||||||
|
case "phone":
|
||||||
|
return value.replace(/\D/g, "");
|
||||||
|
case "address.postcode":
|
||||||
|
return formatJapanesePostalCode(value);
|
||||||
|
default:
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[formatJapanesePostalCode]
|
||||||
|
);
|
||||||
|
|
||||||
|
const syncStepValues = useCallback(
|
||||||
|
(shouldFlush = true) => {
|
||||||
|
const formNode = formRef.current;
|
||||||
|
if (!formNode) {
|
||||||
|
return values;
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextValues: SignupFormData = {
|
||||||
|
...values,
|
||||||
|
address: { ...values.address },
|
||||||
|
};
|
||||||
|
|
||||||
|
const fields = formNode.querySelectorAll<HTMLInputElement | HTMLSelectElement>(
|
||||||
|
"[data-field]"
|
||||||
|
);
|
||||||
|
fields.forEach(field => {
|
||||||
|
const key = field.dataset.field;
|
||||||
|
if (!key) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const normalized = normalizeAutofillValue(key, field.value);
|
||||||
|
if (key.startsWith("address.")) {
|
||||||
|
const addressKey = key.replace("address.", "") as keyof SignupAddress;
|
||||||
|
nextValues.address[addressKey] = normalized;
|
||||||
|
} else if (key === "acceptTerms" || key === "marketingConsent") {
|
||||||
|
// Handle boolean fields separately
|
||||||
|
const boolValue =
|
||||||
|
field.type === "checkbox" ? (field as HTMLInputElement).checked : normalized === "true";
|
||||||
|
(nextValues as Record<string, unknown>)[key] = boolValue;
|
||||||
|
} else {
|
||||||
|
// Only assign to string fields
|
||||||
|
const stringKey = key as keyof Pick<
|
||||||
|
SignupFormData,
|
||||||
|
Exclude<keyof SignupFormData, "address" | "acceptTerms" | "marketingConsent">
|
||||||
|
>;
|
||||||
|
(nextValues as Record<string, unknown>)[stringKey] = normalized;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const applySyncedValues = () => {
|
||||||
|
(Object.keys(nextValues) as Array<keyof SignupFormData>).forEach(key => {
|
||||||
|
if (key === "address") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (nextValues[key] !== values[key]) {
|
||||||
|
setFormValue(key, nextValues[key]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const addressChanged = (Object.keys(nextValues.address) as Array<keyof SignupAddress>).some(
|
||||||
|
key => nextValues.address[key] !== values.address[key]
|
||||||
|
);
|
||||||
|
if (addressChanged) {
|
||||||
|
setFormValue("address", nextValues.address);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (shouldFlush) {
|
||||||
|
flushSync(() => {
|
||||||
|
applySyncedValues();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
applySyncedValues();
|
||||||
|
}
|
||||||
|
|
||||||
|
return nextValues;
|
||||||
|
},
|
||||||
|
[normalizeAutofillValue, setFormValue, values]
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const syncTimer = window.setTimeout(() => {
|
||||||
|
syncStepValues(false);
|
||||||
|
}, 0);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.clearTimeout(syncTimer);
|
||||||
|
};
|
||||||
|
}, [step, syncStepValues]);
|
||||||
|
|
||||||
const isLastStep = step === STEPS.length - 1;
|
const isLastStep = step === STEPS.length - 1;
|
||||||
|
|
||||||
const markStepTouched = useCallback(
|
const markStepTouched = useCallback(
|
||||||
@ -208,7 +322,7 @@ export function SignupForm({
|
|||||||
);
|
);
|
||||||
|
|
||||||
const isStepValid = useCallback(
|
const isStepValid = useCallback(
|
||||||
(stepIndex: number) => {
|
(stepIndex: number, data: SignupFormData = values) => {
|
||||||
const stepKey = STEPS[stepIndex]?.key;
|
const stepKey = STEPS[stepIndex]?.key;
|
||||||
if (!stepKey) {
|
if (!stepKey) {
|
||||||
return true;
|
return true;
|
||||||
@ -217,12 +331,13 @@ export function SignupForm({
|
|||||||
if (!schema) {
|
if (!schema) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return schema.safeParse(values).success;
|
return schema.safeParse(data).success;
|
||||||
},
|
},
|
||||||
[values]
|
[values]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleNext = useCallback(() => {
|
const handleNext = useCallback(() => {
|
||||||
|
const syncedValues = syncStepValues();
|
||||||
markStepTouched(step);
|
markStepTouched(step);
|
||||||
|
|
||||||
if (isLastStep) {
|
if (isLastStep) {
|
||||||
@ -230,12 +345,12 @@ export function SignupForm({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isStepValid(step)) {
|
if (!isStepValid(step, syncedValues)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setStep(s => Math.min(s + 1, STEPS.length - 1));
|
setStep(s => Math.min(s + 1, STEPS.length - 1));
|
||||||
}, [handleSubmit, isLastStep, isStepValid, markStepTouched, step]);
|
}, [handleSubmit, isLastStep, isStepValid, markStepTouched, step, syncStepValues]);
|
||||||
|
|
||||||
const handlePrevious = useCallback(() => {
|
const handlePrevious = useCallback(() => {
|
||||||
setStep(s => Math.max(0, s - 1));
|
setStep(s => Math.max(0, s - 1));
|
||||||
@ -264,15 +379,24 @@ export function SignupForm({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`w-full ${className}`}>
|
<div className={`w-full ${className}`}>
|
||||||
<MultiStepForm
|
<form
|
||||||
steps={steps}
|
ref={formRef}
|
||||||
currentStep={step}
|
autoComplete="on"
|
||||||
onNext={handleNext}
|
onSubmit={event => {
|
||||||
onPrevious={handlePrevious}
|
event.preventDefault();
|
||||||
isLastStep={isLastStep}
|
handleNext();
|
||||||
isSubmitting={isSubmitting || loading}
|
}}
|
||||||
canProceed={isLastStep || isStepValid(step)}
|
>
|
||||||
/>
|
<MultiStepForm
|
||||||
|
steps={steps}
|
||||||
|
currentStep={step}
|
||||||
|
onNext={handleNext}
|
||||||
|
onPrevious={handlePrevious}
|
||||||
|
isLastStep={isLastStep}
|
||||||
|
isSubmitting={isSubmitting || loading}
|
||||||
|
canProceed={isLastStep || isStepValid(step)}
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
|
|
||||||
{error && (
|
{error && (
|
||||||
<ErrorMessage className="mt-4 text-center p-3 bg-danger-soft rounded-lg">
|
<ErrorMessage className="mt-4 text-center p-3 bg-danger-soft rounded-lg">
|
||||||
|
|||||||
@ -6,6 +6,7 @@
|
|||||||
|
|
||||||
import { Input } from "@/components/atoms";
|
import { Input } from "@/components/atoms";
|
||||||
import { FormField } from "@/components/molecules/FormField/FormField";
|
import { FormField } from "@/components/molecules/FormField/FormField";
|
||||||
|
import { genderEnum } from "@customer-portal/domain/common";
|
||||||
|
|
||||||
interface AccountStepProps {
|
interface AccountStepProps {
|
||||||
form: {
|
form: {
|
||||||
@ -29,6 +30,8 @@ interface AccountStepProps {
|
|||||||
export function AccountStep({ form }: AccountStepProps) {
|
export function AccountStep({ form }: AccountStepProps) {
|
||||||
const { values, errors, touched, setValue, setTouchedField } = form;
|
const { values, errors, touched, setValue, setTouchedField } = form;
|
||||||
const getError = (field: string) => (touched[field] ? errors[field] : undefined);
|
const getError = (field: string) => (touched[field] ? errors[field] : undefined);
|
||||||
|
const genderOptions = genderEnum.options;
|
||||||
|
const formatGender = (value: string) => value.charAt(0).toUpperCase() + value.slice(1);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-5">
|
<div className="space-y-5">
|
||||||
@ -41,8 +44,9 @@ export function AccountStep({ form }: AccountStepProps) {
|
|||||||
onChange={e => setValue("firstName", e.target.value)}
|
onChange={e => setValue("firstName", e.target.value)}
|
||||||
onBlur={() => setTouchedField("firstName")}
|
onBlur={() => setTouchedField("firstName")}
|
||||||
placeholder="Taro"
|
placeholder="Taro"
|
||||||
autoComplete="given-name"
|
autoComplete="section-signup given-name"
|
||||||
autoFocus
|
autoFocus
|
||||||
|
data-field="firstName"
|
||||||
/>
|
/>
|
||||||
</FormField>
|
</FormField>
|
||||||
<FormField label="Last Name" error={getError("lastName")} required>
|
<FormField label="Last Name" error={getError("lastName")} required>
|
||||||
@ -52,7 +56,8 @@ export function AccountStep({ form }: AccountStepProps) {
|
|||||||
onChange={e => setValue("lastName", e.target.value)}
|
onChange={e => setValue("lastName", e.target.value)}
|
||||||
onBlur={() => setTouchedField("lastName")}
|
onBlur={() => setTouchedField("lastName")}
|
||||||
placeholder="Yamada"
|
placeholder="Yamada"
|
||||||
autoComplete="family-name"
|
autoComplete="section-signup family-name"
|
||||||
|
data-field="lastName"
|
||||||
/>
|
/>
|
||||||
</FormField>
|
</FormField>
|
||||||
</div>
|
</div>
|
||||||
@ -66,7 +71,8 @@ export function AccountStep({ form }: AccountStepProps) {
|
|||||||
onChange={e => setValue("email", e.target.value)}
|
onChange={e => setValue("email", e.target.value)}
|
||||||
onBlur={() => setTouchedField("email")}
|
onBlur={() => setTouchedField("email")}
|
||||||
placeholder="taro.yamada@example.com"
|
placeholder="taro.yamada@example.com"
|
||||||
autoComplete="email"
|
autoComplete="section-signup email"
|
||||||
|
data-field="email"
|
||||||
/>
|
/>
|
||||||
</FormField>
|
</FormField>
|
||||||
|
|
||||||
@ -90,8 +96,9 @@ export function AccountStep({ form }: AccountStepProps) {
|
|||||||
}}
|
}}
|
||||||
onBlur={() => setTouchedField("phoneCountryCode")}
|
onBlur={() => setTouchedField("phoneCountryCode")}
|
||||||
placeholder="+81"
|
placeholder="+81"
|
||||||
autoComplete="tel-country-code"
|
autoComplete="section-signup tel-country-code"
|
||||||
className="w-20 text-center"
|
className="w-20 text-center"
|
||||||
|
data-field="phoneCountryCode"
|
||||||
/>
|
/>
|
||||||
<Input
|
<Input
|
||||||
name="tel-national"
|
name="tel-national"
|
||||||
@ -104,8 +111,9 @@ export function AccountStep({ form }: AccountStepProps) {
|
|||||||
}}
|
}}
|
||||||
onBlur={() => setTouchedField("phone")}
|
onBlur={() => setTouchedField("phone")}
|
||||||
placeholder="9012345678"
|
placeholder="9012345678"
|
||||||
autoComplete="tel-national"
|
autoComplete="section-signup tel-national"
|
||||||
className="flex-1"
|
className="flex-1"
|
||||||
|
data-field="phone"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</FormField>
|
</FormField>
|
||||||
@ -119,7 +127,8 @@ export function AccountStep({ form }: AccountStepProps) {
|
|||||||
value={values.dateOfBirth ?? ""}
|
value={values.dateOfBirth ?? ""}
|
||||||
onChange={e => setValue("dateOfBirth", e.target.value || undefined)}
|
onChange={e => setValue("dateOfBirth", e.target.value || undefined)}
|
||||||
onBlur={() => setTouchedField("dateOfBirth")}
|
onBlur={() => setTouchedField("dateOfBirth")}
|
||||||
autoComplete="bday"
|
autoComplete="section-signup bday"
|
||||||
|
data-field="dateOfBirth"
|
||||||
/>
|
/>
|
||||||
</FormField>
|
</FormField>
|
||||||
|
|
||||||
@ -129,6 +138,8 @@ export function AccountStep({ form }: AccountStepProps) {
|
|||||||
value={values.gender ?? ""}
|
value={values.gender ?? ""}
|
||||||
onChange={e => setValue("gender", e.target.value || undefined)}
|
onChange={e => setValue("gender", e.target.value || undefined)}
|
||||||
onBlur={() => setTouchedField("gender")}
|
onBlur={() => setTouchedField("gender")}
|
||||||
|
autoComplete="section-signup sex"
|
||||||
|
data-field="gender"
|
||||||
className={[
|
className={[
|
||||||
"flex h-10 w-full rounded-md border border-input bg-background text-foreground px-3 py-2 text-sm",
|
"flex h-10 w-full rounded-md border border-input bg-background text-foreground px-3 py-2 text-sm",
|
||||||
"ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none",
|
"ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none",
|
||||||
@ -141,9 +152,11 @@ export function AccountStep({ form }: AccountStepProps) {
|
|||||||
aria-invalid={Boolean(getError("gender")) || undefined}
|
aria-invalid={Boolean(getError("gender")) || undefined}
|
||||||
>
|
>
|
||||||
<option value="">Select…</option>
|
<option value="">Select…</option>
|
||||||
<option value="male">Male</option>
|
{genderOptions.map(option => (
|
||||||
<option value="female">Female</option>
|
<option key={option} value={option}>
|
||||||
<option value="other">Other</option>
|
{formatGender(option)}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
</select>
|
</select>
|
||||||
</FormField>
|
</FormField>
|
||||||
</div>
|
</div>
|
||||||
@ -156,7 +169,8 @@ export function AccountStep({ form }: AccountStepProps) {
|
|||||||
onChange={e => setValue("company", e.target.value)}
|
onChange={e => setValue("company", e.target.value)}
|
||||||
onBlur={() => setTouchedField("company")}
|
onBlur={() => setTouchedField("company")}
|
||||||
placeholder="Company name"
|
placeholder="Company name"
|
||||||
autoComplete="organization"
|
autoComplete="section-signup organization"
|
||||||
|
data-field="company"
|
||||||
/>
|
/>
|
||||||
</FormField>
|
</FormField>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -5,21 +5,22 @@
|
|||||||
* - postcode → postcode
|
* - postcode → postcode
|
||||||
* - state → state (prefecture)
|
* - state → state (prefecture)
|
||||||
* - city → city
|
* - city → city
|
||||||
* - address1 → address1 (street/block address)
|
* - address1 → address1 (building/room)
|
||||||
* - address2 → address2 (building/room - optional)
|
* - address2 → address2 (street/block)
|
||||||
* - country → "JP"
|
* - country → "JP"
|
||||||
*/
|
*/
|
||||||
|
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useCallback } from "react";
|
import { useCallback, useEffect } from "react";
|
||||||
|
import { ChevronDown } from "lucide-react";
|
||||||
import { Input } from "@/components/atoms";
|
import { Input } from "@/components/atoms";
|
||||||
import { FormField } from "@/components/molecules/FormField/FormField";
|
import { FormField } from "@/components/molecules/FormField/FormField";
|
||||||
import { JAPAN_PREFECTURES, formatJapanesePostalCode } from "@/shared/constants";
|
import { JAPAN_PREFECTURES, formatJapanesePostalCode } from "@/shared/constants";
|
||||||
|
|
||||||
interface AddressData {
|
interface AddressData {
|
||||||
address1: string;
|
address1: string;
|
||||||
address2?: string;
|
address2: string;
|
||||||
city: string;
|
city: string;
|
||||||
state: string;
|
state: string;
|
||||||
postcode: string;
|
postcode: string;
|
||||||
@ -64,9 +65,11 @@ export function AddressStep({ form }: AddressStepProps) {
|
|||||||
const markTouched = () => setTouchedField("address");
|
const markTouched = () => setTouchedField("address");
|
||||||
|
|
||||||
// Set Japan as default country on mount if empty
|
// Set Japan as default country on mount if empty
|
||||||
if (!address.country) {
|
useEffect(() => {
|
||||||
setValue("address", { ...address, country: "JP", countryCode: "JP" });
|
if (!address.country) {
|
||||||
}
|
setValue("address", { ...address, country: "JP", countryCode: "JP" });
|
||||||
|
}
|
||||||
|
}, [address, setValue]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-5">
|
<div className="space-y-5">
|
||||||
@ -79,33 +82,42 @@ export function AddressStep({ form }: AddressStepProps) {
|
|||||||
>
|
>
|
||||||
<Input
|
<Input
|
||||||
name="postal-code"
|
name="postal-code"
|
||||||
|
type="text"
|
||||||
|
inputMode="numeric"
|
||||||
value={address.postcode}
|
value={address.postcode}
|
||||||
onChange={handlePostcodeChange}
|
onChange={handlePostcodeChange}
|
||||||
onBlur={markTouched}
|
onBlur={markTouched}
|
||||||
placeholder="100-0001"
|
placeholder="100-0001"
|
||||||
autoComplete="postal-code"
|
autoComplete="section-signup postal-code"
|
||||||
maxLength={8}
|
maxLength={8}
|
||||||
autoFocus
|
autoFocus
|
||||||
|
data-field="address.postcode"
|
||||||
/>
|
/>
|
||||||
</FormField>
|
</FormField>
|
||||||
|
|
||||||
{/* Prefecture Selection */}
|
{/* Prefecture Selection */}
|
||||||
<FormField label="Prefecture" error={getError("state")} required>
|
<FormField label="Prefecture" error={getError("state")} required>
|
||||||
<select
|
<div className="relative">
|
||||||
name="address-level1"
|
<select
|
||||||
value={address.state}
|
name="address-level1"
|
||||||
onChange={e => updateAddress("state", e.target.value)}
|
value={address.state}
|
||||||
onBlur={markTouched}
|
onChange={e => updateAddress("state", e.target.value)}
|
||||||
className="block w-full h-10 px-3 py-2 border border-input rounded-md shadow-sm bg-background text-foreground text-sm focus:outline-none focus:ring-2 focus:ring-ring focus:border-ring transition-colors"
|
onBlur={markTouched}
|
||||||
autoComplete="address-level1"
|
className="block w-full h-11 pl-4 pr-10 py-2.5 border border-border rounded-lg appearance-none bg-card text-foreground text-sm shadow-sm focus:outline-none focus:ring-2 focus:ring-ring focus:border-primary transition-colors cursor-pointer"
|
||||||
>
|
autoComplete="section-signup address-level1"
|
||||||
<option value="">Select prefecture</option>
|
data-field="address.state"
|
||||||
{JAPAN_PREFECTURES.map(p => (
|
>
|
||||||
<option key={p.value} value={p.value}>
|
<option value="">Select prefecture</option>
|
||||||
{p.label}
|
{JAPAN_PREFECTURES.map(p => (
|
||||||
</option>
|
<option key={p.value} value={p.value}>
|
||||||
))}
|
{p.label}
|
||||||
</select>
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
<div className="absolute inset-y-0 right-0 flex items-center pr-3 pointer-events-none">
|
||||||
|
<ChevronDown className="h-4 w-4 text-muted-foreground" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</FormField>
|
</FormField>
|
||||||
|
|
||||||
{/* City/Ward */}
|
{/* City/Ward */}
|
||||||
@ -121,46 +133,50 @@ export function AddressStep({ form }: AddressStepProps) {
|
|||||||
onChange={e => updateAddress("city", e.target.value)}
|
onChange={e => updateAddress("city", e.target.value)}
|
||||||
onBlur={markTouched}
|
onBlur={markTouched}
|
||||||
placeholder="Shibuya-ku"
|
placeholder="Shibuya-ku"
|
||||||
autoComplete="address-level2"
|
autoComplete="section-signup address-level2"
|
||||||
|
data-field="address.city"
|
||||||
/>
|
/>
|
||||||
</FormField>
|
</FormField>
|
||||||
|
|
||||||
{/* Street Address */}
|
{/* Street / Block (Address 2) */}
|
||||||
<FormField
|
<FormField
|
||||||
label="Street Address"
|
label="Street / Block (Address 2)"
|
||||||
error={getError("address1")}
|
error={getError("address2")}
|
||||||
required
|
required
|
||||||
helperText="Block and building number (e.g., 3-8-2 Higashi Azabu)"
|
helperText="e.g., 2-20-9 Wakabayashi"
|
||||||
>
|
>
|
||||||
<Input
|
<Input
|
||||||
name="address-line1"
|
name="address-line1"
|
||||||
value={address.address1}
|
type="text"
|
||||||
onChange={e => updateAddress("address1", e.target.value)}
|
value={address.address2}
|
||||||
|
onChange={e => updateAddress("address2", e.target.value)}
|
||||||
onBlur={markTouched}
|
onBlur={markTouched}
|
||||||
placeholder="3-8-2 Higashi Azabu"
|
placeholder="2-20-9 Wakabayashi"
|
||||||
autoComplete="address-line1"
|
autoComplete="section-signup address-line1"
|
||||||
|
required
|
||||||
|
data-field="address.address2"
|
||||||
/>
|
/>
|
||||||
</FormField>
|
</FormField>
|
||||||
|
|
||||||
{/* Building & Room (Optional - for apartments) */}
|
{/* Building / Room (Address 1) */}
|
||||||
<FormField
|
<FormField
|
||||||
label="Building Name & Room Number"
|
label="Building / Room (Address 1)"
|
||||||
error={getError("address2")}
|
error={getError("address1")}
|
||||||
helperText="e.g., 3F Azabu Maruka Bldg"
|
required
|
||||||
|
helperText="e.g., Gramercy 201"
|
||||||
>
|
>
|
||||||
<Input
|
<Input
|
||||||
name="address-line2"
|
name="address-line2"
|
||||||
value={address.address2 ?? ""}
|
type="text"
|
||||||
onChange={e => updateAddress("address2", e.target.value)}
|
value={address.address1}
|
||||||
|
onChange={e => updateAddress("address1", e.target.value)}
|
||||||
onBlur={markTouched}
|
onBlur={markTouched}
|
||||||
placeholder="3F Azabu Maruka Bldg"
|
placeholder="Gramercy 201"
|
||||||
autoComplete="address-line2"
|
autoComplete="section-signup address-line2"
|
||||||
|
required
|
||||||
|
data-field="address.address1"
|
||||||
/>
|
/>
|
||||||
</FormField>
|
</FormField>
|
||||||
|
|
||||||
<p className="text-sm text-muted-foreground bg-info-soft border border-info/25 rounded-lg p-3">
|
|
||||||
Please input your address in Japan. This will be used for service delivery and setup.
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -38,7 +38,7 @@ export function PasswordStep({ form }: PasswordStepProps) {
|
|||||||
type="email"
|
type="email"
|
||||||
name="email"
|
name="email"
|
||||||
value={values.email}
|
value={values.email}
|
||||||
autoComplete="username email"
|
autoComplete="section-signup username"
|
||||||
readOnly
|
readOnly
|
||||||
className="sr-only"
|
className="sr-only"
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
@ -53,7 +53,8 @@ export function PasswordStep({ form }: PasswordStepProps) {
|
|||||||
onChange={e => setValue("password", e.target.value)}
|
onChange={e => setValue("password", e.target.value)}
|
||||||
onBlur={() => setTouchedField("password")}
|
onBlur={() => setTouchedField("password")}
|
||||||
placeholder="Create a secure password"
|
placeholder="Create a secure password"
|
||||||
autoComplete="new-password"
|
autoComplete="section-signup new-password"
|
||||||
|
data-field="password"
|
||||||
/>
|
/>
|
||||||
</FormField>
|
</FormField>
|
||||||
|
|
||||||
@ -97,7 +98,8 @@ export function PasswordStep({ form }: PasswordStepProps) {
|
|||||||
onChange={e => setValue("confirmPassword", e.target.value)}
|
onChange={e => setValue("confirmPassword", e.target.value)}
|
||||||
onBlur={() => setTouchedField("confirmPassword")}
|
onBlur={() => setTouchedField("confirmPassword")}
|
||||||
placeholder="Re-enter your password"
|
placeholder="Re-enter your password"
|
||||||
autoComplete="new-password"
|
autoComplete="section-signup new-password"
|
||||||
|
data-field="confirmPassword"
|
||||||
/>
|
/>
|
||||||
</FormField>
|
</FormField>
|
||||||
|
|
||||||
|
|||||||
@ -73,7 +73,7 @@ interface ReviewStepProps {
|
|||||||
gender?: "male" | "female" | "other";
|
gender?: "male" | "female" | "other";
|
||||||
address: {
|
address: {
|
||||||
address1: string;
|
address1: string;
|
||||||
address2?: string;
|
address2: string;
|
||||||
city: string;
|
city: string;
|
||||||
state: string;
|
state: string;
|
||||||
postcode: string;
|
postcode: string;
|
||||||
@ -95,8 +95,8 @@ export function ReviewStep({ form }: ReviewStepProps) {
|
|||||||
|
|
||||||
// Format address for display
|
// Format address for display
|
||||||
const formattedAddress = [
|
const formattedAddress = [
|
||||||
address.address1,
|
|
||||||
address.address2,
|
address.address2,
|
||||||
|
address.address1,
|
||||||
address.city,
|
address.city,
|
||||||
address.state,
|
address.state,
|
||||||
address.postcode,
|
address.postcode,
|
||||||
|
|||||||
@ -305,24 +305,7 @@ export function AddressConfirmation({
|
|||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-muted-foreground mb-1">
|
<label className="block text-sm font-medium text-muted-foreground mb-1">
|
||||||
Street Address *
|
Street / Block (Address 2)
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={editedAddress?.address1 || ""}
|
|
||||||
onChange={e => {
|
|
||||||
setError(null); // Clear error on input
|
|
||||||
setEditedAddress(prev => (prev ? { ...prev, address1: e.target.value } : null));
|
|
||||||
}}
|
|
||||||
className="w-full px-3 py-2 border border-input rounded-md bg-background text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:border-ring transition-colors"
|
|
||||||
placeholder="123 Main Street"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-muted-foreground mb-1">
|
|
||||||
Street Address Line 2
|
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
@ -332,7 +315,24 @@ export function AddressConfirmation({
|
|||||||
setEditedAddress(prev => (prev ? { ...prev, address2: e.target.value } : null));
|
setEditedAddress(prev => (prev ? { ...prev, address2: e.target.value } : null));
|
||||||
}}
|
}}
|
||||||
className="w-full px-3 py-2 border border-input rounded-md bg-background text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:border-ring transition-colors"
|
className="w-full px-3 py-2 border border-input rounded-md bg-background text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:border-ring transition-colors"
|
||||||
placeholder="Apartment, suite, etc. (optional)"
|
placeholder="2-20-9 Wakabayashi"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-muted-foreground mb-1">
|
||||||
|
Building / Room (Address 1) *
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={editedAddress?.address1 || ""}
|
||||||
|
onChange={e => {
|
||||||
|
setError(null); // Clear error on input
|
||||||
|
setEditedAddress(prev => (prev ? { ...prev, address1: e.target.value } : null));
|
||||||
|
}}
|
||||||
|
className="w-full px-3 py-2 border border-input rounded-md bg-background text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:border-ring transition-colors"
|
||||||
|
placeholder="Gramercy 201"
|
||||||
|
required
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -427,9 +427,11 @@ export function AddressConfirmation({
|
|||||||
{address?.address1 ? (
|
{address?.address1 ? (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="text-foreground space-y-1">
|
<div className="text-foreground space-y-1">
|
||||||
<p className="font-semibold text-base">{address.address1}</p>
|
{(address.address2 || address.address1) && (
|
||||||
{address.address2 ? (
|
<p className="font-semibold text-base">{address.address2 || address.address1}</p>
|
||||||
<p className="text-muted-foreground">{address.address2}</p>
|
)}
|
||||||
|
{address.address2 && address.address1 ? (
|
||||||
|
<p className="text-muted-foreground">{address.address1}</p>
|
||||||
) : null}
|
) : null}
|
||||||
<p className="text-muted-foreground">
|
<p className="text-muted-foreground">
|
||||||
{[address.city, address.state].filter(Boolean).join(", ")}
|
{[address.city, address.state].filter(Boolean).join(", ")}
|
||||||
|
|||||||
@ -45,8 +45,8 @@ const normalizeCountryValue = (value?: string | null) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const DEFAULT_LABELS: Partial<Record<keyof Address, string>> = {
|
const DEFAULT_LABELS: Partial<Record<keyof Address, string>> = {
|
||||||
address1: "Address Line 1",
|
address1: "Building / Room (Address 1)",
|
||||||
address2: "Address Line 2",
|
address2: "Street / Block (Address 2)",
|
||||||
city: "City",
|
city: "City",
|
||||||
state: "State/Prefecture",
|
state: "State/Prefecture",
|
||||||
postcode: "Postcode",
|
postcode: "Postcode",
|
||||||
@ -57,8 +57,8 @@ const DEFAULT_LABELS: Partial<Record<keyof Address, string>> = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const DEFAULT_PLACEHOLDERS: Partial<Record<keyof Address, string>> = {
|
const DEFAULT_PLACEHOLDERS: Partial<Record<keyof Address, string>> = {
|
||||||
address1: "123 Main Street",
|
address1: "Gramercy 201",
|
||||||
address2: "Apartment, suite, etc. (optional)",
|
address2: "2-20-9 Wakabayashi",
|
||||||
city: "Tokyo",
|
city: "Tokyo",
|
||||||
state: "Tokyo",
|
state: "Tokyo",
|
||||||
postcode: "100-0001",
|
postcode: "100-0001",
|
||||||
@ -308,12 +308,12 @@ export function AddressForm({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{/* Street Address */}
|
{/* Street / Block (Address 2) */}
|
||||||
{renderField("address1")}
|
|
||||||
|
|
||||||
{/* Street Address Line 2 */}
|
|
||||||
{renderField("address2")}
|
{renderField("address2")}
|
||||||
|
|
||||||
|
{/* Building / Room (Address 1) */}
|
||||||
|
{renderField("address1")}
|
||||||
|
|
||||||
{/* City, State, Postal Code Row */}
|
{/* City, State, Postal Code Row */}
|
||||||
<div
|
<div
|
||||||
className={`grid gap-4 ${variant === "compact" ? "grid-cols-1 sm:grid-cols-3" : "grid-cols-1 md:grid-cols-3"}`}
|
className={`grid gap-4 ${variant === "compact" ? "grid-cols-1 sm:grid-cols-3" : "grid-cols-1 md:grid-cols-3"}`}
|
||||||
|
|||||||
105
pnpm-lock.yaml
generated
105
pnpm-lock.yaml
generated
@ -192,9 +192,6 @@ importers:
|
|||||||
"@heroicons/react":
|
"@heroicons/react":
|
||||||
specifier: ^2.2.0
|
specifier: ^2.2.0
|
||||||
version: 2.2.0(react@19.2.3)
|
version: 2.2.0(react@19.2.3)
|
||||||
"@react-google-maps/api":
|
|
||||||
specifier: ^2.20.8
|
|
||||||
version: 2.20.8(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
|
||||||
"@tanstack/react-query":
|
"@tanstack/react-query":
|
||||||
specifier: ^5.90.14
|
specifier: ^5.90.14
|
||||||
version: 5.90.14(react@19.2.3)
|
version: 5.90.14(react@19.2.3)
|
||||||
@ -849,18 +846,6 @@ packages:
|
|||||||
}
|
}
|
||||||
engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 }
|
engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 }
|
||||||
|
|
||||||
"@googlemaps/js-api-loader@1.16.8":
|
|
||||||
resolution:
|
|
||||||
{
|
|
||||||
integrity: sha512-CROqqwfKotdO6EBjZO/gQGVTbeDps5V7Mt9+8+5Q+jTg5CRMi3Ii/L9PmV3USROrt2uWxtGzJHORmByxyo9pSQ==,
|
|
||||||
}
|
|
||||||
|
|
||||||
"@googlemaps/markerclusterer@2.5.3":
|
|
||||||
resolution:
|
|
||||||
{
|
|
||||||
integrity: sha512-x7lX0R5yYOoiNectr10wLgCBasNcXFHiADIBdmn7jQllF2B5ENQw5XtZK+hIw4xnV0Df0xhN4LN98XqA5jaiOw==,
|
|
||||||
}
|
|
||||||
|
|
||||||
"@grpc/grpc-js@1.14.2":
|
"@grpc/grpc-js@1.14.2":
|
||||||
resolution:
|
resolution:
|
||||||
{
|
{
|
||||||
@ -2078,27 +2063,6 @@ packages:
|
|||||||
integrity: sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==,
|
integrity: sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==,
|
||||||
}
|
}
|
||||||
|
|
||||||
"@react-google-maps/api@2.20.8":
|
|
||||||
resolution:
|
|
||||||
{
|
|
||||||
integrity: sha512-wtLYFtCGXK3qbIz1H5to3JxbosPnKsvjDKhqGylXUb859EskhzR7OpuNt0LqdLarXUtZCJTKzPn3BNaekNIahg==,
|
|
||||||
}
|
|
||||||
peerDependencies:
|
|
||||||
react: ^16.8 || ^17 || ^18 || ^19
|
|
||||||
react-dom: ^16.8 || ^17 || ^18 || ^19
|
|
||||||
|
|
||||||
"@react-google-maps/infobox@2.20.0":
|
|
||||||
resolution:
|
|
||||||
{
|
|
||||||
integrity: sha512-03PJHjohhaVLkX6+NHhlr8CIlvUxWaXhryqDjyaZ8iIqqix/nV8GFdz9O3m5OsjtxtNho09F/15j14yV0nuyLQ==,
|
|
||||||
}
|
|
||||||
|
|
||||||
"@react-google-maps/marker-clusterer@2.20.0":
|
|
||||||
resolution:
|
|
||||||
{
|
|
||||||
integrity: sha512-tieX9Va5w1yP88vMgfH1pHTacDQ9TgDTjox3tLlisKDXRQWdjw+QeVVghhf5XqqIxXHgPdcGwBvKY6UP+SIvLw==,
|
|
||||||
}
|
|
||||||
|
|
||||||
"@scarf/scarf@1.4.0":
|
"@scarf/scarf@1.4.0":
|
||||||
resolution:
|
resolution:
|
||||||
{
|
{
|
||||||
@ -2519,12 +2483,6 @@ packages:
|
|||||||
integrity: sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==,
|
integrity: sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==,
|
||||||
}
|
}
|
||||||
|
|
||||||
"@types/google.maps@3.58.1":
|
|
||||||
resolution:
|
|
||||||
{
|
|
||||||
integrity: sha512-X9QTSvGJ0nCfMzYOnaVs/k6/4L+7F5uCS+4iUmkLEls6J9S/Phv+m/i3mDeyc49ZBgwab3EFO1HEoBY7k98EGQ==,
|
|
||||||
}
|
|
||||||
|
|
||||||
"@types/http-cache-semantics@4.0.4":
|
"@types/http-cache-semantics@4.0.4":
|
||||||
resolution:
|
resolution:
|
||||||
{
|
{
|
||||||
@ -4803,12 +4761,6 @@ packages:
|
|||||||
integrity: sha512-MAQUJuIo7Xqk8EVNP+6d3CKq9c80hi4tjIbIAT6lmGW9W6WzlHiu9PS8uSuUYU+Do+j1baiFp3H25XEVxDIG2g==,
|
integrity: sha512-MAQUJuIo7Xqk8EVNP+6d3CKq9c80hi4tjIbIAT6lmGW9W6WzlHiu9PS8uSuUYU+Do+j1baiFp3H25XEVxDIG2g==,
|
||||||
}
|
}
|
||||||
|
|
||||||
invariant@2.2.4:
|
|
||||||
resolution:
|
|
||||||
{
|
|
||||||
integrity: sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==,
|
|
||||||
}
|
|
||||||
|
|
||||||
ioredis@5.8.2:
|
ioredis@5.8.2:
|
||||||
resolution:
|
resolution:
|
||||||
{
|
{
|
||||||
@ -5052,12 +5004,6 @@ packages:
|
|||||||
integrity: sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==,
|
integrity: sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==,
|
||||||
}
|
}
|
||||||
|
|
||||||
kdbush@4.0.2:
|
|
||||||
resolution:
|
|
||||||
{
|
|
||||||
integrity: sha512-WbCVYJ27Sz8zi9Q7Q0xHC+05iwkm3Znipc2XTlrnJbsHMYktW4hPhXUE8Ys1engBrvffoSCqbil1JQAa7clRpA==,
|
|
||||||
}
|
|
||||||
|
|
||||||
keyv@4.5.4:
|
keyv@4.5.4:
|
||||||
resolution:
|
resolution:
|
||||||
{
|
{
|
||||||
@ -5289,13 +5235,6 @@ packages:
|
|||||||
integrity: sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==,
|
integrity: sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==,
|
||||||
}
|
}
|
||||||
|
|
||||||
loose-envify@1.4.0:
|
|
||||||
resolution:
|
|
||||||
{
|
|
||||||
integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==,
|
|
||||||
}
|
|
||||||
hasBin: true
|
|
||||||
|
|
||||||
lowercase-keys@3.0.0:
|
lowercase-keys@3.0.0:
|
||||||
resolution:
|
resolution:
|
||||||
{
|
{
|
||||||
@ -6915,12 +6854,6 @@ packages:
|
|||||||
babel-plugin-macros:
|
babel-plugin-macros:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
supercluster@8.0.1:
|
|
||||||
resolution:
|
|
||||||
{
|
|
||||||
integrity: sha512-IiOea5kJ9iqzD2t7QJq/cREyLHTtSmUT6gQsweojg9WH2sYJqZK9SswTu6jrscO6D1G5v5vYZ9ru/eq85lXeZQ==,
|
|
||||||
}
|
|
||||||
|
|
||||||
supports-color@7.2.0:
|
supports-color@7.2.0:
|
||||||
resolution:
|
resolution:
|
||||||
{
|
{
|
||||||
@ -7919,13 +7852,6 @@ snapshots:
|
|||||||
"@eslint/core": 0.17.0
|
"@eslint/core": 0.17.0
|
||||||
levn: 0.4.1
|
levn: 0.4.1
|
||||||
|
|
||||||
"@googlemaps/js-api-loader@1.16.8": {}
|
|
||||||
|
|
||||||
"@googlemaps/markerclusterer@2.5.3":
|
|
||||||
dependencies:
|
|
||||||
fast-deep-equal: 3.1.3
|
|
||||||
supercluster: 8.0.1
|
|
||||||
|
|
||||||
"@grpc/grpc-js@1.14.2":
|
"@grpc/grpc-js@1.14.2":
|
||||||
dependencies:
|
dependencies:
|
||||||
"@grpc/proto-loader": 0.8.0
|
"@grpc/proto-loader": 0.8.0
|
||||||
@ -8636,21 +8562,6 @@ snapshots:
|
|||||||
|
|
||||||
"@protobufjs/utf8@1.1.0": {}
|
"@protobufjs/utf8@1.1.0": {}
|
||||||
|
|
||||||
"@react-google-maps/api@2.20.8(react-dom@19.2.3(react@19.2.3))(react@19.2.3)":
|
|
||||||
dependencies:
|
|
||||||
"@googlemaps/js-api-loader": 1.16.8
|
|
||||||
"@googlemaps/markerclusterer": 2.5.3
|
|
||||||
"@react-google-maps/infobox": 2.20.0
|
|
||||||
"@react-google-maps/marker-clusterer": 2.20.0
|
|
||||||
"@types/google.maps": 3.58.1
|
|
||||||
invariant: 2.2.4
|
|
||||||
react: 19.2.3
|
|
||||||
react-dom: 19.2.3(react@19.2.3)
|
|
||||||
|
|
||||||
"@react-google-maps/infobox@2.20.0": {}
|
|
||||||
|
|
||||||
"@react-google-maps/marker-clusterer@2.20.0": {}
|
|
||||||
|
|
||||||
"@scarf/scarf@1.4.0": {}
|
"@scarf/scarf@1.4.0": {}
|
||||||
|
|
||||||
"@sendgrid/client@8.1.6":
|
"@sendgrid/client@8.1.6":
|
||||||
@ -8903,8 +8814,6 @@ snapshots:
|
|||||||
"@types/express-serve-static-core": 5.1.0
|
"@types/express-serve-static-core": 5.1.0
|
||||||
"@types/serve-static": 2.2.0
|
"@types/serve-static": 2.2.0
|
||||||
|
|
||||||
"@types/google.maps@3.58.1": {}
|
|
||||||
|
|
||||||
"@types/http-cache-semantics@4.0.4":
|
"@types/http-cache-semantics@4.0.4":
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
@ -10412,10 +10321,6 @@ snapshots:
|
|||||||
kind-of: 6.0.3
|
kind-of: 6.0.3
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
invariant@2.2.4:
|
|
||||||
dependencies:
|
|
||||||
loose-envify: 1.4.0
|
|
||||||
|
|
||||||
ioredis@5.8.2:
|
ioredis@5.8.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
"@ioredis/commands": 1.4.0
|
"@ioredis/commands": 1.4.0
|
||||||
@ -10541,8 +10446,6 @@ snapshots:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
graceful-fs: 4.2.11
|
graceful-fs: 4.2.11
|
||||||
|
|
||||||
kdbush@4.0.2: {}
|
|
||||||
|
|
||||||
keyv@4.5.4:
|
keyv@4.5.4:
|
||||||
dependencies:
|
dependencies:
|
||||||
json-buffer: 3.0.1
|
json-buffer: 3.0.1
|
||||||
@ -10663,10 +10566,6 @@ snapshots:
|
|||||||
|
|
||||||
long@5.3.2: {}
|
long@5.3.2: {}
|
||||||
|
|
||||||
loose-envify@1.4.0:
|
|
||||||
dependencies:
|
|
||||||
js-tokens: 4.0.0
|
|
||||||
|
|
||||||
lowercase-keys@3.0.0:
|
lowercase-keys@3.0.0:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
@ -11619,10 +11518,6 @@ snapshots:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
"@babel/core": 7.28.5
|
"@babel/core": 7.28.5
|
||||||
|
|
||||||
supercluster@8.0.1:
|
|
||||||
dependencies:
|
|
||||||
kdbush: 4.0.2
|
|
||||||
|
|
||||||
supports-color@7.2.0:
|
supports-color@7.2.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
has-flag: 4.0.0
|
has-flag: 4.0.0
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user