Skip to content

Commit c0870aa

Browse files
committed
feat: checkout
1 parent 91bf2ae commit c0870aa

22 files changed

Lines changed: 1217 additions & 316 deletions
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { HerbatikaLogo } from "@/components/herbatika-logo";
2+
3+
export function CheckoutFooter() {
4+
return (
5+
<footer className="w-full border-t border-border-secondary bg-surface">
6+
<div className="mx-auto flex w-full max-w-max-w items-center justify-center px-400 py-700 lg:px-550">
7+
<HerbatikaLogo imageClassName="h-15" size="md" />
8+
</div>
9+
</footer>
10+
);
11+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
"use client";
2+
3+
import { Icon } from "@techsio/ui-kit/atoms/icon";
4+
import { Link } from "@techsio/ui-kit/atoms/link";
5+
import { LinkButton } from "@techsio/ui-kit/atoms/link-button";
6+
import NextLink from "next/link";
7+
import { HerbatikaLogo } from "@/components/herbatika-logo";
8+
9+
export function CheckoutHeader() {
10+
return (
11+
<header className="w-full border-b border-border-secondary bg-surface">
12+
<div className="mx-auto flex w-full max-w-max-w items-center justify-between gap-250 px-400 py-350 lg:px-550">
13+
<div className="flex min-w-0 items-center gap-300">
14+
<HerbatikaLogo imageClassName="h-15" size="md" />
15+
<span className="hidden items-center gap-150 text-sm text-fg-primary sm:inline-flex">
16+
<Icon className="text-sm" icon="icon-[mdi--shield-check-outline]" />
17+
Bezpečný nákup
18+
</span>
19+
</div>
20+
21+
<div className="flex items-center gap-200">
22+
<Link
23+
as={NextLink}
24+
className="hidden items-center gap-100 text-sm text-fg-primary hover:text-primary md:inline-flex"
25+
href="/#chat"
26+
>
27+
<Icon className="text-primary" icon="icon-[mdi--message-outline]" />
28+
Spustiť chat
29+
</Link>
30+
<Link
31+
as={NextLink}
32+
className="hidden items-center gap-100 text-sm text-fg-primary hover:text-primary lg:inline-flex"
33+
href="tel:+421232112345"
34+
>
35+
<Icon className="text-primary" icon="icon-[mdi--phone-outline]" />
36+
+421 2/321 123 45
37+
</Link>
38+
<LinkButton
39+
as={NextLink}
40+
className="px-250 text-sm font-medium"
41+
href="/auth/login"
42+
size="sm"
43+
variant="secondary"
44+
>
45+
Prihlásiť sa
46+
</LinkButton>
47+
</div>
48+
</div>
49+
</header>
50+
);
51+
}
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import {
2+
CHECKOUT_STEPS,
3+
DEFAULT_CHECKOUT_STEP_SLUG,
4+
type CheckoutStepSlug,
5+
} from "./checkout.constants";
6+
7+
const CHECKOUT_STEP_SLUGS = CHECKOUT_STEPS.map((step) => step.slug);
8+
9+
export const isCheckoutStepSlug = (value: string): value is CheckoutStepSlug => {
10+
return CHECKOUT_STEP_SLUGS.includes(value as CheckoutStepSlug);
11+
};
12+
13+
export const resolveCheckoutStepSlug = (
14+
value: string | undefined,
15+
): CheckoutStepSlug => {
16+
if (!value) {
17+
return DEFAULT_CHECKOUT_STEP_SLUG;
18+
}
19+
20+
return isCheckoutStepSlug(value) ? value : DEFAULT_CHECKOUT_STEP_SLUG;
21+
};
22+
23+
export const resolveCheckoutStepHref = (step: CheckoutStepSlug) => {
24+
return `/checkout/${step}`;
25+
};
26+
27+
export const resolveCheckoutStepIndexBySlug = (step: CheckoutStepSlug) => {
28+
const index = CHECKOUT_STEPS.findIndex((item) => item.slug === step);
29+
return index >= 0 ? index : 0;
30+
};
31+
32+
export const resolveRequiredCheckoutStepSlug = (params: {
33+
hasItems: boolean;
34+
hasPayment: boolean;
35+
hasShipping: boolean;
36+
hasStoredAddress: boolean;
37+
}): CheckoutStepSlug => {
38+
if (!params.hasItems) {
39+
return "kosik";
40+
}
41+
42+
if (!params.hasShipping || !params.hasPayment) {
43+
return "doprava-platba";
44+
}
45+
46+
if (!params.hasStoredAddress) {
47+
return "udaje";
48+
}
49+
50+
return "suhrn";
51+
};
52+
53+
export const canAccessCheckoutStep = (params: {
54+
requestedStep: CheckoutStepSlug;
55+
hasItems: boolean;
56+
hasPayment: boolean;
57+
hasShipping: boolean;
58+
hasStoredAddress: boolean;
59+
}) => {
60+
switch (params.requestedStep) {
61+
case "kosik":
62+
return true;
63+
case "doprava-platba":
64+
return true;
65+
case "udaje":
66+
return params.hasItems && params.hasShipping && params.hasPayment;
67+
case "suhrn":
68+
return (
69+
params.hasItems &&
70+
params.hasShipping &&
71+
params.hasPayment &&
72+
params.hasStoredAddress
73+
);
74+
default:
75+
return false;
76+
}
77+
};
Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
import { COUNTRY_SELECT_ITEMS } from "@/components/checkout/checkout.constants";
2+
import type { CheckoutStepSlug } from "@/components/checkout/checkout.constants";
3+
import { resolveCheckoutStepHref } from "@/components/checkout/checkout-route.utils";
4+
import type { CheckoutController } from "@/components/checkout/use-checkout-controller";
5+
import { CheckoutCartStepSection } from "@/components/checkout/sections/checkout-cart-step-section";
6+
import { CheckoutDetailsStepSection } from "@/components/checkout/sections/checkout-details-step-section";
7+
import { CheckoutOrderSummarySection } from "@/components/checkout/sections/checkout-order-summary-section";
8+
import { CheckoutShippingPaymentStepSection } from "@/components/checkout/sections/checkout-shipping-payment-step-section";
9+
import { CheckoutSummaryStepSection } from "@/components/checkout/sections/checkout-summary-step-section";
10+
import { resolveProviderLabel } from "@/components/checkout/checkout.utils";
11+
12+
type CheckoutStepContentProps = {
13+
activeStep: CheckoutStepSlug;
14+
controller: CheckoutController;
15+
};
16+
17+
export function CheckoutStepContent({
18+
activeStep,
19+
controller,
20+
}: CheckoutStepContentProps) {
21+
const cartStepHref = resolveCheckoutStepHref("kosik");
22+
const shippingStepHref = resolveCheckoutStepHref("doprava-platba");
23+
const detailsStepHref = resolveCheckoutStepHref("udaje");
24+
const summaryStepHref = resolveCheckoutStepHref("suhrn");
25+
const selectedPaymentProviderId = controller.hasPayment
26+
? (controller.checkoutPaymentQuery.paymentProviders[0]?.id ?? undefined)
27+
: undefined;
28+
const selectedPaymentLabel =
29+
typeof selectedPaymentProviderId === "string" && selectedPaymentProviderId.length > 0
30+
? resolveProviderLabel(selectedPaymentProviderId)
31+
: undefined;
32+
33+
const orderSummarySection = (
34+
<CheckoutOrderSummarySection
35+
cartItems={controller.cartItems}
36+
cartSubtotalAmount={controller.cartSubtotalAmount}
37+
cartTotalAmount={controller.cartTotalAmount}
38+
currencyCode={controller.currencyCode}
39+
hasPayment={controller.hasPayment}
40+
hasShipping={controller.hasShipping}
41+
selectedOptionName={controller.checkoutShippingQuery.selectedOption?.name ?? undefined}
42+
selectedShippingPrice={controller.selectedShippingPrice}
43+
/>
44+
);
45+
46+
if (activeStep === "kosik") {
47+
return (
48+
<div className="checkout-step-grid">
49+
<div className="space-y-350">
50+
<CheckoutCartStepSection
51+
cartId={controller.cartQuery.cart?.id}
52+
cartItems={controller.cartItems}
53+
cartSubtotalAmount={controller.cartSubtotalAmount}
54+
currencyCode={controller.currencyCode}
55+
nextStepHref={shippingStepHref}
56+
/>
57+
</div>
58+
<aside className="space-y-300 xl:sticky xl:top-400 xl:self-start">
59+
{orderSummarySection}
60+
</aside>
61+
</div>
62+
);
63+
}
64+
65+
if (activeStep === "doprava-platba") {
66+
return (
67+
<div className="checkout-step-grid">
68+
<div className="space-y-350">
69+
<CheckoutShippingPaymentStepSection
70+
backStepHref={cartStepHref}
71+
canContinue={controller.hasShipping && controller.hasPayment}
72+
nextStepHref={detailsStepHref}
73+
paymentProps={{
74+
canInitiatePayment: controller.checkoutPaymentQuery.canInitiatePayment,
75+
hasPayment: controller.hasPayment,
76+
isBusy: controller.isBusy,
77+
isInitiatingPayment: controller.checkoutPaymentQuery.isInitiatingPayment,
78+
onSelectPaymentProvider: controller.handleSelectPaymentProvider,
79+
paymentProviders: controller.checkoutPaymentQuery.paymentProviders,
80+
selectedPaymentProviderId,
81+
}}
82+
shippingProps={{
83+
currencyCode: controller.currencyCode,
84+
hasShipping: controller.hasShipping,
85+
isBusy: controller.isBusy,
86+
onSelectShipping: controller.handleSelectShipping,
87+
selectedShippingMethodId:
88+
controller.checkoutShippingQuery.selectedShippingMethodId,
89+
shippingOptions: controller.checkoutShippingQuery.shippingOptions,
90+
shippingPrices: controller.checkoutShippingQuery.shippingPrices,
91+
}}
92+
/>
93+
</div>
94+
<aside className="space-y-300 xl:sticky xl:top-400 xl:self-start">
95+
{orderSummarySection}
96+
</aside>
97+
</div>
98+
);
99+
}
100+
101+
if (activeStep === "udaje") {
102+
return (
103+
<div className="checkout-step-grid">
104+
<div className="space-y-350">
105+
<CheckoutDetailsStepSection
106+
addressProps={{
107+
addressForm: controller.addressForm,
108+
countryItems: COUNTRY_SELECT_ITEMS,
109+
createAccountConsent: controller.createAccountConsent,
110+
hasCustomerSupportNote: controller.hasCustomerSupportNote,
111+
hasDifferentShippingAddress: controller.hasDifferentShippingAddress,
112+
hasStoredAddress: controller.hasStoredAddress,
113+
isCompanyPurchase: controller.isCompanyPurchase,
114+
isBusy: controller.isBusy,
115+
isSavingAddress: controller.updateCartAddressMutation.isPending,
116+
onCreateAccountConsentChange: controller.setCreateAccountConsent,
117+
onCustomerSupportNoteToggle: controller.setHasCustomerSupportNote,
118+
onDifferentShippingAddressChange:
119+
controller.setHasDifferentShippingAddress,
120+
onIsCompanyPurchaseChange: controller.setIsCompanyPurchase,
121+
onSaveAddress: controller.handleSaveAddress,
122+
onUpdateAddressField: controller.updateAddressField,
123+
ready: Boolean(controller.cartQuery.cart?.id),
124+
}}
125+
backStepHref={shippingStepHref}
126+
canContinue={controller.hasStoredAddress}
127+
nextStepHref={summaryStepHref}
128+
/>
129+
</div>
130+
<aside className="space-y-300 xl:sticky xl:top-400 xl:self-start">
131+
{orderSummarySection}
132+
</aside>
133+
</div>
134+
);
135+
}
136+
137+
return (
138+
<div className="checkout-step-grid">
139+
<div className="space-y-350">
140+
<CheckoutSummaryStepSection
141+
completeProps={{
142+
addressForm: controller.addressForm,
143+
canCompleteOrder: controller.canCompleteOrder,
144+
cartTotalAmount: controller.cartTotalAmount,
145+
currencyCode: controller.currencyCode,
146+
detailsStepHref,
147+
hasPayment: controller.hasPayment,
148+
hasShipping: controller.hasShipping,
149+
hasStoredAddress: controller.hasStoredAddress,
150+
heurekaConsent: controller.heurekaConsent,
151+
isCompletingOrder: controller.completeCartMutation.isPending,
152+
marketingConsent: controller.marketingConsent,
153+
onHeurekaConsentChange: controller.setHeurekaConsent,
154+
onMarketingConsentChange: controller.setMarketingConsent,
155+
onCompleteOrder: controller.handleCompleteOrder,
156+
paymentLabel: selectedPaymentLabel,
157+
shippingLabel: controller.checkoutShippingQuery.selectedOption?.name ?? undefined,
158+
shippingStepHref,
159+
}}
160+
/>
161+
</div>
162+
<aside className="space-y-300 xl:sticky xl:top-400 xl:self-start">
163+
{orderSummarySection}
164+
</aside>
165+
</div>
166+
);
167+
}

apps/herbatika/src/components/checkout/checkout.constants.ts

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,15 @@ export type AddressFormState = {
66
lastName: string;
77
phone: string;
88
company: string;
9+
companyId: string;
10+
taxId: string;
11+
vatId: string;
912
address1: string;
1013
address2: string;
1114
city: string;
1215
postalCode: string;
1316
countryCode: string;
17+
customerNote: string;
1418
};
1519

1620
export const DEFAULT_ADDRESS_FORM: AddressFormState = {
@@ -19,11 +23,15 @@ export const DEFAULT_ADDRESS_FORM: AddressFormState = {
1923
lastName: "",
2024
phone: "",
2125
company: "",
26+
companyId: "",
27+
taxId: "",
28+
vatId: "",
2229
address1: "",
2330
address2: "",
2431
city: "",
2532
postalCode: "",
2633
countryCode: "SK",
34+
customerNote: "",
2735
};
2836

2937
export const COUNTRY_SELECT_ITEMS: SelectItem[] = [
@@ -34,8 +42,12 @@ export const COUNTRY_SELECT_ITEMS: SelectItem[] = [
3442
];
3543

3644
export const CHECKOUT_STEPS = [
37-
{ id: "cart", title: "Košík" },
38-
{ id: "address", title: "Údaje" },
39-
{ id: "shipping", title: "Doprava" },
40-
{ id: "payment", title: "Platba" },
45+
{ id: "cart", slug: "kosik", title: "Košík" },
46+
{ id: "shipping-payment", slug: "doprava-platba", title: "Doprava a platba" },
47+
{ id: "address", slug: "udaje", title: "Vaše údaje" },
48+
{ id: "summary", slug: "suhrn", title: "Súhrn" },
4149
] as const;
50+
51+
export type CheckoutStepSlug = (typeof CHECKOUT_STEPS)[number]["slug"];
52+
53+
export const DEFAULT_CHECKOUT_STEP_SLUG: CheckoutStepSlug = "kosik";

0 commit comments

Comments
 (0)