diff --git a/examples/README.md b/examples/README.md index 532f3702..0fbbf946 100644 --- a/examples/README.md +++ b/examples/README.md @@ -7,24 +7,26 @@ Here you can find examples written using _bau_. [Frontend Mentor](https://frontendmentor.io) is an online platform that provides real-world front-end development challenges designed to help developers practice and improve their coding skills. It offers a wide range of project-based challenges, from beginner to advanced levels, that simulate real-world design-to-code scenarios. Below is a list of projects implemented using _Bau_. -| Name | Live | Code | + +| Name | Live | Code | | ----------------------------- | ------------------------------------------------------------------------------------ | --------------------------------------- | -| Bento Grid | [live](https://grucloud.github.io/bau/frontendmentor/bento-grid/) | [code](./bento-grid) | -| Blog Preview Card | [live](https://grucloud.github.io/bau/frontendmentor/blog-preview-card/) | [code](./blog-preview-card) | -| Calculator | [live](https://grucloud.github.io/bau/frontendmentor/calculator/) | [code](./calculator) | -| Contact Form | [live](https://grucloud.github.io/bau/frontendmentor/contact-form/) | [code](./contact-form) | -| E-commerce Product Page | [live](https://grucloud.github.io/bau/frontendmentor/e-commerce-product-page/) | [code](./e-commerce-product-page) | -| Faq Accordion | [live](https://grucloud.github.io/bau/frontendmentor/faq-accordion/) | [code](./faq-accordion) | -| Interactive Comments Section | [live](https://grucloud.github.io/bau/frontendmentor/interactive-comments-section/) | [code](./interactive-comments-section) | -| Interactive Rating Component | [live](https://grucloud.github.io/bau/frontendmentor/interactive-rating-component/) | [code](./interactive-rating-component) | -| IP Address Tracker | [live](https://grucloud.github.io/bau/frontendmentor/ip-address-tracker/) | [code](./ip-address-tracker) | -| Job Listing With Filtering | [live](https://grucloud.github.io/bau/frontendmentor/job-listings-with-filtering/) | [code](./job-listings-with-filtering) | -| Launch Countdown Timer | [live](https://grucloud.github.io/bau/frontendmentor/launch-countdown-timer/) | [code](./launch-countdown-timer) | +| Bento Grid | [live](https://grucloud.github.io/bau/frontendmentor/bento-grid/) | [code](./bento-grid) | +| Blog Preview Card | [live](https://grucloud.github.io/bau/frontendmentor/blog-preview-card/) | [code](./blog-preview-card) | +| Calculator | [live](https://grucloud.github.io/bau/frontendmentor/calculator/) | [code](./calculator) | +| Contact Form | [live](https://grucloud.github.io/bau/frontendmentor/contact-form/) | [code](./contact-form) | +| E-commerce Product Page | [live](https://grucloud.github.io/bau/frontendmentor/e-commerce-product-page/) | [code](./e-commerce-product-page) | +| Faq Accordion | [live](https://grucloud.github.io/bau/frontendmentor/faq-accordion/) | [code](./faq-accordion) | +| Interactive Comments Section | [live](https://grucloud.github.io/bau/frontendmentor/interactive-comments-section/) | [code](./interactive-comments-section) | +| Interactive Rating Component | [live](https://grucloud.github.io/bau/frontendmentor/interactive-rating-component/) | [code](./interactive-rating-component) | +| IP Address Tracker | [live](https://grucloud.github.io/bau/frontendmentor/ip-address-tracker/) | [code](./ip-address-tracker) | +| Job Listing With Filtering | [live](https://grucloud.github.io/bau/frontendmentor/job-listings-with-filtering/) | [code](./job-listings-with-filtering) | +| Launch Countdown Timer | [live](https://grucloud.github.io/bau/frontendmentor/launch-countdown-timer/) | [code](./launch-countdown-timer) | +| Newsletter Signup Form | [live](https://grucloud.github.io/bau/frontendmentor/newsletter-signup-form/) | [code](./newsletter-signup-form) | | Mortgage Repayment Calculator | [live](https://grucloud.github.io/bau/frontendmentor/mortgage-repayment-calculator/) | [code](./mortgage-repayment-calculator) | -| Multi Step Form | [live](https://grucloud.github.io/bau/frontendmentor/multi-step-form/) | [code](./multi-step-form) | -| Product List Cart | [live](https://grucloud.github.io/bau/frontendmentor/product-list-cart/) | [code](./product-list-cart) | -| Product Preview Card | [live](https://grucloud.github.io/bau/frontendmentor/product-preview-card/) | [code](./product-preview-card) | -| Recipe Page | [live](https://grucloud.github.io/bau/frontendmentor/recipePage/) | [code](./recipePage) | -| Rest Countries | [live](https://grucloud.github.io/bau/frontendmentor/rest-countries/) | [code](./rest-countries) | -| Result Summary Component | [live](https://grucloud.github.io/bau/frontendmentor/result-summary-component/) | [code](./result-summary-component) | -| Social Link Profile | [live](https://grucloud.github.io/bau/frontendmentor/social-link-profile/) | [code](./social-link-profile) | +| Multi Step Form | [live](https://grucloud.github.io/bau/frontendmentor/multi-step-form/) | [code](./multi-step-form) | +| Product List Cart | [live](https://grucloud.github.io/bau/frontendmentor/product-list-cart/) | [code](./product-list-cart) | +| Product Preview Card | [live](https://grucloud.github.io/bau/frontendmentor/product-preview-card/) | [code](./product-preview-card) | +| Recipe Page | [live](https://grucloud.github.io/bau/frontendmentor/recipePage/) | [code](./recipePage) | +| Rest Countries | [live](https://grucloud.github.io/bau/frontendmentor/rest-countries/) | [code](./rest-countries) | +| Result Summary Component | [live](https://grucloud.github.io/bau/frontendmentor/result-summary-component/) | [code](./result-summary-component) | +| Social Link Profile | [live](https://grucloud.github.io/bau/frontendmentor/social-link-profile/) | [code](./social-link-profile) | diff --git a/examples/newsletter-signup-form/.gitignore b/examples/newsletter-signup-form/.gitignore new file mode 100644 index 00000000..a547bf36 --- /dev/null +++ b/examples/newsletter-signup-form/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/examples/newsletter-signup-form/.npmrc b/examples/newsletter-signup-form/.npmrc new file mode 100644 index 00000000..6b5f38e8 --- /dev/null +++ b/examples/newsletter-signup-form/.npmrc @@ -0,0 +1,2 @@ +save-exact = true +package-lock = false diff --git a/examples/newsletter-signup-form/README.md b/examples/newsletter-signup-form/README.md new file mode 100644 index 00000000..e3cba5e7 --- /dev/null +++ b/examples/newsletter-signup-form/README.md @@ -0,0 +1,23 @@ +# Frontend Mentor Newsletter Signup Form + +Here is the implementation in [Bau.js](https://github.com/grucloud/bau) of the [Frontend MentorNewsletter Signup Form code challenge](https://www.frontendmentor.io/challenges/newsletter-signup-form-with-success-message-3FC1AZbNrv/hub) + +## Workflow + +Install the dependencies: + +```sh +npm install +``` + +Start a development server: + +```sh +npm run dev +``` + +Build a production version: + +```sh +npm run build +``` diff --git a/examples/newsletter-signup-form/index.html b/examples/newsletter-signup-form/index.html new file mode 100644 index 00000000..80e2cfad --- /dev/null +++ b/examples/newsletter-signup-form/index.html @@ -0,0 +1,18 @@ + + + + + + + Newsletter Signup Form | FrontendMentor + + + +
+ + + diff --git a/examples/newsletter-signup-form/package.json b/examples/newsletter-signup-form/package.json new file mode 100644 index 00000000..d8e02897 --- /dev/null +++ b/examples/newsletter-signup-form/package.json @@ -0,0 +1,23 @@ +{ + "name": "frontendmentor-newsletter-signup-form", + "homepage": "https://grucloud.github.io/bau/frontendmentor/newsletter-signup-form/", + "private": true, + "version": "0.95.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc && vite build", + "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.95.0", + "@grucloud/bau-css": "^0.95.0", + "@grucloud/bau-ui": "^0.95.0" + } +} diff --git a/examples/newsletter-signup-form/public/assets/images/favicon-32x32.png b/examples/newsletter-signup-form/public/assets/images/favicon-32x32.png new file mode 100644 index 00000000..1e2df7f0 Binary files /dev/null and b/examples/newsletter-signup-form/public/assets/images/favicon-32x32.png differ diff --git a/examples/newsletter-signup-form/public/assets/images/icon-list.svg b/examples/newsletter-signup-form/public/assets/images/icon-list.svg new file mode 100644 index 00000000..8e1ada38 --- /dev/null +++ b/examples/newsletter-signup-form/public/assets/images/icon-list.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/examples/newsletter-signup-form/public/assets/images/icon-success.svg b/examples/newsletter-signup-form/public/assets/images/icon-success.svg new file mode 100644 index 00000000..258cab71 --- /dev/null +++ b/examples/newsletter-signup-form/public/assets/images/icon-success.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/examples/newsletter-signup-form/public/assets/images/illustration-sign-up-desktop.svg b/examples/newsletter-signup-form/public/assets/images/illustration-sign-up-desktop.svg new file mode 100644 index 00000000..d5d9895b --- /dev/null +++ b/examples/newsletter-signup-form/public/assets/images/illustration-sign-up-desktop.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/examples/newsletter-signup-form/public/assets/images/illustration-sign-up-mobile.svg b/examples/newsletter-signup-form/public/assets/images/illustration-sign-up-mobile.svg new file mode 100644 index 00000000..3ad16b9d --- /dev/null +++ b/examples/newsletter-signup-form/public/assets/images/illustration-sign-up-mobile.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/examples/newsletter-signup-form/src/main.ts b/examples/newsletter-signup-form/src/main.ts new file mode 100644 index 00000000..2a23813e --- /dev/null +++ b/examples/newsletter-signup-form/src/main.ts @@ -0,0 +1,18 @@ +import { createContext, type Context } from "@grucloud/bau-ui/context"; +import newsletterSignupForm from "./newsletterSignupForm"; +import "./style.css"; + +const context = createContext(); + +const app = (context: Context) => { + const { bau } = context; + const { main } = bau.tags; + const NewsletterSignupForm = newsletterSignupForm(context); + + return function () { + return main(NewsletterSignupForm()); + }; +}; + +const App = app(context); +document.getElementById("app")?.replaceChildren(App()); diff --git a/examples/newsletter-signup-form/src/newsletterSignupForm.ts b/examples/newsletter-signup-form/src/newsletterSignupForm.ts new file mode 100644 index 00000000..56e7faf6 --- /dev/null +++ b/examples/newsletter-signup-form/src/newsletterSignupForm.ts @@ -0,0 +1,201 @@ +import { type Context } from "@grucloud/bau-ui/context"; + +export default function (context: Context) { + const { bau, css } = context; + const { + div, + form, + h1, + p, + label, + input, + button, + article, + aside, + ul, + li, + img, + span, + strong, + } = bau.tags; + + const doneState = bau.state(false); + const emailState = bau.state(""); + + const className = css` + display: grid; + grid-template-columns: 1fr min-content; + grid-template-areas: "form image"; + @media (max-width: 900px) { + grid-template-areas: "image" "form"; + border-radius: 0; + } + padding: 1rem; + margin-inline: 1rem; + background-color: var(--White); + border-radius: 2rem; + > aside { + grid-area: image; + width: 100%; + & img { + display: block; + margin: auto; + @media (max-width: 900px) { + background-image: url("./assets/images/illustration-sign-up-mobile.svg"); + background-repeat: no-repeat; + background-position: center; + width: 375px; + height: 284px; + } + } + } + > form { + grid-area: form; + margin: auto; + padding-inline: 2rem; + padding-block: 1rem; + max-width: 500px; + display: grid; + gap: 1rem; + > ul { + > li { + display: flex; + align-items: center; + gap: 0.7rem; + padding-block: 0.3rem; + position: relative; + &::before { + height: 21px; + width: 21px; + background-image: url("./assets/images/icon-list.svg"); + content: ""; + } + } + } + & h1 { + font-size: 2.5rem; + text-align: center; + } + > label { + display: grid; + font-size: 0.8rem; + font-weight: bold; + gap: 0.5rem; + > div { + display: flex; + justify-content: space-between; + .error { + color: var(--color-danger); + display: none; + } + } + &:has(> input:user-invalid) { + .error { + display: block; + color: var(--color-danger); + } + } + > input { + height: 56px; + padding-inline: 1rem; + border-radius: 0.5rem; + color: inherit; + border: 1px solid var(--grey); + &:focus { + outline: 1px auto var(--color-primary); + } + &:user-invalid { + outline: 1px auto var(--color-danger); + background-color: var(--color-danger-lightest); + color: var(--color-danger); + } + &:user-valid { + outline: 1px auto var(--color-success); + } + } + } + } + `; + + const onsubmit = (event: any) => { + event.preventDefault(); + const { email } = Object.fromEntries(new FormData(event.target)); + emailState.val = String(email); + doneState.val = true; + }; + + const Thankyou = ({}) => + form( + { + onsubmit: (event: any) => { + event.preventDefault(); + }, + class: css` + background-color: var(--White); + max-width: 400px; + padding: 3rem; + margin-inline: 1rem; + border-radius: 0.5rem; + display: grid; + gap: 2rem; + & img { + } + `, + }, + img({ src: "./assets/images/icon-success.svg", width: 64, height: 64 }), + h1("Thanks for subscribing!"), + p( + "A confirmation email has been sent to ", + strong(emailState.val), + ". Please open it and click the button inside to confirm your subscription." + ), + button( + { + type: "submit", + onclick: () => { + doneState.val = false; + }, + }, + "Dismiss message" + ) + ); + + const Newsletter = () => + article( + { class: className }, + form( + { onsubmit }, + h1("Stay updated!"), + p("Join 60,000+ product managers receiving monthly updates on:"), + ul( + li("Product discovery and building what matters"), + li("Measuring to ensure updates are a success"), + li("And much more!") + ), + label( + div( + "Email address", + span({ class: "error" }, "Valid email required") + ), + input({ + name: "email", + type: "email", + placeholder: "email@company.com", + required: true, + }) + ), + p(button({ type: "submit" }, "Subscribe to monthly newsletter")) + ), + aside( + img({ + width: 400, + height: 593, + src: "./assets/images/illustration-sign-up-desktop.svg", + }) + ) + ); + + return () => { + return div(() => (doneState.val ? Thankyou({}) : Newsletter())); + }; +} diff --git a/examples/newsletter-signup-form/src/style.css b/examples/newsletter-signup-form/src/style.css new file mode 100644 index 00000000..535260b4 --- /dev/null +++ b/examples/newsletter-signup-form/src/style.css @@ -0,0 +1,54 @@ +@import url("https://fonts.googleapis.com/css2?family=Roboto:wght@400;700&display=swap"); +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +:root { + --tomato: hsl(4, 100%, 67%); + --dark-slate-grey: hsl(234, 29%, 20%); + --charcoal-grey: hsl(235, 18%, 26%); + --grey: hsl(231, 7%, 60%); + --White: hsl(0, 0%, 100%); + --color-primary-h: 4; + --color-primary-base-s: 100%; + --color-primary-l: 67%; + + --color-neutral-h: 245; + --color-neutral-base-s: 18%; + --color-neutral-l: 26%; + + --color-danger-h: 358; + --color-danger-base-s: 79%; + --color-danger-l: 66%; + + --color-danger-lightest: hsl(358, 79%, calc(66% * 1.4)); + + --font-color-primary: white; + --background-color: white; +} +body { + background-color: hsl(234, 29%, 20%); + font: 400 16px/1.6 "Roboto", sans-serif; + min-height: 100vh; + display: grid; + place-content: center; + color: var(--dark-slate-grey); +} + +button[type="submit"] { + height: 56px; + width: 100%; + border-radius: 0.5rem; + background-color: var(--dark-slate-grey); + color: var(--font-color-primary); + font-weight: 700; + font-size: 1rem; + cursor: pointer; + border: none; + transition: all 0.3s; + &:hover { + background-image: linear-gradient(45deg, #ff527a, #fd6937); + } +} diff --git a/examples/newsletter-signup-form/src/vite-env.d.ts b/examples/newsletter-signup-form/src/vite-env.d.ts new file mode 100644 index 00000000..11f02fe2 --- /dev/null +++ b/examples/newsletter-signup-form/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/examples/newsletter-signup-form/tsconfig.json b/examples/newsletter-signup-form/tsconfig.json new file mode 100644 index 00000000..75abdef2 --- /dev/null +++ b/examples/newsletter-signup-form/tsconfig.json @@ -0,0 +1,23 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "module": "ESNext", + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src"] +} diff --git a/examples/newsletter-signup-form/vite.config.js b/examples/newsletter-signup-form/vite.config.js new file mode 100644 index 00000000..21c887cd --- /dev/null +++ b/examples/newsletter-signup-form/vite.config.js @@ -0,0 +1,11 @@ +import { defineConfig } from "vite"; + +export default defineConfig(({ command, mode, ssrBuild }) => { + return { + base: "/bau/frontendmentor/newsletter-signup-form/", + build: { outDir: "../../dist/frontendmentor/newsletter-signup-form" }, + server: { + open: true, + }, + }; +});