diff --git a/bau/bau.d.ts b/bau/bau.d.ts index 9f679a59..9b89dbca 100644 --- a/bau/bau.d.ts +++ b/bau/bau.d.ts @@ -139,7 +139,7 @@ type Tags = Readonly>> & { [K in keyof HTMLElementTagNameMap]: TagFunc; }; -declare function state(initVal: T): State; +declare function state(initVal?: T): State; declare function tagsNS(namespaceURI: string): Tags; declare function bind(input: BindInput): HTMLElement; declare function derive(computed: () => T): ReadonlyState; diff --git a/examples/multi-step-form/package.json b/examples/multi-step-form/package.json index 8d02ea68..7d7935c0 100644 --- a/examples/multi-step-form/package.json +++ b/examples/multi-step-form/package.json @@ -1,20 +1,24 @@ { "name": "frontendmentor-multi-step-form", + "homepage": "https://grucloud.github.io/bau/frontendmentor/multi-step-form/", "private": true, "version": "0.92.0", "type": "module", "scripts": { "dev": "vite", "build": "tsc && vite build", - "preview": "vite preview" + "preview": "vite preview", + "deploy": "gh-pages -d ../../dist" }, "devDependencies": { + "gh-pages": "6.1.1", "typescript": "^5.0.2", "vite": "^5.2.11" }, "dependencies": { "@grucloud/bau": "^0.92.0", "@grucloud/bau-css": "^0.92.0", - "@grucloud/bau-ui": "^0.92.0" + "@grucloud/bau-ui": "^0.92.0", + "bignumber.js": "9.1.2" } } diff --git a/examples/multi-step-form/src/data/addons.json b/examples/multi-step-form/src/data/addons.json new file mode 100644 index 00000000..16b3b88a --- /dev/null +++ b/examples/multi-step-form/src/data/addons.json @@ -0,0 +1,17 @@ +[ + { + "name": "Online service", + "description": "Access to multiplayer games", + "pricePerMonth": "1" + }, + { + "name": "Larger storage", + "description": "Extra 1TB of cloud save", + "pricePerMonth": "2" + }, + { + "name": "Customizable profile", + "description": "Custom theme on your profile", + "pricePerMonth": "3" + } +] diff --git a/examples/multi-step-form/src/data/plans.json b/examples/multi-step-form/src/data/plans.json new file mode 100644 index 00000000..8a154244 --- /dev/null +++ b/examples/multi-step-form/src/data/plans.json @@ -0,0 +1,17 @@ +[ + { + "name": "Arcade", + "pricePerMonth": "9", + "image": "./assets/images/icon-arcade.svg" + }, + { + "name": "Advanced", + "pricePerMonth": "12", + "image": "./assets/images/icon-advanced.svg" + }, + { + "name": "Pro", + "pricePerMonth": "15", + "image": "./assets/images/icon-pro.svg" + } +] diff --git a/examples/multi-step-form/src/multiStepForm.ts b/examples/multi-step-form/src/multiStepForm.ts index 41944135..bf916837 100644 --- a/examples/multi-step-form/src/multiStepForm.ts +++ b/examples/multi-step-form/src/multiStepForm.ts @@ -1,38 +1,67 @@ import { type Context } from "@grucloud/bau-ui/context"; import stepYourInfo from "./stepYourInfo"; import stepSelectPlan from "./stepSelectPlan"; +import stepAddOns from "./stepAddOns"; +import stepSummary from "./stepSummary"; + +import ADDONS from "./data/addons.json"; +import PLANS from "./data/plans.json"; export default function (context: Context) { const { bau, css } = context; - const { article, div, header, ul, li, span } = bau.tags; - const StepYourInfo = stepYourInfo(context); - const StepSelectPlan = stepSelectPlan(context); + const { article, div, header, ul, li } = bau.tags; const currentStepState = bau.state(1); + const plan = bau.state(PLANS[0]); + const isPerYear = bau.state(false); + const addons = bau.state([]); + + const StepYourInfo = stepYourInfo(context); + const StepSelectPlan = stepSelectPlan(context); + const StepAddOns = stepAddOns(context); + const StepSummary = stepSummary(context, { plan, isPerYear, addons }); + const className = css` - border: 1px solid red; max-width: 1000px; - margin: auto; + min-height: 500px; + background-color: var(--background-color); display: flex; - justify-content: center; - align-items: stretch; + margin: 1rem; gap: 0.6rem; border-radius: 0.6rem; padding: 1rem; - + @media (max-width: 600px) { + flex-direction: column; + padding: 0rem; + margin: 0rem; + border-radius: 0px; + } & header { - border: 1px solid blue; background-image: url("./assets/images/bg-sidebar-desktop.svg"); background-size: cover; + flex-shrink: 0; + min-height: 10rem; + @media (max-width: 600px) { + background-image: url("./assets/images/bg-sidebar-mobile.svg"); + } > ul { display: flex; flex-direction: column; + @media (max-width: 600px) { + flex-direction: row; + justify-content: space-around; + } gap: 1rem; - padding: 1rem; + padding-inline: 2rem; + padding-block: 2.4rem; + > li { &.active { - color: red; + .step-number { + background-color: var(--pastel-blue); + color: var(--font-color); + } } display: flex; align-items: center; @@ -42,44 +71,71 @@ export default function (context: Context) { .step-number { font-weight: bold; padding: 1rem; - border: 1px solid white; + border: 1px solid var(--pastel-blue); border-radius: 100%; width: 2rem; height: 2rem; display: grid; place-content: center; } - .step-label { - text-transform: uppercase; - font-size: smaller; - color: var(--font-color-inverse-secondary); - } - .label { - text-transform: uppercase; - font-weight: bold; + .step-labels { + @media (max-width: 600px) { + display: none; + } + .step-label { + text-transform: uppercase; + font-size: smaller; + color: var(--font-color-inverse-secondary); + font-size: 0.875rem; + } + .label { + text-transform: uppercase; + font-weight: 500; + font-size: 0.875rem; + } } } } } - .content { - border: 1px solid blue; - min-width: 375px; - min-height: 400px; - > ul { - > li { - display: none; - &.active { - display: block; - } - form { - display: flex; - flex-direction: column; - gap: 1rem; + & ul.content { + @media (max-width: 600px) { + margin-top: -5rem; + } + background-color: var(--background-color); + margin-inline: 2rem; + border-radius: 0.6rem; + + > li { + display: none; + height: 100%; + &.active { + display: flex; + } + & form { + display: flex; + flex-direction: column; + gap: 1rem; + height: 100%; + padding-inline: 1.5rem; + padding-block: 2rem; + max-width: 500px; + + h1 + p, + small { + color: var(--font-color-secondary); } } } } + & footer { + display: flex; + flex-direction: row-reverse; + justify-content: space-between; + flex-grow: 1; + align-items: flex-end; + } `; + const isCurrentIndex = (index: number) => currentStepState.val == index; const Header = ({ index, label }: any) => @@ -87,7 +143,8 @@ export default function (context: Context) { { class: () => isCurrentIndex(index) && "active" }, div({ class: "step-number" }, index), div( - div({ class: "step-label" }, "Step", index), + { class: "step-labels" }, + div({ class: "step-label" }, "Step ", index), div({ class: "label" }, label) ) ); @@ -95,21 +152,39 @@ export default function (context: Context) { const next = () => { currentStepState.val++; }; - const previous = () => { + const onPrevious = () => { currentStepState.val--; }; const onsubmitYourInfo = (event: HTMLFormElement) => { event.preventDefault(); const payload = Object.fromEntries(new FormData(event.currentTarget)); + console.log(payload); next(); }; - const onsubmitAddon = (event) => { + const onsubmitPlan = (event: HTMLFormElement) => { event.preventDefault(); const payload = Object.fromEntries(new FormData(event.currentTarget)); - alert(JSON.stringify(payload)); - // next(); + const planFound = PLANS.find(({ name }) => name == String(payload.plan)); + if (planFound) { + plan.val = planFound; + } + isPerYear.val = !!payload.yearly; + next(); + }; + + const onsubmitAddon = (event: HTMLFormElement) => { + event.preventDefault(); + const checkboxes = [ + ...event.currentTarget.querySelectorAll('input[name="addons"]:checked'), + ].map(({ value }) => value); + addons.val = ADDONS.filter(({ name }) => checkboxes.includes(name)); + next(); + }; + + const onChangePlan = () => { + currentStepState.val = 2; }; const steps = [ @@ -119,19 +194,30 @@ export default function (context: Context) { }, { label: "Select Plan", - Content: ({}) => StepSelectPlan({ onsubmit: onsubmitAddon }), + Content: ({}) => StepSelectPlan({ onsubmit: onsubmitPlan, onPrevious }), + }, + { + label: "Add on", + Content: ({}) => StepAddOns({ onsubmit: onsubmitAddon, onPrevious }), + }, + { + label: "Summary", + Content: ({}) => StepSummary({ onPrevious, onChangePlan }), }, ]; return () => { return article( { class: className }, - header(ul(steps.map(({ label }, index) => Header({ index, label })))), - div( + header( + ul(steps.map(({ label }, index) => Header({ index: index + 1, label }))) + ), + ul( { class: "content" }, - ul( - steps.map(({ Content, label }, index) => - li({ class: () => isCurrentIndex(index) && "active" }, Content({})) + steps.map(({ Content }, index) => + li( + { class: () => isCurrentIndex(index + 1) && "active" }, + Content({}) ) ) ) diff --git a/examples/multi-step-form/src/stepAddOns.ts b/examples/multi-step-form/src/stepAddOns.ts index 681e37c5..22376c5c 100644 --- a/examples/multi-step-form/src/stepAddOns.ts +++ b/examples/multi-step-form/src/stepAddOns.ts @@ -1,18 +1,83 @@ import { type Context } from "@grucloud/bau-ui/context"; +import { formatCurrency } from "./utils"; +import ADDONS from "./data/addons.json"; export default function (context: Context) { const { bau, css } = context; - const { form, h1, button } = bau.tags; + const { + form, + h1, + button, + footer, + p, + div, + label, + input, + strong, + small, + span, + } = bau.tags; - const className = css` - border: 1px solid red; - `; + const className = css``; - return ({ onsubmit }) => { + const AddOnCheckboxes = () => + div( + { + class: css` + display: grid; + gap: 1rem; + & label { + border: 1px solid var(--color-emphasis-200); + border-radius: 0.5rem; + padding-inline: 1rem; + padding-block: 0.5rem; + display: flex; + align-items: center; + justify-content: space-between; + gap: 1rem; + cursor: pointer; + & .info { + flex-grow: 1; + } + & .price { + font-weight: bold; + font-size: 0.8rem; + color: var(--Purplish); + } + } + `, + }, + ADDONS.map(({ name, description, pricePerMonth }) => + label( + input({ + type: "checkbox", + role: "checkbox", + name: "addons", + value: name, + }), + span({ class: "info" }, p(strong(name)), small(description)), + div( + { class: "price" }, + "+", + formatCurrency(Number(pricePerMonth)), + "/mo" + ) + ) + ) + ); + return ({ onsubmit, onPrevious }: any) => { return form( { class: className, onsubmit }, - h1("Add-Ons"), - button({ type: "submit" }, "Next") + h1("Pick add-ons"), + p("Add-ons help enhance your gaming experience."), + AddOnCheckboxes(), + footer( + button({ type: "submit" }, "Next"), + button( + { type: "button", class: "plain", onclick: onPrevious }, + "Go back" + ) + ) ); }; } diff --git a/examples/multi-step-form/src/stepSelectPlan.ts b/examples/multi-step-form/src/stepSelectPlan.ts index c6e6730d..b4bb8f30 100644 --- a/examples/multi-step-form/src/stepSelectPlan.ts +++ b/examples/multi-step-form/src/stepSelectPlan.ts @@ -1,59 +1,129 @@ import { type Context } from "@grucloud/bau-ui/context"; +import PLANS from "./data/plans.json"; export default function (context: Context) { const { bau, css } = context; - const { - form, - button, - h1, - p, - input, - footer, - div, - label, - span, - strong, - small, - } = bau.tags; + const { form, button, h1, p, input, footer, div, label, strong, small, img } = + bau.tags; - const className = css` - border: 1px solid red; - .radio-group { - display: flex; - justify-content: space-around; - > label { - border: 1px solid red; - padding: 1rem; - } - } - `; + const className = css``; + const payPerYearState = bau.state(false); + const onChangePerYear = (event: any) => { + payPerYearState.val = event.target.checked; + }; - return ({ onsubmit }: any) => { + const RadioGroupPlan = () => + div( + { + class: css` + display: grid; + gap: 1rem; + grid-template-columns: repeat(3, 1fr); + @media (max-width: 800px) { + grid-template-columns: 1fr; + grid-template-rows: repeat(3, 1fr); + } + > label { + flex-basis: 1; + padding: 1rem; + display: flex; + flex-wrap: wrap; + gap: 1rem; + input { + width: 0px; + } + img { + //margin-bottom: 1rem; + } + p { + line-height: 1.2rem; + } + small { + color: var(--font-color-secondary); + } + } + `, + }, + PLANS.map(({ name, image, pricePerMonth }) => + label( + input({ + type: "radio", + name: "plan", + value: name, + required: true, + }), + img({ src: image, alt: "" }), + () => + div( + p(strong(name)), + p( + small( + payPerYearState.val + ? `$${Number(pricePerMonth) * 10}/year` + : `$${pricePerMonth}/mo` + ) + ), + p(small(payPerYearState.val && "2 months free")) + ) + ) + ) + ); + + const MonthYearSwitch = () => + label( + { + class: css` + font-weight: 400; + display: flex; + align-items: center; + justify-content: center; + background-color: var(--background-color-body); + transition: all 0.3s; + padding: 1rem; + font-weight: bold; + font-size: 0.9rem; + &::before { + content: "Monthly"; + color: var(--color-emphasis-900); + } + &::after { + content: "Yearly"; + color: var(--color-emphasis-500); + } + &:has(input:checked) { + &::before { + color: var(--color-emphasis-500); + } + &::after { + color: var(--color-emphasis-900); + } + } + & input { + margin-inline: 0.7rem; + } + `, + }, + input({ + type: "checkbox", + name: "yearly", + role: "switch", + onchange: onChangePerYear, + }) + ); + + return ({ onsubmit, onPrevious }: any) => { return form( { class: className, onsubmit }, h1("Select your plan"), p("You have the option of monthly or yearly billing."), - div( - { class: "radio-group" }, - label( - input({ type: "radio", name: "plan", id: "arcade" }), - p(strong("Arcade")), - p(small("$9/month")) - ), - label( - input({ type: "radio", name: "plan", id: "advanced" }), - p(strong("Advanced")), - p(small("$12/month")) - ), - label( - input({ type: "radio", name: "plan", id: "pro" }), - p(strong("Pro")), - p(small("$15/month")) - ) - ), + RadioGroupPlan(), + MonthYearSwitch(), footer( - button({ type: "button" }, "Go back"), - button({ type: "submit" }, "Next Step") + button({ type: "submit" }, "Next Step"), + button( + { type: "button", class: "plain", onclick: onPrevious }, + "Go back" + ) ) ); }; diff --git a/examples/multi-step-form/src/stepSummary.ts b/examples/multi-step-form/src/stepSummary.ts index f4ed2d2c..7c677a53 100644 --- a/examples/multi-step-form/src/stepSummary.ts +++ b/examples/multi-step-form/src/stepSummary.ts @@ -1,14 +1,153 @@ +import { State } from "@grucloud/bau"; import { type Context } from "@grucloud/bau-ui/context"; +import BN from "bignumber.js"; +import { formatCurrency } from "./utils"; -export default function (context: Context) { +export default function ( + context: Context, + { + plan, + isPerYear, + addons, + }: { plan: State; isPerYear: State; addons: State } +) { const { bau, css } = context; - const { section, h1 } = bau.tags; + const { + form, + h1, + p, + img, + button, + footer, + table, + tbody, + thead, + th, + tr, + td, + a, + small, + strong, + } = bau.tags; + + const convertPrice = (price: string) => { + return isPerYear.val + ? `${formatCurrency(BN(price).times(10).toNumber())}/year` + : `${formatCurrency(Number(price))}/mo`; + }; + + const totalPrice = bau.derive(() => { + const totalAddOn = addons.val.reduce( + (acc, { pricePerMonth }) => acc.plus(pricePerMonth), + BN(0) + ); + const price = BN(plan.val.pricePerMonth).plus(totalAddOn).toString(); + return convertPrice(price); + }); const className = css` - border: 1px solid red; + padding: 2rem; + > table { + a { + color: var(--font-color-secondary); + font-weight: 400; + font-size: 0.85rem; + } + background-color: var(--background-color-body); + border-collapse: collapse; + th, + td { + padding-inline: 1rem; + padding-block: 0.5rem; + } + td { + text-align: right; + } + thead { + border-bottom: 1px solid var(--color-emphasis-100); + td { + font-weight: bold; + } + } + tbody { + padding: 0.5rem; + th { + color: var(--font-color-secondary); + font-weight: 400; + font-size: 0.9rem; + } + td { + font-size: 0.8rem; + } + } + } + .total { + padding-inline: 1rem; + padding-block: 0.5rem; + display: flex; + justify-content: space-between; + } `; + const confirm = () => { + isDone.val = true; + }; + + const FinishingUp = ({ onPrevious, onChangePlan }: any) => + form( + { class: className }, + h1("Finishing up"), + p("Double-check everything looks OK before confirming."), + table( + () => + thead( + th( + p( + plan.val.name, + " (", + () => (isPerYear.val ? "Yearly" : "Monthly"), + ")" + ), + a({ href: "#?step=2", onclick: onChangePlan }, "change") + ), + td(convertPrice(plan.val.pricePerMonth)) + ), + bau.loop(addons, tbody(), ({ name, pricePerMonth }) => + tr(th(name), td(convertPrice(pricePerMonth))) + ) + ), + p( + { class: "total" }, + small("Total (", () => (isPerYear.val ? "per year" : "per month"), ")"), + strong(totalPrice) + ), + + footer( + button({ type: "submit", onclick: confirm }, "Confirm"), + button( + { type: "button", class: "plain", onclick: onPrevious }, + "Go back" + ) + ) + ); + + const ThankYou = () => + form( + { + class: css` + align-items: center; + padding: 2rem; + `, + }, + img({ src: "./assets/images/icon-thank-you.svg", height: 50, width: 50 }), + h1("Thank you!"), + p( + "Thanks for confirming your subscription!We hope you have fun using our platform. If you ever need support, please feel free to email us at support@loremgaming.com." + ) + ); + const isDone = bau.state(false); - return () => { - return section({ class: className }, h1("SSummary")); + return ({ onPrevious, onChangePlan }: any) => { + return () => + isDone.val ? ThankYou() : FinishingUp({ onPrevious, onChangePlan }); }; } diff --git a/examples/multi-step-form/src/stepYourInfo.ts b/examples/multi-step-form/src/stepYourInfo.ts index 0633e888..e545da91 100644 --- a/examples/multi-step-form/src/stepYourInfo.ts +++ b/examples/multi-step-form/src/stepYourInfo.ts @@ -2,31 +2,52 @@ import { type Context } from "@grucloud/bau-ui/context"; export default function (context: Context) { const { bau, css } = context; - const { form, h1, input, label, button } = bau.tags; + const { form, h1, input, label, button, p, footer } = bau.tags; const className = css` - border: 1px solid red; display: flex; flex-direction: column; + h1 { + } + p { + font-size: 0.9rem; + } `; - return ({ onsubmit }) => { + return ({ onsubmit }: any) => { return form( { class: className, onsubmit }, - h1("Your Info"), + h1("Personal Info"), + p("Please provide your name, email address, and phone number."), label( "Name", - input({ name: "name", placeholder: "e.g Stephen King", required: true }) + input({ + type: "text", + name: "name", + placeholder: "e.g Stephen King", + required: true, + }) ), label( "Email Address", - input({ type: "email", placeholder: "e.g stephenking@lorem.com" }) + input({ + type: "email", + required: true, + placeholder: "e.g stephenking@lorem.com", + }) ), label( "Phone Number", - input({ name: "phone", placeholder: "e.g. 1234567890" }) + input({ + type: "text", + required: true, + name: "phone", + pattern: String.raw`\d*`, + minLength: 6, + placeholder: "e.g. 1234567890", + }) ), - button({ type: "submit" }, "Next") + footer(button({ type: "submit" }, "Next")) ); }; } diff --git a/examples/multi-step-form/src/style.css b/examples/multi-step-form/src/style.css index 5bdddd1a..e8e0906e 100644 --- a/examples/multi-step-form/src/style.css +++ b/examples/multi-step-form/src/style.css @@ -21,37 +21,150 @@ */ :root { + --White: hsl(0, 0%, 100%); + --pastel-blue: hsl(228, 100%, 84%); + --Purplish: hsl(243, 100%, 62%); + --Strawberry: hsl(354, 84%, 57%); --color-primary-h: 26; --color-primary-base-s: 100%; --color-primary-l: 55%; - --color-neutral-h: 219; --color-neutral-base-s: 9%; --color-neutral-l: 45%; - - --background-color: #fafafa; --font-size: 1rem; - --font-color-secondary: #68707d; } body { + --font-color-secondary: hsl(231, 11%, 63%); + --font-color: hsl(213, 96%, 18%); + --background-color: white; + --background-color-body: hsl(218, 100%, 97%); + color: var(--font-color); font-family: "Ubuntu", sans-serif; max-width: 100vw; - background-color: var(--background-color); + background-color: var(--background-color-body); font-size: var(--font-size); } main { display: grid; - align-items: center; + place-items: center; min-height: 100vh; + max-width: 940px; + margin: auto; + @media (max-width: 600px) { + place-items: flex-start; + } } ul { list-style: none; } + button { cursor: pointer; - /* background: none; - border: none; - border-radius: 1rem; */ + padding-inline: 1rem; + padding-block: 0.4rem; + border-radius: 0.3rem; + border: transparent; + font-size: 1rem; + max-height: fit-content; +} +button.plain { + color: var(--font-color-secondary); +} + +button[type="submit"] { + color: white; + background-color: var(--color-primary); +} +input[type="text"], +input[type="email"] { + padding-block: 0.7rem; + padding-inline: 0.8rem; + font-size: 1rem; + &::placeholder { + color: var(--color-emphasis-400); + font-weight: 500; + } + border: 1px solid var(--color-emphasis-300); + border-radius: 0.5rem; + + &:user-invalid { + outline: 1px auto var(--color-danger); + } + &:user-valid { + outline: 1px auto var(--color-success); + } +} + +input[role="checkbox"] { + cursor: pointer; + width: 1.2rem; + height: 1.2rem; + border-radius: 0.3rem; + appearance: none; + border: 1px solid var(--color-emphasis-300); + display: grid; + place-content: center; + &::after { + content: ""; + font-size: 1rem; + color: var(--font-color-inverse); + } + &:checked { + background-color: var(--Purplish); + &::after { + content: "✔"; + } + } +} + +input[role="switch"] { + appearance: none; + display: inline-flex; + align-items: center; + background-color: var(--color-primary); + border-radius: 1rem; + width: 3rem; + height: 1.4rem; + &::before { + content: ""; + border-radius: 50%; + background-color: var(--color-emphasis-100); + width: 1rem; + height: 1rem; + margin: 0.2rem; + transition: all 0.3s; + left: 5%; + } + &:checked { + &::before { + background-color: var(--color-emphasis-300); + transform: translateX(1.5rem); + } + } +} + +label { + transition: all 0.2s; + &:has(input[type="radio"]) { + outline: 1px solid var(--color-emphasis-100); + cursor: pointer; + border-radius: 0.5rem; + & input { + opacity: 0; + } + } + &:has(input[type="radio"]:checked, input[role="checkbox"]:checked) { + outline: 2px solid var(--Purplish); + } + &:has(input[type="radio"]:focus) { + outline: 1px solid var(--Purplish); + } + &:has(input[type="text"], input[type="email"]) { + display: inline-flex; + flex-direction: column; + gap: 0.3rem; + font-size: 0.75rem; + } } diff --git a/examples/multi-step-form/src/types.ts b/examples/multi-step-form/src/types.ts new file mode 100644 index 00000000..2d4dc6a9 --- /dev/null +++ b/examples/multi-step-form/src/types.ts @@ -0,0 +1,13 @@ +type Addon = { + // + name: string; + description: string; + pricePerMonth: string; +}; + +type Plan = { + // + name: string; + pricePerMonth: string; + image: string; +}; diff --git a/examples/multi-step-form/src/utils.ts b/examples/multi-step-form/src/utils.ts new file mode 100644 index 00000000..88dc6154 --- /dev/null +++ b/examples/multi-step-form/src/utils.ts @@ -0,0 +1,9 @@ +const locale = "en-US"; +const currency = "USD"; + +export const formatCurrency = (number: number) => + new Intl.NumberFormat(locale, { + style: "currency", + currency, + maximumFractionDigits: 0, + }).format(number); diff --git a/examples/multi-step-form/vite.config.js b/examples/multi-step-form/vite.config.js index 41713bec..eb1cf4c6 100644 --- a/examples/multi-step-form/vite.config.js +++ b/examples/multi-step-form/vite.config.js @@ -2,6 +2,8 @@ import { defineConfig } from "vite"; export default defineConfig(({ command, mode, ssrBuild }) => { return { + base: "/bau/frontendmentor/multi-step-form/", + build: { outDir: "../../dist/frontendmentor/multi-step-form" }, server: { open: true, },