From b850018f047ec748eb03bf14eedb40eb1b3e7e5f Mon Sep 17 00:00:00 2001 From: Frederic Heem Date: Wed, 30 Oct 2024 08:38:06 -0300 Subject: [PATCH] Interactive Rating Component --- .../interactive-rating-component/.gitignore | 24 +++ examples/interactive-rating-component/.npmrc | 2 + .../interactive-rating-component/README.md | 23 +++ .../interactive-rating-component/index.html | 17 ++ .../interactive-rating-component/package.json | 20 +++ .../public/assets/images/favicon-32x32.png | Bin 0 -> 1063 bytes .../public/assets/images/icon-star.svg | 1 + .../assets/images/illustration-thank-you.svg | 1 + .../src/interactiveRating.ts | 168 ++++++++++++++++++ .../interactive-rating-component/src/main.ts | 20 +++ .../src/style.css | 31 ++++ .../src/vite-env.d.ts | 1 + .../tsconfig.json | 23 +++ .../vite.config.js | 9 + 14 files changed, 340 insertions(+) create mode 100644 examples/interactive-rating-component/.gitignore create mode 100644 examples/interactive-rating-component/.npmrc create mode 100644 examples/interactive-rating-component/README.md create mode 100644 examples/interactive-rating-component/index.html create mode 100644 examples/interactive-rating-component/package.json create mode 100644 examples/interactive-rating-component/public/assets/images/favicon-32x32.png create mode 100644 examples/interactive-rating-component/public/assets/images/icon-star.svg create mode 100644 examples/interactive-rating-component/public/assets/images/illustration-thank-you.svg create mode 100644 examples/interactive-rating-component/src/interactiveRating.ts create mode 100644 examples/interactive-rating-component/src/main.ts create mode 100644 examples/interactive-rating-component/src/style.css create mode 100644 examples/interactive-rating-component/src/vite-env.d.ts create mode 100644 examples/interactive-rating-component/tsconfig.json create mode 100644 examples/interactive-rating-component/vite.config.js diff --git a/examples/interactive-rating-component/.gitignore b/examples/interactive-rating-component/.gitignore new file mode 100644 index 00000000..a547bf36 --- /dev/null +++ b/examples/interactive-rating-component/.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/interactive-rating-component/.npmrc b/examples/interactive-rating-component/.npmrc new file mode 100644 index 00000000..6b5f38e8 --- /dev/null +++ b/examples/interactive-rating-component/.npmrc @@ -0,0 +1,2 @@ +save-exact = true +package-lock = false diff --git a/examples/interactive-rating-component/README.md b/examples/interactive-rating-component/README.md new file mode 100644 index 00000000..fc42d8e9 --- /dev/null +++ b/examples/interactive-rating-component/README.md @@ -0,0 +1,23 @@ +# Frontend Mentor Interactive Rating Component + +Here is the implementation in [Bau.js](https://github.com/grucloud/bau) of the [Frontend Mentor Interactive Rating Component code challenge](https://www.frontendmentor.io/challenges/interactive-rating-component-koxpeBUmI) + +## 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/interactive-rating-component/index.html b/examples/interactive-rating-component/index.html new file mode 100644 index 00000000..79fae46c --- /dev/null +++ b/examples/interactive-rating-component/index.html @@ -0,0 +1,17 @@ + + + + + + + Interactive Rating Component | FrontendMentor + + +
+ + + diff --git a/examples/interactive-rating-component/package.json b/examples/interactive-rating-component/package.json new file mode 100644 index 00000000..c2bd4e2b --- /dev/null +++ b/examples/interactive-rating-component/package.json @@ -0,0 +1,20 @@ +{ + "name": "frontendmentor-interactive-rating-component", + "private": true, + "version": "0.85.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc && vite build", + "preview": "vite preview" + }, + "devDependencies": { + "typescript": "^5.0.2", + "vite": "^5.2.11" + }, + "dependencies": { + "@grucloud/bau": "^0.85.0", + "@grucloud/bau-css": "^0.85.0", + "@grucloud/bau-ui": "^0.85.0" + } +} diff --git a/examples/interactive-rating-component/public/assets/images/favicon-32x32.png b/examples/interactive-rating-component/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/interactive-rating-component/public/assets/images/illustration-thank-you.svg b/examples/interactive-rating-component/public/assets/images/illustration-thank-you.svg new file mode 100644 index 00000000..bf22b942 --- /dev/null +++ b/examples/interactive-rating-component/public/assets/images/illustration-thank-you.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/examples/interactive-rating-component/src/interactiveRating.ts b/examples/interactive-rating-component/src/interactiveRating.ts new file mode 100644 index 00000000..665c5c53 --- /dev/null +++ b/examples/interactive-rating-component/src/interactiveRating.ts @@ -0,0 +1,168 @@ +import { type Context } from "@grucloud/bau-ui/context"; + +const ratingKey = "rating"; +const submittedKey = "submitted"; + +export default function (context: Context) { + const { bau, css, window } = context; + const { h1, p, ul, li, button, form, img, picture, section, article, div } = + bau.tags; + + const className = css` + max-width: 400px; + margin: 1rem; + background-color: var(--clr-dark-700); + border-radius: 1em; + .panel { + display: flex; + flex-direction: column; + gap: 1rem; + padding: 2rem; + } + + h1 { + margin: 0; + } + picture { + img { + background-color: var(--clr-dark-500); + border-radius: 50%; + padding: 1rem; + } + } + p { + color: var(--clr-neutral-300); + } + ul { + padding: 0; + display: flex; + align-items: center; + justify-content: space-between; + gap: 1rem; + + li { + list-style: none; + display: inline-flex; + align-items: center; + justify-content: center; + height: 3rem; + width: 3rem; + border-radius: 50%; + background-color: var(--clr-dark-500); + color: var(--clr-neutral-300); + cursor: pointer; + + &:hover { + background-color: var(--clr-dark-300); + color: var(--clr-neutral-100); + } + &.active { + color: white; + background-color: var(--clr-primary); + } + } + } + button { + padding: 1rem 0; + width: 100%; + border-radius: 1rem; + border: none; + cursor: pointer; + background-color: var(--clr-primary); + color: var(--clr-neutral-100); + font-size: 1rem; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.2rem; + transition: all 0.5s; + &:hover { + background-color: var(--clr-neutral-100); + color: var(--clr-primary); + } + } + + .thankyou { + align-items: center; + h1 { + font-size: 2rem; + } + .badge { + color: var(--clr-primary); + background-color: var(--clr-dark-500); + padding: 0.5rem 1rem; + border-radius: 1rem; + } + } + `; + + const search = new URLSearchParams(window.location.search); + const ratingState = bau.state(Number(search.get(ratingKey))); + const submittedState = bau.state(!!search.get(submittedKey)); + + const onRating = (rating: number) => () => { + const search = new URLSearchParams(window.location.search); + search.set(ratingKey, String(rating)); + window.history.pushState( + "", + "", + `?${search.toString()}${window.location.hash}` + ); + ratingState.val = rating; + }; + + const onsubmit = (event: any) => { + event.preventDefault(); + const search = new URLSearchParams(window.location.search); + search.set(submittedKey, "true"); + window.history.replaceState( + "", + "", + `?${search.toString()}${window.location.hash}` + ); + submittedState.val = true; + }; + + const InteractiveRatingContent = () => + form( + { class: "panel", onsubmit }, + picture(img({ src: "./assets/images/icon-star.svg", alt: "star" })), + h1("How did we do?"), + p( + "Please let us know how we did with your support request. All feedback is appreciated to help us improve our offering!" + ), + ul( + Array(5) + .fill("") + .map((_, i) => + li( + { + class: () => i + 1 === ratingState.val && "active", + onclick: onRating(i + 1), + }, + i + 1 + ) + ) + ), + button({ type: "submit" }, "Submit") + ); + + const ThankYou = () => + section( + { class: ["thankyou", "panel"] }, + img({ + src: "./assets/images/illustration-thank-you.svg", + alt: "Thank you", + }), + div({ class: "badge" }, "You selected ", ratingState.val, " out of 5"), + h1("Thank you"), + p( + "We appreciate you taking the time to give a rating. If you ever need more support, don’t hesitate to get in touch!" + ) + ); + + return function interactiveRating() { + return article({ class: className }, () => + submittedState.val ? ThankYou() : InteractiveRatingContent() + ); + }; +} diff --git a/examples/interactive-rating-component/src/main.ts b/examples/interactive-rating-component/src/main.ts new file mode 100644 index 00000000..060bfa23 --- /dev/null +++ b/examples/interactive-rating-component/src/main.ts @@ -0,0 +1,20 @@ +import { createContext, type Context } from "@grucloud/bau-ui/context"; +import interactiveRating from "./interactiveRating"; + +import "./style.css"; + +const context = createContext(); + +const app = (context: Context) => { + const { bau } = context; + const { main } = bau.tags; + + const InteractiveRating = interactiveRating(context); + + return function () { + return main(InteractiveRating()); + }; +}; + +const App = app(context); +document.getElementById("app")?.replaceChildren(App()); diff --git a/examples/interactive-rating-component/src/style.css b/examples/interactive-rating-component/src/style.css new file mode 100644 index 00000000..1408a821 --- /dev/null +++ b/examples/interactive-rating-component/src/style.css @@ -0,0 +1,31 @@ +@import url("https://fonts.googleapis.com/css2?family=Overpass:wght@400;700&display=swap"); + +* { + margin: 0; + box-sizing: border-box; +} + +:root { + --clr-primary: hsl(25, 97%, 53%); + + --clr-neutral-100: hsl(0, 0%, 100%); + --clr-neutral-300: hsl(217, 12%, 63%); + --clr-neutral-500: hsl(216, 12%, 54%); + --clr-dark-300: hsl(213, 19%, 50%); + + --clr-dark-500: hsl(213, 19%, 30%); + + --clr-dark-700: hsl(213, 19%, 18%); + --clr-dark-900: hsl(216, 12%, 8%); +} + +body { + font-family: "Overpass", sans-serif; + min-height: 100vh; + display: grid; + place-items: center; + background-color: var(--clr-dark-900); + color: var(--clr-neutral-100); + @media (max-width: 600px) { + } +} diff --git a/examples/interactive-rating-component/src/vite-env.d.ts b/examples/interactive-rating-component/src/vite-env.d.ts new file mode 100644 index 00000000..11f02fe2 --- /dev/null +++ b/examples/interactive-rating-component/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/examples/interactive-rating-component/tsconfig.json b/examples/interactive-rating-component/tsconfig.json new file mode 100644 index 00000000..75abdef2 --- /dev/null +++ b/examples/interactive-rating-component/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/interactive-rating-component/vite.config.js b/examples/interactive-rating-component/vite.config.js new file mode 100644 index 00000000..41713bec --- /dev/null +++ b/examples/interactive-rating-component/vite.config.js @@ -0,0 +1,9 @@ +import { defineConfig } from "vite"; + +export default defineConfig(({ command, mode, ssrBuild }) => { + return { + server: { + open: true, + }, + }; +});