From 1c3e45df851e808fd4a6d852d4303b2894ade089 Mon Sep 17 00:00:00 2001 From: Frederic Heem Date: Thu, 5 Dec 2024 11:53:11 -0300 Subject: [PATCH] newsletter signup form --- examples/README.md | 40 ++-- examples/newsletter-signup-form/.gitignore | 24 +++ examples/newsletter-signup-form/.npmrc | 2 + examples/newsletter-signup-form/README.md | 23 ++ examples/newsletter-signup-form/index.html | 18 ++ examples/newsletter-signup-form/package.json | 23 ++ .../public/assets/images/favicon-32x32.png | Bin 0 -> 1063 bytes .../public/assets/images/icon-list.svg | 1 + .../public/assets/images/icon-success.svg | 1 + .../images/illustration-sign-up-desktop.svg | 1 + .../images/illustration-sign-up-mobile.svg | 1 + examples/newsletter-signup-form/src/main.ts | 18 ++ .../src/newsletterSignupForm.ts | 201 ++++++++++++++++++ examples/newsletter-signup-form/src/style.css | 54 +++++ .../newsletter-signup-form/src/vite-env.d.ts | 1 + examples/newsletter-signup-form/tsconfig.json | 23 ++ .../newsletter-signup-form/vite.config.js | 11 + 17 files changed, 423 insertions(+), 19 deletions(-) create mode 100644 examples/newsletter-signup-form/.gitignore create mode 100644 examples/newsletter-signup-form/.npmrc create mode 100644 examples/newsletter-signup-form/README.md create mode 100644 examples/newsletter-signup-form/index.html create mode 100644 examples/newsletter-signup-form/package.json create mode 100644 examples/newsletter-signup-form/public/assets/images/favicon-32x32.png create mode 100644 examples/newsletter-signup-form/public/assets/images/icon-list.svg create mode 100644 examples/newsletter-signup-form/public/assets/images/icon-success.svg create mode 100644 examples/newsletter-signup-form/public/assets/images/illustration-sign-up-desktop.svg create mode 100644 examples/newsletter-signup-form/public/assets/images/illustration-sign-up-mobile.svg create mode 100644 examples/newsletter-signup-form/src/main.ts create mode 100644 examples/newsletter-signup-form/src/newsletterSignupForm.ts create mode 100644 examples/newsletter-signup-form/src/style.css create mode 100644 examples/newsletter-signup-form/src/vite-env.d.ts create mode 100644 examples/newsletter-signup-form/tsconfig.json create mode 100644 examples/newsletter-signup-form/vite.config.js 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 0000000000000000000000000000000000000000..1e2df7f089f46dd930239e418bf13e8e4c1cca0f GIT binary patch literal 1063 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE3?yBabR7dyEa{HEjtmSN`?>!lvVtU&J%W50 z7^>757#dm_7=8hT8eT9klo~KFyh>nTu$sZZAYL$MSD+081EY0-Plzi}!G9Wnr(eF` z|M+F+!xzhMK3;zF(cgdn_B?v|`rD7+fB)V2@MY=EN9|{BEWPpQ{f}RtfBl|!^+Ea3 z%d2iZe)i@2x9`6{eg66V&!4ULpSPU4KJ)Uu^RM22`~BzO$wz&&PyPA(@7<4I`yRjQ zJAZr1#XB#){D-o5+r^Vjdc3)fsquh@R{^yA;Z|8C!Zw`1zDvzMNK{QUFr z^G~yvU&yK5nOnE3q5ts2d8Z#d`S9=G|MwriU%2{m%ii0owp@Sy>dXGuA$NemCsPvS z7YvLEt(>(tKmBDkpML+hROc(RL;oBPGFiO5x8~1ErS?rgb&N^g?k~Ih+L^ zk;M!Q+`=Ht$S`Y;1W=H@#M9T6{SlWiKeL>x)y)*39#2mf#}JFtt&`J}n+ycniko-@ z3;26vOWyCzeS7I$?C$UX{_kIKV}WAF+24(KRQ;Wbm=(hXgpO=b(eBb|byZ~P5M3dt zyDB-nDx`Zw9LoWN-GrY`kHxRfy}Q3KfBxRdclJCv_%N_mL-x0j+x*I+s%JIY&vgg3Y zTes?8j%DMbh5PocynFBNZJq}$w`Q+tKfe6=^y}tkkv_Zb+uQBmSHB|O-+X(*4%OrO zQF%8%OSj*z`t$7X`oHyz%ReTa40^8r7Z}c}C9V-ADTyViR>?)FK#IZ0z|cb1&_LJF zGQ`l-%D~vl*g)IB$jZQAv*1E06b-rgDVb@NxHTN|kz5AU01WSllAy$Lg@U5|w9K4T zg_6pGRE5lfl4J&kiaC!z@o*G|X=t4CKYhmYX%GXmGPhnbx3IFX_hb=fVFi~4lfx;@ u%9}$JPT#n4;>ejJGDp}?H+U@Y(qnifE?Dx($#g2v3I \ 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, + }, + }; +});