Skip to content

Commit b83a968

Browse files
committed
feat: add route aware checkout shell
1 parent 49f46a1 commit b83a968

8 files changed

Lines changed: 107 additions & 87 deletions

File tree

apps/herbatika/src/app/layout.tsx

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import type { Metadata } from "next";
22
import localFont from "next/font/local";
33
import { Open_Sans, Roboto, Rubik } from "next/font/google";
4-
import { HerbatikaFooter } from "@/components/herbatika-footer";
5-
import { HerbatikaHeader } from "@/components/herbatika-header";
4+
import { Suspense } from "react";
5+
import { AppShell } from "@/components/app-shell";
66
import "./globals.css";
77
import { Providers } from "./providers";
88

@@ -59,11 +59,9 @@ export default function RootLayout({
5959
>
6060
<body className={`text-fg-primary ${verdana.className}`}>
6161
<Providers>
62-
<div className="flex min-h-dvh flex-col bg-base">
63-
<HerbatikaHeader />
64-
<div className="flex-1">{children}</div>
65-
<HerbatikaFooter />
66-
</div>
62+
<Suspense fallback={<div className="min-h-dvh bg-base">{children}</div>}>
63+
<AppShell>{children}</AppShell>
64+
</Suspense>
6765
</Providers>
6866
</body>
6967
</html>
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
"use client";
2+
3+
import { usePathname } from "next/navigation";
4+
import type { PropsWithChildren } from "react";
5+
import { CheckoutFooter } from "@/components/checkout/checkout-footer";
6+
import { CheckoutHeader } from "@/components/checkout/checkout-header";
7+
import { HerbatikaFooter } from "@/components/herbatika-footer";
8+
import { HerbatikaHeader } from "@/components/herbatika-header";
9+
10+
export function AppShell({ children }: PropsWithChildren) {
11+
const pathname = usePathname();
12+
const isCheckoutRoute = pathname.startsWith("/checkout");
13+
14+
if (isCheckoutRoute) {
15+
return (
16+
<div className="flex min-h-dvh flex-col bg-base">
17+
<CheckoutHeader />
18+
<div className="flex-1">{children}</div>
19+
<CheckoutFooter />
20+
</div>
21+
);
22+
}
23+
24+
return (
25+
<div className="flex min-h-dvh flex-col bg-base">
26+
<HerbatikaHeader />
27+
<div className="flex-1">{children}</div>
28+
<HerbatikaFooter />
29+
</div>
30+
);
31+
}

apps/herbatika/src/components/header/herbatika-cart-item.utils.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ export const resolveLineItemHref = (item: HttpTypes.StoreCartLineItem) => {
1414
return `/p/${productHandle}`;
1515
}
1616

17-
return "/checkout";
17+
return "/checkout/kosik";
1818
};
1919

2020
export const resolveLineItemInventory = (item: HttpTypes.StoreCartLineItem) => {

apps/herbatika/src/components/header/herbatika-cart-popover.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -181,7 +181,7 @@ export function HerbatikaCartPopover({
181181
as={NextLink}
182182
block
183183
className="justify-center"
184-
href="/checkout"
184+
href="/checkout/kosik"
185185
onClick={handleClose}
186186
size="md"
187187
variant="primary"

apps/herbatika/src/components/herbatika-header.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,7 @@ export function HerbatikaHeader() {
151151
<LinkButton
152152
as={NextLink}
153153
className="px-350 py-250 text-xl font-bold"
154-
href="/checkout"
154+
href="/checkout/kosik"
155155
icon="token-icon-cart"
156156
size="sm"
157157
variant="primary"

apps/herbatika/src/components/herbatika-logo.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,22 +5,25 @@ import NextLink from "next/link";
55
interface HerbatikaLogoProps {
66
className?: string;
77
href?: string;
8+
imageClassName?: string;
89
size?: "sm" | "md" | "lg";
910
}
1011

1112
export function HerbatikaLogo({
1213
className,
1314
href = "/",
15+
imageClassName,
1416
size = "md",
1517
}: HerbatikaLogoProps) {
1618
const sizeClass =
1719
size === "sm" ? "h-11 w-auto" : size === "lg" ? "h-[4.5rem] w-auto" : "h-13 w-auto";
20+
const imageClasses = imageClassName ? `${sizeClass} ${imageClassName}` : sizeClass;
1821

1922
return (
2023
<Link as={NextLink} className={className} href={href}>
2124
<Image
2225
alt="Herbatika"
23-
className={sizeClass}
26+
className={imageClasses}
2427
src="/herbatika-logo.svg"
2528
/>
2629
</Link>

apps/herbatika/src/components/storefront-checkout-flow.tsx

Lines changed: 63 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,77 @@
11
"use client";
22

3-
import { COUNTRY_SELECT_ITEMS } from "@/components/checkout/checkout.constants";
3+
import { useRouter } from "next/navigation";
4+
import { useEffect } from "react";
5+
import type { CheckoutStepSlug } from "@/components/checkout/checkout.constants";
6+
import { CheckoutStepContent } from "@/components/checkout/checkout-step-content";
7+
import {
8+
canAccessCheckoutStep,
9+
resolveCheckoutStepHref,
10+
resolveCheckoutStepIndexBySlug,
11+
resolveRequiredCheckoutStepSlug,
12+
} from "@/components/checkout/checkout-route.utils";
413
import { useCheckoutController } from "@/components/checkout/use-checkout-controller";
5-
import { CheckoutAddressSection } from "@/components/checkout/sections/checkout-address-section";
6-
import { CheckoutCompleteSection } from "@/components/checkout/sections/checkout-complete-section";
714
import { CheckoutCompletedOrderSection } from "@/components/checkout/sections/checkout-completed-order-section";
815
import { CheckoutEmptyCartSection } from "@/components/checkout/sections/checkout-empty-cart-section";
916
import { CheckoutFeedbackSection } from "@/components/checkout/sections/checkout-feedback-section";
10-
import { CheckoutOrderSummarySection } from "@/components/checkout/sections/checkout-order-summary-section";
11-
import { CheckoutPaymentSection } from "@/components/checkout/sections/checkout-payment-section";
12-
import { CheckoutShippingSection } from "@/components/checkout/sections/checkout-shipping-section";
1317
import { CheckoutStepsSection } from "@/components/checkout/sections/checkout-steps-section";
1418

15-
export function StorefrontCheckoutFlow() {
19+
type StorefrontCheckoutFlowProps = {
20+
activeStep: CheckoutStepSlug;
21+
};
22+
23+
export function StorefrontCheckoutFlow({
24+
activeStep,
25+
}: StorefrontCheckoutFlowProps) {
26+
const router = useRouter();
1627
const controller = useCheckoutController();
28+
const requiredStep = resolveRequiredCheckoutStepSlug({
29+
hasItems: controller.hasItems,
30+
hasPayment: controller.hasPayment,
31+
hasShipping: controller.hasShipping,
32+
hasStoredAddress: controller.hasStoredAddress,
33+
});
34+
const redirectStep =
35+
requiredStep === "kosik" && activeStep !== "kosik"
36+
? "doprava-platba"
37+
: requiredStep;
1738

18-
return (
19-
<main className="mx-auto flex w-full max-w-max-w flex-col gap-500 px-400 py-550 lg:px-550">
20-
<header className="space-y-200">
21-
<h1 className="text-2xl font-semibold text-fg-primary">Dokončenie objednávky</h1>
22-
<p className="text-sm text-fg-secondary">
23-
Vyplňte údaje, zvoľte dopravu a platbu, potom potvrďte objednávku.
24-
</p>
25-
</header>
39+
const canAccessStep = canAccessCheckoutStep({
40+
requestedStep: activeStep,
41+
hasItems: controller.hasItems,
42+
hasPayment: controller.hasPayment,
43+
hasShipping: controller.hasShipping,
44+
hasStoredAddress: controller.hasStoredAddress,
45+
});
46+
47+
const isStepGateLoading =
48+
controller.cartQuery.isLoading || controller.cartQuery.isFetching;
49+
const hasResolvedCart = typeof controller.cartQuery.cart !== "undefined";
50+
const shouldRedirectStep =
51+
hasResolvedCart &&
52+
!isStepGateLoading &&
53+
!canAccessStep &&
54+
!controller.completedOrderId &&
55+
redirectStep !== activeStep;
56+
57+
useEffect(() => {
58+
if (!shouldRedirectStep) {
59+
return;
60+
}
61+
62+
router.replace(resolveCheckoutStepHref(redirectStep));
63+
}, [redirectStep, router, shouldRedirectStep]);
64+
65+
if (shouldRedirectStep) {
66+
return <main className="mx-auto min-h-dvh w-full max-w-max-w" />;
67+
}
2668

69+
const activeStepIndex = resolveCheckoutStepIndexBySlug(activeStep);
70+
71+
return (
72+
<main className="checkout-main mx-auto flex w-full flex-col gap-750 px-400 pt-700 pb-850 lg:px-550">
2773
<CheckoutStepsSection
28-
checkoutStepIndex={controller.checkoutStepIndex}
74+
checkoutStepIndex={activeStepIndex}
2975
steps={controller.checkoutSteps}
3076
/>
3177

@@ -42,66 +88,7 @@ export function StorefrontCheckoutFlow() {
4288
{!controller.completedOrderId && !controller.hasItems ? <CheckoutEmptyCartSection /> : null}
4389

4490
{!controller.completedOrderId && controller.hasItems ? (
45-
<div className="grid gap-500 xl:grid-cols-3">
46-
<div className="space-y-350 xl:col-span-2">
47-
<CheckoutAddressSection
48-
acceptTermsConsent={controller.acceptTermsConsent}
49-
addressForm={controller.addressForm}
50-
countryItems={COUNTRY_SELECT_ITEMS}
51-
createAccountConsent={controller.createAccountConsent}
52-
hasStoredAddress={controller.hasStoredAddress}
53-
isBusy={controller.isBusy}
54-
isSavingAddress={controller.updateCartAddressMutation.isPending}
55-
onAcceptTermsConsentChange={controller.setAcceptTermsConsent}
56-
onCreateAccountConsentChange={controller.setCreateAccountConsent}
57-
onSaveAddress={controller.handleSaveAddress}
58-
onUpdateAddressField={controller.updateAddressField}
59-
ready={Boolean(controller.cartQuery.cart?.id)}
60-
/>
61-
62-
<CheckoutShippingSection
63-
currencyCode={controller.currencyCode}
64-
hasShipping={controller.hasShipping}
65-
isBusy={controller.isBusy}
66-
onSelectShipping={controller.handleSelectShipping}
67-
selectedShippingMethodId={controller.checkoutShippingQuery.selectedShippingMethodId}
68-
shippingOptions={controller.checkoutShippingQuery.shippingOptions}
69-
shippingPrices={controller.checkoutShippingQuery.shippingPrices}
70-
/>
71-
72-
<CheckoutPaymentSection
73-
canInitiatePayment={controller.checkoutPaymentQuery.canInitiatePayment}
74-
hasPayment={controller.hasPayment}
75-
isBusy={controller.isBusy}
76-
isInitiatingPayment={controller.checkoutPaymentQuery.isInitiatingPayment}
77-
onSelectPaymentProvider={controller.handleSelectPaymentProvider}
78-
paymentProviders={controller.checkoutPaymentQuery.paymentProviders}
79-
/>
80-
81-
<CheckoutCompleteSection
82-
acceptTermsConsent={controller.acceptTermsConsent}
83-
canCompleteOrder={controller.canCompleteOrder}
84-
hasPayment={controller.hasPayment}
85-
hasShipping={controller.hasShipping}
86-
hasStoredAddress={controller.hasStoredAddress}
87-
isCompletingOrder={controller.completeCartMutation.isPending}
88-
onCompleteOrder={controller.handleCompleteOrder}
89-
/>
90-
</div>
91-
92-
<aside className="space-y-300 xl:sticky xl:top-400 xl:self-start">
93-
<CheckoutOrderSummarySection
94-
cartItems={controller.cartItems}
95-
cartSubtotalAmount={controller.cartSubtotalAmount}
96-
cartTotalAmount={controller.cartTotalAmount}
97-
currencyCode={controller.currencyCode}
98-
hasPayment={controller.hasPayment}
99-
hasShipping={controller.hasShipping}
100-
selectedOptionName={controller.checkoutShippingQuery.selectedOption?.name ?? undefined}
101-
selectedShippingPrice={controller.selectedShippingPrice}
102-
/>
103-
</aside>
104-
</div>
91+
<CheckoutStepContent activeStep={activeStep} controller={controller} />
10592
) : null}
10693
</main>
10794
);

apps/herbatika/src/styles/tokens/components/components.css

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
@import "./_herbatica-button.css";
22
@import "./_herbatica-breadcrumb.css";
33
@import "./_herbatica-blog.css";
4+
@import "./_herbatica-checkout.css";
45
@import "./_herbatica-footer.css";
56
@import "./_herbatica-header.css";
67
@import "./_herbatica-homepage.css";

0 commit comments

Comments
 (0)