diff --git a/examples/age-calculator-app/.gitignore b/examples/age-calculator-app/.gitignore new file mode 100644 index 00000000..a547bf36 --- /dev/null +++ b/examples/age-calculator-app/.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/age-calculator-app/.npmrc b/examples/age-calculator-app/.npmrc new file mode 100644 index 00000000..6b5f38e8 --- /dev/null +++ b/examples/age-calculator-app/.npmrc @@ -0,0 +1,2 @@ +save-exact = true +package-lock = false diff --git a/examples/age-calculator-app/README.md b/examples/age-calculator-app/README.md new file mode 100644 index 00000000..d20b46f4 --- /dev/null +++ b/examples/age-calculator-app/README.md @@ -0,0 +1,23 @@ +# Frontend Mentor Age Calculator App + +Here is the implementation in [Bau.js](https://github.com/grucloud/bau) of the [Frontend MentorAge Calculator App code challenge](https://www.frontendmentor.io/challenges/age-calculator-app-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/age-calculator-app/index.html b/examples/age-calculator-app/index.html new file mode 100644 index 00000000..b3b4167c --- /dev/null +++ b/examples/age-calculator-app/index.html @@ -0,0 +1,18 @@ + + + + + + + Age Calculator App | FrontendMentor + + + +
+ + + diff --git a/examples/age-calculator-app/package.json b/examples/age-calculator-app/package.json new file mode 100644 index 00000000..7713f940 --- /dev/null +++ b/examples/age-calculator-app/package.json @@ -0,0 +1,23 @@ +{ + "name": "frontendmentor-age-calculator-app", + "homepage": "https://grucloud.github.io/bau/frontendmentor/age-calculator-app/", + "private": true, + "version": "0.97.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.97.0", + "@grucloud/bau-css": "^0.97.0", + "@grucloud/bau-ui": "^0.97.0" + } +} diff --git a/examples/age-calculator-app/public/assets/images/favicon-32x32.png b/examples/age-calculator-app/public/assets/images/favicon-32x32.png new file mode 100644 index 00000000..1e2df7f0 Binary files /dev/null and b/examples/age-calculator-app/public/assets/images/favicon-32x32.png differ diff --git a/examples/age-calculator-app/public/assets/images/icon-arrow.svg b/examples/age-calculator-app/public/assets/images/icon-arrow.svg new file mode 100644 index 00000000..ea0a8b8a --- /dev/null +++ b/examples/age-calculator-app/public/assets/images/icon-arrow.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/examples/age-calculator-app/src/ageCalculator.ts b/examples/age-calculator-app/src/ageCalculator.ts new file mode 100644 index 00000000..f54f698e --- /dev/null +++ b/examples/age-calculator-app/src/ageCalculator.ts @@ -0,0 +1,173 @@ +import { type Context } from "@grucloud/bau-ui/context"; + +export default function (context: Context) { + const { bau, css } = context; + const { p, label, input, form, i, img, hr, span, section, button } = bau.tags; + + const yearsState = bau.state("--"); + const monthsState = bau.state("--"); + const daysState = bau.state("--"); + + const className = css` + display: grid; + gap: 0.5rem; + padding: 2rem; + border-radius: 1rem 1rem 7rem 1rem; + background-color: var(--White); + min-width: 600px; + @media (max-width: 600px) { + min-width: unset; + } + .dob { + display: flex; + gap: 1rem; + & label { + display: grid; + text-transform: uppercase; + font-size: 0.7rem; + font-weight: 600; + color: var(--Smokey-grey); + letter-spacing: 0.1rem; + gap: 0.3rem; + + & input { + @media (min-width: 600px) { + min-width: 5rem; + } + padding: 1rem; + border-radius: 0.5rem; + border: 1px solid var(--Light-grey); + font-size: 1.2rem; + font-weight: 600; + &:focus { + outline: 1px auto var(--color-primary); + } + } + } + } + .submit { + display: flex; + align-items: center; + + & hr { + height: 1px; + background-color: var(--Light-grey); + border: none; + width: 100%; + @media (min-width: 600px) { + &:last-child { + display: none; + } + } + } + & button { + border: none; + border-radius: 50%; + background-color: var(--color-primary); + cursor: pointer; + @media (max-width: 600px) { + img { + width: 42px; + height: 42px; + } + } + } + } + + .age-result { + font-size: 32px; + font-weight: 800; + font-style: italic; + @media (min-width: 600px) { + font-size: 60px; + } + .timeunit { + color: var(--color-primary); + } + } + `; + + const onsubmit = (event: any) => { + event.preventDefault(); + const { day, month, year } = Object.fromEntries(new FormData(event.target)); + console.log(day, month, year); + + const dob = Date.parse(`${year}-${month}-${day}`); + const now = Date.now(); + let age = (now - dob) / 1000; + + const secondsInYear = 31536000; + const secondsInMonth = 2628000; + const secondsInDay = 86400; + + yearsState.val = String(Math.floor(age / secondsInYear)); + let remainingSeconds = age % secondsInYear; + + monthsState.val = String(Math.floor(remainingSeconds / secondsInMonth)); + remainingSeconds %= secondsInMonth; + + daysState.val = String(Math.floor(remainingSeconds / secondsInDay)); + }; + + return () => { + return form( + { class: className, onsubmit }, + section( + { class: "dob" }, + label( + "Day", + input({ + type: "number", + name: "day", + placeholder: "DD", + min: 1, + max: 31, + required: true, + }) + ), + label( + "Month", + input({ + type: "number", + name: "month", + placeholder: "MM", + min: 1, + max: 12, + required: true, + }) + ), + label( + "Year", + input({ + type: "number", + name: "year", + placeholder: "YYYY", + min: 1900, + max: new Date().getFullYear(), + required: true, + }) + ) + ), + section( + { class: "submit" }, + hr(), + button( + { type: "submit" }, + img({ + src: "./assets/images/icon-arrow.svg", + alt: "submit", + height: 80, + width: 80, + }) + ), + hr() + ), + section( + { class: "age-result" }, + p(span({ class: "timeunit" }, yearsState), i(" years")), + p(span({ class: "timeunit" }, monthsState), i(" months")), + p(span({ class: "timeunit" }, daysState), i(" days")) + ) + ); + }; +} diff --git a/examples/age-calculator-app/src/main.ts b/examples/age-calculator-app/src/main.ts new file mode 100644 index 00000000..e4b69cda --- /dev/null +++ b/examples/age-calculator-app/src/main.ts @@ -0,0 +1,18 @@ +import { createContext, type Context } from "@grucloud/bau-ui/context"; +import ageCalculator from "./ageCalculator"; +import "./style.css"; + +const context = createContext(); + +const app = (context: Context) => { + const { bau } = context; + const { main } = bau.tags; + const AgeCalculator = ageCalculator(context); + + return function () { + return main(AgeCalculator()); + }; +}; + +const App = app(context); +document.getElementById("app")?.replaceChildren(App()); diff --git a/examples/age-calculator-app/src/style.css b/examples/age-calculator-app/src/style.css new file mode 100644 index 00000000..82df585a --- /dev/null +++ b/examples/age-calculator-app/src/style.css @@ -0,0 +1,50 @@ +@import url("https://fonts.googleapis.com/css2?family=Poppins:ital,wght@0,700;1,400;1,800&display=swap"); + +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +:root { + --Purple: hsl(259, 100%, 65%); + --Light-red: hsl(0, 100%, 67%); + + --White: hsl(0, 0%, 100%); + --Off-white: hsl(0, 0%, 94%); + --Light-grey: hsl(0, 0%, 86%); + --Smokey-grey: hsl(0, 1%, 44%); + --Off-black: hsl(0, 0%, 8%); + + --color-primary-h: 259; + --color-primary-base-s: 100%; + --color-primary-l: 65%; + + --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%; + + --font-color-primary: white; + --background-color: white; +} +body { + background-color: var(--Light-grey); + font: 400 16px/1.6 "Poppins", sans-serif; + min-height: 100vh; + display: grid; + place-content: center; +} + +input[type="number"]::-webkit-inner-spin-button { + -webkit-appearance: none; +} + +input[type="number"] { + -moz-appearance: textfield; + appearance: textfield; + margin: 0; +} diff --git a/examples/age-calculator-app/src/vite-env.d.ts b/examples/age-calculator-app/src/vite-env.d.ts new file mode 100644 index 00000000..11f02fe2 --- /dev/null +++ b/examples/age-calculator-app/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/examples/age-calculator-app/tsconfig.json b/examples/age-calculator-app/tsconfig.json new file mode 100644 index 00000000..75abdef2 --- /dev/null +++ b/examples/age-calculator-app/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/age-calculator-app/vite.config.js b/examples/age-calculator-app/vite.config.js new file mode 100644 index 00000000..a378cfc4 --- /dev/null +++ b/examples/age-calculator-app/vite.config.js @@ -0,0 +1,11 @@ +import { defineConfig } from "vite"; + +export default defineConfig(({ command, mode, ssrBuild }) => { + return { + base: "/bau/frontendmentor/age-calculator-app/", + build: { outDir: "../../dist/frontendmentor/age-calculator-app" }, + server: { + open: true, + }, + }; +}); diff --git a/examples/newsletter-signup-form/src/newsletterSignupForm.ts b/examples/newsletter-signup-form/src/newsletterSignupForm.ts index 56e7faf6..d790b79f 100644 --- a/examples/newsletter-signup-form/src/newsletterSignupForm.ts +++ b/examples/newsletter-signup-form/src/newsletterSignupForm.ts @@ -30,7 +30,7 @@ export default function (context: Context) { grid-template-areas: "image" "form"; border-radius: 0; } - padding: 1rem; + padding-inline: 1rem; margin-inline: 1rem; background-color: var(--White); border-radius: 2rem;