Refactor loading components to consolidate loading states and remove deprecated loading spinner. Update RouteLoading to use skeleton mode and replace LoadingSpinner with inline skeletons in buttons and other components for a more consistent loading experience. Adjust forms to simplify error handling and validation logic.
This commit is contained in:
parent
f662c3eb45
commit
afa201c4e5
@ -1,23 +1,19 @@
|
||||
import type { ReactNode } from "react";
|
||||
import { PageLayout } from "@/components/layout/PageLayout";
|
||||
import { PageLoadingState } from "@/components/ui";
|
||||
|
||||
interface RouteLoadingProps {
|
||||
icon?: ReactNode;
|
||||
title: string;
|
||||
description?: string;
|
||||
mode?: "spinner" | "content";
|
||||
mode?: "skeleton" | "content";
|
||||
children?: ReactNode;
|
||||
}
|
||||
|
||||
// Shared route-level loading wrapper used by segment loading.tsx files
|
||||
export function RouteLoading({ icon, title, description, mode = "spinner", children }: RouteLoadingProps) {
|
||||
if (mode === "spinner") {
|
||||
return (
|
||||
<PageLayout icon={icon} title={title} description={description} loading>
|
||||
{/* When loading=true, PageLayout renders its own loading UI */}
|
||||
<></>
|
||||
</PageLayout>
|
||||
);
|
||||
export function RouteLoading({ icon, title, description, mode = "skeleton", children }: RouteLoadingProps) {
|
||||
if (mode === "skeleton") {
|
||||
return <PageLoadingState title={title} />;
|
||||
}
|
||||
return (
|
||||
<PageLayout icon={icon} title={title} description={description}>
|
||||
|
||||
@ -2,7 +2,7 @@ import type { AnchorHTMLAttributes, ButtonHTMLAttributes, ReactNode } from "reac
|
||||
import { forwardRef } from "react";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
import { cn } from "@/shared/utils";
|
||||
import { LoadingSpinner } from "@/components/ui/loading-spinner";
|
||||
// Loading spinner removed - using inline spinner for buttons
|
||||
|
||||
const buttonVariants = cva(
|
||||
"inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:opacity-50 disabled:pointer-events-none ring-offset-background",
|
||||
@ -76,7 +76,9 @@ const Button = forwardRef<HTMLButtonElement | HTMLAnchorElement, ButtonProps>((p
|
||||
{...anchorProps}
|
||||
>
|
||||
<span className="inline-flex items-center gap-2">
|
||||
{loading ? <LoadingSpinner size="sm" /> : leftIcon}
|
||||
{loading ? (
|
||||
<div className="animate-spin rounded-full h-4 w-4 border border-current border-t-transparent" />
|
||||
) : leftIcon}
|
||||
<span>{loading ? loadingText ?? children : children}</span>
|
||||
{!loading && rightIcon ? <span className="ml-1">{rightIcon}</span> : null}
|
||||
</span>
|
||||
@ -94,7 +96,9 @@ const Button = forwardRef<HTMLButtonElement | HTMLAnchorElement, ButtonProps>((p
|
||||
{...buttonProps}
|
||||
>
|
||||
<span className="inline-flex items-center gap-2">
|
||||
{loading ? <LoadingSpinner size="sm" /> : leftIcon}
|
||||
{loading ? (
|
||||
<div className="animate-spin rounded-full h-4 w-4 border border-current border-t-transparent" />
|
||||
) : leftIcon}
|
||||
<span>{loading ? loadingText ?? children : children}</span>
|
||||
{!loading && rightIcon ? <span className="ml-1">{rightIcon}</span> : null}
|
||||
</span>
|
||||
|
||||
@ -23,8 +23,7 @@ export type { StatusPillProps } from "./status-pill";
|
||||
export { Badge, badgeVariants } from "./badge";
|
||||
export type { BadgeProps } from "./badge";
|
||||
|
||||
export { LoadingSpinner, CenteredLoadingSpinner, spinnerVariants } from "./loading-spinner";
|
||||
export type { LoadingSpinnerProps } from "./loading-spinner";
|
||||
// Loading components consolidated into skeleton loading
|
||||
|
||||
export {
|
||||
ErrorState,
|
||||
|
||||
@ -92,31 +92,6 @@ export function LoadingStats({ count = 4 }: { count?: number }) {
|
||||
);
|
||||
}
|
||||
|
||||
export function LoadingSpinner({
|
||||
size = "md",
|
||||
className,
|
||||
}: {
|
||||
size?: "sm" | "md" | "lg";
|
||||
className?: string;
|
||||
}) {
|
||||
const sizeClasses = {
|
||||
sm: "h-4 w-4",
|
||||
md: "h-8 w-8",
|
||||
lg: "h-12 w-12",
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={cn("flex items-center justify-center", className)}>
|
||||
<div
|
||||
className={cn(
|
||||
"animate-spin rounded-full border-2 border-gray-300 border-t-primary",
|
||||
sizeClasses[size]
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function PageLoadingState({ title }: { title: string }) {
|
||||
return (
|
||||
<div className="py-8">
|
||||
|
||||
@ -1,68 +0,0 @@
|
||||
import { forwardRef } from "react";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
import { cn } from "@/shared/utils";
|
||||
|
||||
const spinnerVariants = cva(
|
||||
"animate-spin rounded-full border-solid border-current border-r-transparent",
|
||||
{
|
||||
variants: {
|
||||
size: {
|
||||
xs: "h-3 w-3 border",
|
||||
sm: "h-4 w-4 border",
|
||||
default: "h-6 w-6 border-2",
|
||||
lg: "h-8 w-8 border-2",
|
||||
xl: "h-12 w-12 border-4",
|
||||
},
|
||||
variant: {
|
||||
default: "text-blue-600",
|
||||
white: "text-white",
|
||||
gray: "text-gray-400",
|
||||
current: "text-current",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
size: "default",
|
||||
variant: "default",
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
interface LoadingSpinnerProps
|
||||
extends React.HTMLAttributes<HTMLDivElement>,
|
||||
VariantProps<typeof spinnerVariants> {
|
||||
label?: string;
|
||||
}
|
||||
|
||||
const LoadingSpinner = forwardRef<HTMLDivElement, LoadingSpinnerProps>(
|
||||
({ className, size, variant, label = "Loading...", ...props }, ref) => {
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn("inline-block", className)}
|
||||
role="status"
|
||||
aria-label={label}
|
||||
{...props}
|
||||
>
|
||||
<div className={cn(spinnerVariants({ size, variant }))} />
|
||||
<span className="sr-only">{label}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
LoadingSpinner.displayName = "LoadingSpinner";
|
||||
|
||||
// Centered loading spinner for full-page or container loading
|
||||
const CenteredLoadingSpinner = forwardRef<
|
||||
HTMLDivElement,
|
||||
LoadingSpinnerProps & { containerClassName?: string }
|
||||
>(({ containerClassName, ...props }, ref) => {
|
||||
return (
|
||||
<div className={cn("flex items-center justify-center p-8", containerClassName)}>
|
||||
<LoadingSpinner ref={ref} {...props} />
|
||||
</div>
|
||||
);
|
||||
});
|
||||
CenteredLoadingSpinner.displayName = "CenteredLoadingSpinner";
|
||||
|
||||
export { LoadingSpinner, CenteredLoadingSpinner, spinnerVariants };
|
||||
export type { LoadingSpinnerProps };
|
||||
@ -12,10 +12,10 @@ import { FormField } from "@/components/common/FormField";
|
||||
import { usePasswordReset } from "../../hooks/use-auth";
|
||||
import { useZodForm } from "@/core/forms";
|
||||
import {
|
||||
passwordResetRequestSchema,
|
||||
passwordResetSchema,
|
||||
type PasswordResetRequestData,
|
||||
type PasswordResetData
|
||||
passwordResetRequestFormSchema,
|
||||
passwordResetFormSchema,
|
||||
type PasswordResetRequestFormData,
|
||||
type PasswordResetFormData
|
||||
} from "@customer-portal/domain";
|
||||
|
||||
interface PasswordResetFormProps {
|
||||
@ -39,11 +39,11 @@ export function PasswordResetForm({
|
||||
|
||||
// Zod form for password reset request
|
||||
const requestForm = useZodForm({
|
||||
schema: passwordResetRequestSchema,
|
||||
schema: passwordResetRequestFormSchema,
|
||||
initialValues: { email: "" },
|
||||
onSubmit: async (data) => {
|
||||
try {
|
||||
await requestPasswordReset(data.email);
|
||||
await requestPasswordReset(data);
|
||||
onSuccess?.();
|
||||
} catch (err) {
|
||||
const errorMessage = err instanceof Error ? err.message : "Request failed";
|
||||
@ -54,16 +54,11 @@ export function PasswordResetForm({
|
||||
|
||||
// Zod form for password reset (with confirm password)
|
||||
const resetForm = useZodForm({
|
||||
schema: passwordResetSchema.extend({
|
||||
confirmPassword: passwordResetSchema.shape.password,
|
||||
}).refine((data) => data.password === data.confirmPassword, {
|
||||
message: "Passwords do not match",
|
||||
path: ["confirmPassword"],
|
||||
}),
|
||||
schema: passwordResetFormSchema,
|
||||
initialValues: { token: token || "", password: "", confirmPassword: "" },
|
||||
onSubmit: async (data) => {
|
||||
try {
|
||||
await resetPassword(data.token, data.password);
|
||||
await resetPassword(data);
|
||||
onSuccess?.();
|
||||
} catch (err) {
|
||||
const errorMessage = err instanceof Error ? err.message : "Reset failed";
|
||||
@ -110,22 +105,22 @@ export function PasswordResetForm({
|
||||
placeholder="Enter your email"
|
||||
value={requestForm.values.email}
|
||||
onChange={(e) => requestForm.setValue("email", e.target.value)}
|
||||
onBlur={() => requestForm.setTouched("email", true)}
|
||||
onBlur={() => requestForm.validate()}
|
||||
disabled={loading || requestForm.isSubmitting}
|
||||
className={requestForm.errors.email ? "border-red-300" : ""}
|
||||
/>
|
||||
</FormField>
|
||||
|
||||
{(error || requestForm.errors._form) && (
|
||||
{error && (
|
||||
<ErrorMessage>
|
||||
{requestForm.errors._form || error}
|
||||
{error}
|
||||
</ErrorMessage>
|
||||
)}
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
className="w-full"
|
||||
disabled={loading || requestForm.isSubmitting || !requestForm.isValid}
|
||||
disabled={loading || requestForm.isSubmitting}
|
||||
loading={loading || requestForm.isSubmitting}
|
||||
>
|
||||
Send reset link
|
||||
@ -167,7 +162,7 @@ export function PasswordResetForm({
|
||||
placeholder="Enter new password"
|
||||
value={resetForm.values.password}
|
||||
onChange={(e) => resetForm.setValue("password", e.target.value)}
|
||||
onBlur={() => resetForm.setTouched("password", true)}
|
||||
onBlur={() => resetForm.validate()}
|
||||
disabled={loading || resetForm.isSubmitting}
|
||||
className={resetForm.errors.password ? "border-red-300" : ""}
|
||||
/>
|
||||
@ -183,22 +178,22 @@ export function PasswordResetForm({
|
||||
placeholder="Confirm new password"
|
||||
value={resetForm.values.confirmPassword}
|
||||
onChange={(e) => resetForm.setValue("confirmPassword", e.target.value)}
|
||||
onBlur={() => resetForm.setTouched("confirmPassword", true)}
|
||||
onBlur={() => resetForm.validate()}
|
||||
disabled={loading || resetForm.isSubmitting}
|
||||
className={resetForm.errors.confirmPassword ? "border-red-300" : ""}
|
||||
/>
|
||||
</FormField>
|
||||
|
||||
{(error || resetForm.errors._form) && (
|
||||
{error && (
|
||||
<ErrorMessage>
|
||||
{resetForm.errors._form || error}
|
||||
{error}
|
||||
</ErrorMessage>
|
||||
)}
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
className="w-full"
|
||||
disabled={loading || resetForm.isSubmitting || !resetForm.isValid}
|
||||
disabled={loading || resetForm.isSubmitting}
|
||||
loading={loading || resetForm.isSubmitting}
|
||||
>
|
||||
Update password
|
||||
|
||||
@ -9,7 +9,6 @@ import { useCallback } from "react";
|
||||
import { Input } from "@/components/ui";
|
||||
import { FormField } from "@/components/common/FormField";
|
||||
import type { SignupFormData } from "@customer-portal/domain";
|
||||
import { type UseZodFormReturn } from "@/core/forms";
|
||||
|
||||
const COUNTRIES = [
|
||||
{ code: "US", name: "United States" },
|
||||
@ -37,17 +36,13 @@ const COUNTRIES = [
|
||||
interface AddressStepProps {
|
||||
values: SignupFormData["address"];
|
||||
errors: Record<string, string>;
|
||||
touched: Record<string, boolean>;
|
||||
setValue: (field: keyof SignupFormData["address"], value: string) => void;
|
||||
setTouchedField: (field: keyof SignupFormData["address"]) => void;
|
||||
}
|
||||
|
||||
export function AddressStep({
|
||||
values: address,
|
||||
errors,
|
||||
touched,
|
||||
setValue,
|
||||
setTouchedField,
|
||||
}: AddressStepProps) {
|
||||
const updateAddressField = useCallback((field: keyof SignupFormData["address"], value: string) => {
|
||||
setValue(field, value);
|
||||
@ -57,14 +52,14 @@ export function AddressStep({
|
||||
<div className="space-y-6">
|
||||
<FormField
|
||||
label="Street Address"
|
||||
error={touched.street ? errors.street : undefined}
|
||||
error={errors.street}
|
||||
required
|
||||
>
|
||||
<Input
|
||||
type="text"
|
||||
value={address.street}
|
||||
onChange={(e) => updateAddressField("street", e.target.value)}
|
||||
onBlur={() => setTouchedField("address")}
|
||||
onBlur={() => undefined}
|
||||
placeholder="Enter your street address"
|
||||
className="w-full"
|
||||
/>
|
||||
@ -72,13 +67,13 @@ export function AddressStep({
|
||||
|
||||
<FormField
|
||||
label="Address Line 2 (Optional)"
|
||||
error={touched["address.streetLine2"] ? errors["address.streetLine2"] : undefined}
|
||||
error={errors.streetLine2}
|
||||
>
|
||||
<Input
|
||||
type="text"
|
||||
value={address.streetLine2 || ""}
|
||||
onChange={(e) => updateAddressField("streetLine2", e.target.value)}
|
||||
onBlur={() => setTouchedField("address")}
|
||||
onBlur={() => undefined}
|
||||
placeholder="Apartment, suite, etc."
|
||||
className="w-full"
|
||||
/>
|
||||
@ -87,14 +82,14 @@ export function AddressStep({
|
||||
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2">
|
||||
<FormField
|
||||
label="City"
|
||||
error={touched["address.city"] ? errors["address.city"] : undefined}
|
||||
error={errors.city}
|
||||
required
|
||||
>
|
||||
<Input
|
||||
type="text"
|
||||
value={address.city}
|
||||
onChange={(e) => updateAddressField("city", e.target.value)}
|
||||
onBlur={() => setTouchedField("address")}
|
||||
onBlur={() => undefined}
|
||||
placeholder="Enter your city"
|
||||
className="w-full"
|
||||
/>
|
||||
@ -102,14 +97,14 @@ export function AddressStep({
|
||||
|
||||
<FormField
|
||||
label="State/Province"
|
||||
error={touched["address.state"] ? errors["address.state"] : undefined}
|
||||
error={errors.state}
|
||||
required
|
||||
>
|
||||
<Input
|
||||
type="text"
|
||||
value={address.state}
|
||||
onChange={(e) => updateAddressField("state", e.target.value)}
|
||||
onBlur={() => setTouchedField("address")}
|
||||
onBlur={() => undefined}
|
||||
placeholder="Enter your state/province"
|
||||
className="w-full"
|
||||
/>
|
||||
@ -119,14 +114,14 @@ export function AddressStep({
|
||||
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2">
|
||||
<FormField
|
||||
label="Postal Code"
|
||||
error={touched["address.postalCode"] ? errors["address.postalCode"] : undefined}
|
||||
error={errors.postalCode}
|
||||
required
|
||||
>
|
||||
<Input
|
||||
type="text"
|
||||
value={address.postalCode}
|
||||
onChange={(e) => updateAddressField("postalCode", e.target.value)}
|
||||
onBlur={() => setTouchedField("address")}
|
||||
onBlur={() => undefined}
|
||||
placeholder="Enter your postal code"
|
||||
className="w-full"
|
||||
/>
|
||||
@ -134,13 +129,13 @@ export function AddressStep({
|
||||
|
||||
<FormField
|
||||
label="Country"
|
||||
error={touched["address.country"] ? errors["address.country"] : undefined}
|
||||
error={errors.country}
|
||||
required
|
||||
>
|
||||
<select
|
||||
value={address.country}
|
||||
onChange={(e) => updateAddressField("country", e.target.value)}
|
||||
onBlur={() => setTouchedField("address")}
|
||||
onBlur={() => undefined}
|
||||
className="block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500"
|
||||
>
|
||||
<option value="">Select a country</option>
|
||||
|
||||
@ -8,32 +8,32 @@
|
||||
import { Input } from "@/components/ui";
|
||||
import { FormField } from "@/components/common/FormField";
|
||||
import { type SignupFormData } from "@customer-portal/domain";
|
||||
import { type UseZodFormReturn } from "@/core/forms";
|
||||
// Using simple form interface - no complex form types needed
|
||||
|
||||
interface PersonalStepProps extends Pick<UseZodFormReturn<SignupFormData>,
|
||||
'values' | 'errors' | 'touched' | 'setValue' | 'setTouchedField'> {
|
||||
interface PersonalStepProps {
|
||||
values: SignupFormData;
|
||||
errors: Partial<Record<keyof SignupFormData, string>>;
|
||||
setValue: <K extends keyof SignupFormData>(field: K, value: SignupFormData[K]) => void;
|
||||
}
|
||||
|
||||
export function PersonalStep({
|
||||
values,
|
||||
errors,
|
||||
touched,
|
||||
setValue,
|
||||
setTouchedField,
|
||||
}: PersonalStepProps) {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2">
|
||||
<FormField
|
||||
label="First Name"
|
||||
error={touched.firstName ? errors.firstName : undefined}
|
||||
error={errors.firstName}
|
||||
required
|
||||
>
|
||||
<Input
|
||||
type="text"
|
||||
value={values.firstName}
|
||||
onChange={(e) => setValue("firstName", e.target.value)}
|
||||
onBlur={() => setTouchedField("firstName")}
|
||||
onBlur={() => undefined}
|
||||
placeholder="Enter your first name"
|
||||
className="w-full"
|
||||
/>
|
||||
@ -41,14 +41,14 @@ export function PersonalStep({
|
||||
|
||||
<FormField
|
||||
label="Last Name"
|
||||
error={touched.lastName ? errors.lastName : undefined}
|
||||
error={errors.lastName}
|
||||
required
|
||||
>
|
||||
<Input
|
||||
type="text"
|
||||
value={values.lastName}
|
||||
onChange={(e) => setValue("lastName", e.target.value)}
|
||||
onBlur={() => setTouchedField("lastName")}
|
||||
onBlur={() => undefined}
|
||||
placeholder="Enter your last name"
|
||||
className="w-full"
|
||||
/>
|
||||
@ -57,14 +57,14 @@ export function PersonalStep({
|
||||
|
||||
<FormField
|
||||
label="Email Address"
|
||||
error={touched.email ? errors.email : undefined}
|
||||
error={errors.email}
|
||||
required
|
||||
>
|
||||
<Input
|
||||
type="email"
|
||||
value={values.email}
|
||||
onChange={(e) => setValue("email", e.target.value)}
|
||||
onBlur={() => setTouchedField("email")}
|
||||
onBlur={() => undefined}
|
||||
placeholder="Enter your email address"
|
||||
className="w-full"
|
||||
/>
|
||||
@ -72,14 +72,13 @@ export function PersonalStep({
|
||||
|
||||
<FormField
|
||||
label="Phone Number"
|
||||
error={touched.phone ? errors.phone : undefined}
|
||||
error={errors.phone}
|
||||
required
|
||||
>
|
||||
<Input
|
||||
type="tel"
|
||||
value={values.phone || ""}
|
||||
onChange={(e) => setValue("phone", e.target.value)}
|
||||
onBlur={() => setTouchedField("phone")}
|
||||
placeholder="+81 XX-XXXX-XXXX"
|
||||
className="w-full"
|
||||
/>
|
||||
@ -87,15 +86,14 @@ export function PersonalStep({
|
||||
|
||||
<FormField
|
||||
label="Customer Number"
|
||||
error={touched.sfNumber ? errors.sfNumber : undefined}
|
||||
error={errors.sfNumber}
|
||||
required
|
||||
helpText="Your existing customer number (minimum 6 characters)"
|
||||
helperText="Your existing customer number (minimum 6 characters)"
|
||||
>
|
||||
<Input
|
||||
type="text"
|
||||
value={values.sfNumber}
|
||||
onChange={(e) => setValue("sfNumber", e.target.value)}
|
||||
onBlur={() => setTouchedField("sfNumber")}
|
||||
placeholder="Enter your customer number"
|
||||
className="w-full"
|
||||
/>
|
||||
@ -103,13 +101,12 @@ export function PersonalStep({
|
||||
|
||||
<FormField
|
||||
label="Company (Optional)"
|
||||
error={touched.company ? errors.company : undefined}
|
||||
error={errors.company}
|
||||
>
|
||||
<Input
|
||||
type="text"
|
||||
value={values.company || ""}
|
||||
onChange={(e) => setValue("company", e.target.value)}
|
||||
onBlur={() => setTouchedField("company")}
|
||||
placeholder="Enter your company name"
|
||||
className="w-full"
|
||||
/>
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { LoadingSpinner } from "@/components/ui/loading-spinner";
|
||||
import { Skeleton } from "@/components/ui";
|
||||
import { AlertBanner } from "@/components/common/AlertBanner";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { SubCard } from "@/components/ui/sub-card";
|
||||
@ -209,7 +209,7 @@ export function AddressConfirmation({
|
||||
if (loading) {
|
||||
return wrap(
|
||||
<div className="flex items-center space-x-3">
|
||||
<LoadingSpinner size="sm" />
|
||||
<Skeleton className="h-4 w-4 rounded-full" />
|
||||
<span className="text-gray-600">Loading address information...</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
import { PageLayout } from "@/components/layout/PageLayout";
|
||||
import { LoadingCard, Skeleton } from "@/components/ui/loading-skeleton";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { LoadingSpinner } from "@/components/ui/loading-spinner";
|
||||
import { Skeleton } from "@/components/ui";
|
||||
import { AnimatedCard } from "@/components/ui";
|
||||
import { AddonGroup } from "@/features/catalog/components/base/AddonGroup";
|
||||
import { StepHeader } from "@/components/ui";
|
||||
|
||||
@ -28,9 +28,7 @@ export type UseSimConfigureResult = {
|
||||
// Zod form integration
|
||||
values: SimConfigureFormData;
|
||||
errors: Record<string, string>;
|
||||
touched: Record<string, boolean>;
|
||||
setValue: <K extends keyof SimConfigureFormData>(field: K, value: SimConfigureFormData[K]) => void;
|
||||
setTouchedField: (field: keyof SimConfigureFormData) => void;
|
||||
validate: () => boolean;
|
||||
|
||||
// Convenience getters for specific fields
|
||||
@ -64,8 +62,7 @@ export type UseSimConfigureResult = {
|
||||
|
||||
export function useSimConfigure(planId?: string): UseSimConfigureResult {
|
||||
const searchParams = useSearchParams();
|
||||
const { simParams } = useSimConfigureParams();
|
||||
const { simData, loading: simLoading } = useSimCatalog();
|
||||
const { data: simData, isLoading: simLoading } = useSimCatalog();
|
||||
const { plan: selectedPlan } = useSimPlan(planId);
|
||||
|
||||
// Step orchestration state
|
||||
@ -76,11 +73,8 @@ export function useSimConfigure(planId?: string): UseSimConfigureResult {
|
||||
const {
|
||||
values,
|
||||
errors,
|
||||
touched,
|
||||
setValue,
|
||||
setTouchedField,
|
||||
validate,
|
||||
setValues,
|
||||
} = useZodForm({
|
||||
schema: simConfigureFormSchema,
|
||||
initialValues: {
|
||||
@ -105,7 +99,7 @@ export function useSimConfigure(planId?: string): UseSimConfigureResult {
|
||||
},
|
||||
onSubmit: async (data) => {
|
||||
// This hook doesn't submit directly, just validates
|
||||
return simConfigureFormToRequest(data);
|
||||
simConfigureFormToRequest(data);
|
||||
},
|
||||
});
|
||||
|
||||
@ -129,15 +123,13 @@ export function useSimConfigure(planId?: string): UseSimConfigureResult {
|
||||
const initialSimType = (searchParams.get("simType") as SimType) || "eSIM";
|
||||
const initialActivationType = (searchParams.get("activationType") as ActivationType) || "Immediate";
|
||||
|
||||
setValues({
|
||||
simType: initialSimType,
|
||||
eid: searchParams.get("eid") || "",
|
||||
selectedAddons: searchParams.get("addons")?.split(",").filter(Boolean) || [],
|
||||
activationType: initialActivationType,
|
||||
scheduledActivationDate: searchParams.get("scheduledDate") || "",
|
||||
wantsMnp: searchParams.get("wantsMnp") === "true",
|
||||
mnpData: values.mnpData, // Keep existing MNP data
|
||||
});
|
||||
|
||||
setValue("simType", initialSimType);
|
||||
setValue("eid", searchParams.get("eid") || "");
|
||||
setValue("selectedAddons", searchParams.get("addons")?.split(",").filter(Boolean) || []);
|
||||
setValue("activationType", initialActivationType);
|
||||
setValue("scheduledActivationDate", searchParams.get("scheduledDate") || "");
|
||||
setValue("wantsMnp", searchParams.get("wantsMnp") === "true");
|
||||
}
|
||||
}
|
||||
|
||||
@ -145,7 +137,7 @@ export function useSimConfigure(planId?: string): UseSimConfigureResult {
|
||||
return () => {
|
||||
mounted = false;
|
||||
};
|
||||
}, [simLoading, simData, selectedPlan, searchParams, setValues]);
|
||||
}, [simLoading, simData, selectedPlan, searchParams, setValue]);
|
||||
|
||||
// Step transition handler (memoized)
|
||||
const transitionToStep = useCallback((nextStep: number) => {
|
||||
@ -168,10 +160,10 @@ export function useSimConfigure(planId?: string): UseSimConfigureResult {
|
||||
values.selectedAddons.forEach(addonId => {
|
||||
const addon = simData.addons.find(a => a.id === addonId);
|
||||
if (addon) {
|
||||
if (addon.billingType === "monthly") {
|
||||
monthly += addon.price;
|
||||
if ((addon as any).billingType === "monthly") {
|
||||
monthly += (addon as any).price || 0;
|
||||
} else {
|
||||
oneTime += addon.price;
|
||||
oneTime += (addon as any).price || 0;
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -180,10 +172,10 @@ export function useSimConfigure(planId?: string): UseSimConfigureResult {
|
||||
// Add activation fees
|
||||
if (simData?.activationFees) {
|
||||
const activationFee = simData.activationFees.find(
|
||||
fee => fee.simType === values.simType
|
||||
fee => (fee as any).simType === values.simType
|
||||
);
|
||||
if (activationFee) {
|
||||
oneTime += activationFee.amount;
|
||||
oneTime += (activationFee as any).amount || 0;
|
||||
}
|
||||
}
|
||||
|
||||
@ -223,7 +215,7 @@ export function useSimConfigure(planId?: string): UseSimConfigureResult {
|
||||
|
||||
return {
|
||||
// Data
|
||||
plan: selectedPlan,
|
||||
plan: selectedPlan || null,
|
||||
activationFees: simData?.activationFees || [],
|
||||
addons: simData?.addons || [],
|
||||
loading: simLoading,
|
||||
@ -231,9 +223,7 @@ export function useSimConfigure(planId?: string): UseSimConfigureResult {
|
||||
// Zod form integration
|
||||
values,
|
||||
errors,
|
||||
touched,
|
||||
setValue,
|
||||
setTouchedField,
|
||||
validate,
|
||||
|
||||
// Convenience getters/setters
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
import { PageLayout } from "@/components/layout/PageLayout";
|
||||
import { ShieldCheckIcon, ArrowLeftIcon } from "@heroicons/react/24/outline";
|
||||
import { useVpnCatalog } from "@/features/catalog/hooks";
|
||||
import { LoadingSpinner } from "@/components/ui/loading-spinner";
|
||||
import { LoadingCard } from "@/components/ui";
|
||||
import { AsyncBlock } from "@/components/common/AsyncBlock";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { AlertBanner } from "@/components/common/AlertBanner";
|
||||
|
||||
@ -23,7 +23,7 @@ import { format, formatDistanceToNow } from "date-fns";
|
||||
import { useAuthStore } from "@/features/auth/services/auth.store";
|
||||
import { useDashboardSummary } from "@/features/dashboard/hooks";
|
||||
import { StatCard, QuickAction, DashboardActivityItem } from "@/features/dashboard/components";
|
||||
import { LoadingSpinner } from "@/components/ui/loading-skeleton";
|
||||
import { LoadingStats, LoadingTable } from "@/components/ui";
|
||||
import { ErrorState } from "@/components/ui/error-state";
|
||||
import { formatCurrency, getCurrencyLocale } from "@customer-portal/domain";
|
||||
|
||||
@ -69,7 +69,10 @@ export function DashboardView() {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-64">
|
||||
<div className="text-center space-y-4">
|
||||
<LoadingSpinner size="lg" />
|
||||
<div className="space-y-6">
|
||||
<LoadingStats />
|
||||
<LoadingTable />
|
||||
</div>
|
||||
<p className="text-muted-foreground">Loading dashboard...</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -31,10 +31,7 @@ export type {
|
||||
// COMPONENT PROPS (Pure UI)
|
||||
// =====================================================
|
||||
|
||||
export interface LoadingSpinnerProps {
|
||||
size?: "sm" | "md" | "lg";
|
||||
className?: string;
|
||||
}
|
||||
// LoadingSpinnerProps removed - using skeleton loading instead
|
||||
|
||||
export interface ErrorBoundaryProps {
|
||||
children: React.ReactNode;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user