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

View File

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

View File

@@ -1,7 +1,7 @@
import { useMemo } from "react"; import { useMemo } from "react";
import { Controller, useFieldArray, type UseFormReturn } from "react-hook-form"; import { Controller, useFieldArray, type UseFormReturn } from "react-hook-form";
import { MapPin, Package, Plus, Trash2, Weight } from "lucide-react"; 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 type { Freight } from "@edr/types";
import { import {
calcWagons, calcWagons,
@@ -23,10 +23,12 @@ export function Step5CargoDetails({
form, form,
direction, direction,
referenceData, referenceData,
isLoading,
}: { }: {
form: BookingForm; form: BookingForm;
direction: RouteDirection; direction: RouteDirection;
referenceData?: Freight.BookingReferenceData; referenceData?: Freight.BookingReferenceData;
isLoading?: boolean;
}) { }) {
const cargoType = form.watch("cargoType"); const cargoType = form.watch("cargoType");
const freightType = form.watch("freightType"); const freightType = form.watch("freightType");
@@ -69,6 +71,26 @@ export function Step5CargoDetails({
return null; 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 ( return (
<div className="space-y-6"> <div className="space-y-6">
<StepHeader <StepHeader

View File

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