feat(bookings): Implement skeleton loaders for new booking form steps

This commit is contained in:
ghost2023
2026-06-01 16:10:06 +03:00
parent c880e8c7f9
commit 217f1bf097
4 changed files with 95 additions and 53 deletions

View File

@@ -33,7 +33,7 @@ export default function NewBookingPage() {
const queryClient = useQueryClient();
const [step, setStep] = useState(1);
const { customer } = useAuth();
const { data: referenceData } = useQuery(
const { data: referenceData, isLoading: refDataLoading } = useQuery(
api.bookings.referenceData.queryOptions(),
);
@@ -181,13 +181,14 @@ export default function NewBookingPage() {
{step === 1 && <Step1ContractType form={form} />}
{step === 2 && <Step2ServiceType form={form} />}
{step === 3 && (
<Step4Route form={form} referenceData={referenceData} />
<Step4Route form={form} referenceData={referenceData} isLoading={refDataLoading} />
)}
{step === 4 && (
<Step5CargoDetails
form={form}
direction={direction}
referenceData={referenceData}
isLoading={refDataLoading}
/>
)}
{step === 5 && (

View File

@@ -1,27 +1,28 @@
import { useEffect, useMemo } from "react";
import { Controller, type UseFormReturn } from "react-hook-form";
import { Flame, MapPin, Snowflake } from "lucide-react";
import { Field, SelectItem, Separator, Switch } from "@edr/ui-common";
import { Field, SelectItem, Separator, Skeleton, Switch } from "@edr/ui-common";
import type { Freight } from "@edr/types";
import {
type BookingFormValues,
getRouteDirection,
} from "./schema";
import {
AlertBox,
SelectField,
StepHeader,
StepLabel,
} from "./shared";
import { useEffect, useMemo } from "react";
type BookingForm = UseFormReturn<BookingFormValues>;
export function Step4Route({
form,
referenceData,
isLoading,
}: {
form: BookingForm;
referenceData?: Freight.BookingReferenceData;
isLoading?: boolean;
}) {
const originYard = form.watch("originYard");
const destinationYard = form.watch("destinationYard");
@@ -70,52 +71,50 @@ export function Step4Route({
description="Select the origin and destination yards."
/>
<div className="space-y-3">
<StepLabel>Route</StepLabel>
<div className="grid gap-3 sm:grid-cols-2">
<Controller
name="originYard"
control={form.control}
render={({ field, fieldState }) => (
<SelectField
field={field}
error={fieldState.error}
label="Origin Yard*"
placeholder="Select origin..."
disabled={stationSelectDisabled}
>
<YardSelectOptions
options={yardOptions}
excludeValue={destinationYard}
/>
</SelectField>
)}
/>
<Controller
name="destinationYard"
control={form.control}
render={({ field, fieldState }) => (
<SelectField
field={field}
error={fieldState.error}
label="Destination Yard *"
placeholder="Select destination..."
disabled={stationSelectDisabled}
>
<YardSelectOptions
options={yardOptions}
excludeValue={originYard}
/>
</SelectField>
)}
/>
</div>
{!referenceData && (
<AlertBox tone="warning">
Loading reference data...
</AlertBox>
)}
{direction && (
{isLoading ? (
<LoadingSkeleton />
) : (
<div className="space-y-3">
<StepLabel>Route</StepLabel>
<div className="grid gap-3 sm:grid-cols-2">
<Controller
name="originYard"
control={form.control}
render={({ field, fieldState }) => (
<SelectField
field={field}
error={fieldState.error}
label="Origin Yard*"
placeholder="Select origin..."
disabled={stationSelectDisabled}
>
<YardSelectOptions
options={yardOptions}
excludeValue={destinationYard}
/>
</SelectField>
)}
/>
<Controller
name="destinationYard"
control={form.control}
render={({ field, fieldState }) => (
<SelectField
field={field}
error={fieldState.error}
label="Destination Yard *"
placeholder="Select destination..."
disabled={stationSelectDisabled}
>
<YardSelectOptions
options={yardOptions}
excludeValue={originYard}
/>
</SelectField>
)}
/>
</div>
{direction && (
<div
className={`flex items-center gap-2 rounded-lg border px-3 py-2 text-xs font-medium ${directionStyle[direction]}`}
>
@@ -123,7 +122,8 @@ export function Step4Route({
{directionLabel[direction]}
</div>
)}
</div>
</div>
)}
{direction && direction != "domestic" && (
<Controller
@@ -190,6 +190,24 @@ export function Step4Route({
);
}
function LoadingSkeleton() {
return (
<div className="space-y-4 rounded-xl border border-border p-4">
<div className="grid gap-3 sm:grid-cols-2">
<div className="space-y-2">
<Skeleton className="h-3 w-20" />
<Skeleton className="h-10 w-full" />
</div>
<div className="space-y-2">
<Skeleton className="h-3 w-24" />
<Skeleton className="h-10 w-full" />
</div>
</div>
<Skeleton className="h-8 w-full" />
</div>
);
}
function YardSelectOptions({
options,
excludeValue,

View File

@@ -1,7 +1,7 @@
import { useMemo } from "react";
import { Controller, useFieldArray, type UseFormReturn } from "react-hook-form";
import { MapPin, Package, Plus, Trash2, Weight } from "lucide-react";
import { Button, Field, FieldError, FieldLabel, Input } from "@edr/ui-common";
import { Button, Field, FieldError, FieldLabel, Input, Skeleton } from "@edr/ui-common";
import type { Freight } from "@edr/types";
import {
calcWagons,
@@ -23,10 +23,12 @@ export function Step5CargoDetails({
form,
direction,
referenceData,
isLoading,
}: {
form: BookingForm;
direction: RouteDirection;
referenceData?: Freight.BookingReferenceData;
isLoading?: boolean;
}) {
const cargoType = form.watch("cargoType");
const freightType = form.watch("freightType");
@@ -69,6 +71,26 @@ export function Step5CargoDetails({
return null;
}
if (isLoading) {
return (
<div className="space-y-6">
<StepHeader
title="Cargo Details"
description="Define your cargo type, weight, and container configuration."
/>
<div className="space-y-4 rounded-xl border border-border p-4">
<Skeleton className="h-4 w-24" />
<div className="grid gap-3 sm:grid-cols-2">
<Skeleton className="h-24 w-full" />
<Skeleton className="h-24 w-full" />
</div>
<Skeleton className="h-10 w-full" />
<Skeleton className="h-10 w-1/3" />
</div>
</div>
);
}
return (
<div className="space-y-6">
<StepHeader

View File

@@ -23,6 +23,7 @@ export type {
export * from "./components/button";
export * from "./components/input";
export * from "./components/skeleton";
export * from "./components/textarea";
export * from "./components/label";
export * from "./components/card";