From c7d2433a5fd5ab24e0c9491320567036913c215c Mon Sep 17 00:00:00 2001 From: Michael Brusegard <56915010+michaelbrusegard@users.noreply.github.com> Date: Thu, 24 Oct 2024 16:47:05 +0200 Subject: [PATCH] Reapply "Reapply "Merge branch 'main' into dev"" This reverts commit 612c43fd3072832ff81665a47b4c01f341ecd8d2. --- .github/workflows/code-quality.yml | 7 +- .github/workflows/deploy-script.yml | 2 +- .gitignore | 3 + .husky/_/pre-commit | 60 ------ .husky/_/prepare-commit-msg | 60 ------ biome.json | 3 + bun.lockb | Bin 230727 -> 223963 bytes global.d.ts | 2 - lefthook.yml | 2 +- lighthouserc.cjs | 66 +++++++ lighthouserc.yml | 37 ---- messages/en.json | 54 +++++- messages/no.json | 54 +++++- next.config.js | 8 +- package.json | 18 +- postcss.config.cjs => postcss.config.js | 3 +- .../news/{(header) => (main)}/layout.tsx | 0 .../news/{(header) => (main)}/loading.tsx | 2 +- .../news/{(header) => (main)}/page.tsx | 12 +- .../(default)/news/[article]/page.tsx | 4 +- .../(default)/storage/(main)/layout.tsx | 87 +++++++++ .../(default)/storage/(main)/loading.tsx | 16 ++ .../(default)/storage/(main)/page.tsx | 54 ++++++ src/app/[locale]/(default)/storage/layout.tsx | 38 ---- .../[locale]/(default)/storage/loading.tsx | 24 --- .../[locale]/(default)/storage/new/page.tsx | 3 + src/app/[locale]/(default)/storage/page.tsx | 156 --------------- .../storage/shopping-cart/layout.tsx | 38 ++++ .../storage/shopping-cart/loading.tsx | 23 +++ .../(default)/storage/shopping-cart/page.tsx | 54 ++++++ src/app/[locale]/error.tsx | 44 +++++ src/app/[locale]/not-found.tsx | 22 +++ src/app/not-found.tsx | 13 +- .../composites/CategorySelector.tsx | 46 +++++ src/components/composites/ConfirmDialog.tsx | 63 ++++++ .../composites/PaginationCarousel.tsx | 23 +++ .../PaginationCarouselClient.tsx} | 5 +- .../PaginationCarouselSkeleton.tsx | 6 +- src/components/composites/SearchBar.tsx | 20 ++ src/components/composites/SortSelector.tsx | 58 ++++++ src/components/home/HelloWorld.tsx | 1 + src/components/layout/Footer.tsx | 2 +- src/components/layout/Header.tsx | 34 ++-- src/components/layout/Main.tsx | 2 +- .../header}/DarkModeMenu.tsx | 0 .../header}/LocaleMenu.tsx | 0 .../layout/{ => header}/MobileSheet.tsx | 2 +- src/components/layout/{ => header}/Nav.tsx | 0 .../header}/ProfileMenu.tsx | 0 src/components/news/ArticleCard.tsx | 5 +- src/components/news/CardGridSkeleton.tsx | 4 +- src/components/news/ItemGridSkeleton.tsx | 6 +- .../providers/IntlErrorProvider.tsx | 7 +- src/components/storage/AddToCartButton.tsx | 73 +++++++ src/components/storage/BorrowDialog.tsx | 56 ++++++ src/components/storage/ItemCard.tsx | 57 ++++++ src/components/storage/ItemCardSkeleton.tsx | 30 +++ src/components/storage/LoanForm.tsx | 103 ++++++++++ src/components/storage/SelectorsSkeleton.tsx | 13 ++ .../storage/ShoppingCartClearDialog.tsx | 45 +++++ src/components/storage/ShoppingCartLink.tsx | 55 ++++++ src/components/storage/ShoppingCartTable.tsx | 117 ++++++++++++ .../storage/ShoppingCartTableSkeleton.tsx | 69 +++++++ src/components/storage/SkeletonCard.tsx | 17 -- src/components/ui/Calendar.tsx | 68 +++++++ src/components/ui/Card.tsx | 33 ++-- src/components/ui/Combobox.tsx | 17 +- src/components/ui/DatePicker.tsx | 72 +++++++ src/components/ui/Form.tsx | 179 ++++++++++++++++++ src/components/ui/Input.tsx | 2 +- src/components/ui/Label.tsx | 26 +++ src/components/ui/Loader.tsx | 30 +++ src/components/ui/SearchBar.tsx | 36 ---- src/components/ui/Table.tsx | 117 ++++++++++++ src/lib/hooks/useLocalStorage.ts | 103 ++++++++++ src/lib/locale/index.ts | 8 + src/lib/locale/{i18n.ts => request.ts} | 3 +- src/mock-data/items.ts | 20 ++ tailwind.config.ts | 4 + tsconfig.json | 2 +- 80 files changed, 2086 insertions(+), 522 deletions(-) delete mode 100755 .husky/_/pre-commit delete mode 100755 .husky/_/prepare-commit-msg create mode 100644 lighthouserc.cjs delete mode 100644 lighthouserc.yml rename postcss.config.cjs => postcss.config.js (52%) rename src/app/[locale]/(default)/news/{(header) => (main)}/layout.tsx (100%) rename src/app/[locale]/(default)/news/{(header) => (main)}/loading.tsx (81%) rename src/app/[locale]/(default)/news/{(header) => (main)}/page.tsx (82%) create mode 100644 src/app/[locale]/(default)/storage/(main)/layout.tsx create mode 100644 src/app/[locale]/(default)/storage/(main)/loading.tsx create mode 100644 src/app/[locale]/(default)/storage/(main)/page.tsx delete mode 100644 src/app/[locale]/(default)/storage/layout.tsx delete mode 100644 src/app/[locale]/(default)/storage/loading.tsx create mode 100644 src/app/[locale]/(default)/storage/new/page.tsx delete mode 100644 src/app/[locale]/(default)/storage/page.tsx create mode 100644 src/app/[locale]/(default)/storage/shopping-cart/layout.tsx create mode 100644 src/app/[locale]/(default)/storage/shopping-cart/loading.tsx create mode 100644 src/app/[locale]/(default)/storage/shopping-cart/page.tsx create mode 100644 src/app/[locale]/error.tsx create mode 100644 src/app/[locale]/not-found.tsx create mode 100644 src/components/composites/CategorySelector.tsx create mode 100644 src/components/composites/ConfirmDialog.tsx create mode 100644 src/components/composites/PaginationCarousel.tsx rename src/components/{layout/PaginationCarousel.tsx => composites/PaginationCarouselClient.tsx} (97%) rename src/components/{layout => composites}/PaginationCarouselSkeleton.tsx (93%) create mode 100644 src/components/composites/SearchBar.tsx create mode 100644 src/components/composites/SortSelector.tsx rename src/components/{settings => layout/header}/DarkModeMenu.tsx (100%) rename src/components/{settings => layout/header}/LocaleMenu.tsx (100%) rename src/components/layout/{ => header}/MobileSheet.tsx (96%) rename src/components/layout/{ => header}/Nav.tsx (100%) rename src/components/{settings => layout/header}/ProfileMenu.tsx (100%) create mode 100644 src/components/storage/AddToCartButton.tsx create mode 100644 src/components/storage/BorrowDialog.tsx create mode 100644 src/components/storage/ItemCard.tsx create mode 100644 src/components/storage/ItemCardSkeleton.tsx create mode 100644 src/components/storage/LoanForm.tsx create mode 100644 src/components/storage/SelectorsSkeleton.tsx create mode 100644 src/components/storage/ShoppingCartClearDialog.tsx create mode 100644 src/components/storage/ShoppingCartLink.tsx create mode 100644 src/components/storage/ShoppingCartTable.tsx create mode 100644 src/components/storage/ShoppingCartTableSkeleton.tsx delete mode 100644 src/components/storage/SkeletonCard.tsx create mode 100644 src/components/ui/Calendar.tsx create mode 100644 src/components/ui/DatePicker.tsx create mode 100644 src/components/ui/Form.tsx create mode 100644 src/components/ui/Label.tsx create mode 100644 src/components/ui/Loader.tsx delete mode 100644 src/components/ui/SearchBar.tsx create mode 100644 src/components/ui/Table.tsx create mode 100644 src/lib/hooks/useLocalStorage.ts rename src/lib/locale/{i18n.ts => request.ts} (81%) diff --git a/.github/workflows/code-quality.yml b/.github/workflows/code-quality.yml index 16f4c25..841aaa2 100644 --- a/.github/workflows/code-quality.yml +++ b/.github/workflows/code-quality.yml @@ -39,9 +39,14 @@ jobs: FEIDE_TOKEN_ENDPOINT: "https://auth.dataporten.no/oauth/token" FEIDE_USERINFO_ENDPOINT: "https://auth.dataporten.no/openid/userinfo" NEXT_PUBLIC_SITE_URL: "http://localhost:3000" + LHCI_TOKEN: ${{ secrets.LHCI_BUILD_TOKEN }} steps: - name: Checkout uses: actions/checkout@v4 + with: + fetch-depth: 20 + - name: Fetch base_ref HEAD + run: git fetch --depth=1 origin +refs/heads/${{ github.base_ref }}:refs/remotes/origin/${{ github.base_ref }} - name: Setup bun uses: oven-sh/setup-bun@v2 with: @@ -54,5 +59,5 @@ jobs: env: LHCI_GITHUB_APP_TOKEN: ${{ secrets.LHCI_GITHUB_APP_TOKEN }} run: | - bun add -g @lhci/cli@0.13.x + bun add -g @lhci/cli@0.14.x lhci autorun diff --git a/.github/workflows/deploy-script.yml b/.github/workflows/deploy-script.yml index 38045f4..04ca6cf 100644 --- a/.github/workflows/deploy-script.yml +++ b/.github/workflows/deploy-script.yml @@ -25,7 +25,7 @@ on: jobs: script: name: Script - runs-on: ubuntu-latest + runs-on: self-hosted environment: ${{ inputs.environment }} steps: - uses: appleboy/ssh-action@v1.0.3 diff --git a/.gitignore b/.gitignore index b8a2932..aa2843f 100644 --- a/.gitignore +++ b/.gitignore @@ -20,3 +20,6 @@ public/robots.txt # data /data + +# Ignore husky files, see PR #54 +/.husky/* diff --git a/.husky/_/pre-commit b/.husky/_/pre-commit deleted file mode 100755 index 3fbf5f9..0000000 --- a/.husky/_/pre-commit +++ /dev/null @@ -1,60 +0,0 @@ -#!/bin/sh - -if [ "$LEFTHOOK_VERBOSE" = "1" -o "$LEFTHOOK_VERBOSE" = "true" ]; then - set -x -fi - -if [ "$LEFTHOOK" = "0" ]; then - exit 0 -fi - -call_lefthook() -{ - if test -n "$LEFTHOOK_BIN" - then - "$LEFTHOOK_BIN" "$@" - elif lefthook -h >/dev/null 2>&1 - then - lefthook "$@" - else - dir="$(git rev-parse --show-toplevel)" - osArch=$(uname | tr '[:upper:]' '[:lower:]') - cpuArch=$(uname -m | sed 's/aarch64/arm64/;s/x86_64/x64/') - if test -f "$dir/node_modules/lefthook-${osArch}-${cpuArch}/bin/lefthook" - then - "$dir/node_modules/lefthook-${osArch}-${cpuArch}/bin/lefthook" "$@" - elif test -f "$dir/node_modules/@evilmartians/lefthook/bin/lefthook-${osArch}-${cpuArch}/lefthook" - then - "$dir/node_modules/@evilmartians/lefthook/bin/lefthook-${osArch}-${cpuArch}/lefthook" "$@" - elif test -f "$dir/node_modules/@evilmartians/lefthook-installer/bin/lefthook" - then - "$dir/node_modules/@evilmartians/lefthook-installer/bin/lefthook" "$@" - elif test -f "$dir/node_modules/lefthook/bin/index.js" - then - "$dir/node_modules/lefthook/bin/index.js" "$@" - - elif bundle exec lefthook -h >/dev/null 2>&1 - then - bundle exec lefthook "$@" - elif yarn lefthook -h >/dev/null 2>&1 - then - yarn lefthook "$@" - elif pnpm lefthook -h >/dev/null 2>&1 - then - pnpm lefthook "$@" - elif swift package plugin lefthook >/dev/null 2>&1 - then - swift package --disable-sandbox plugin lefthook "$@" - elif command -v mint >/dev/null 2>&1 - then - mint run csjones/lefthook-plugin "$@" - elif command -v npx >/dev/null 2>&1 - then - npx lefthook "$@" - else - echo "Can't find lefthook in PATH" - fi - fi -} - -call_lefthook run "pre-commit" "$@" diff --git a/.husky/_/prepare-commit-msg b/.husky/_/prepare-commit-msg deleted file mode 100755 index e8e8dda..0000000 --- a/.husky/_/prepare-commit-msg +++ /dev/null @@ -1,60 +0,0 @@ -#!/bin/sh - -if [ "$LEFTHOOK_VERBOSE" = "1" -o "$LEFTHOOK_VERBOSE" = "true" ]; then - set -x -fi - -if [ "$LEFTHOOK" = "0" ]; then - exit 0 -fi - -call_lefthook() -{ - if test -n "$LEFTHOOK_BIN" - then - "$LEFTHOOK_BIN" "$@" - elif lefthook -h >/dev/null 2>&1 - then - lefthook "$@" - else - dir="$(git rev-parse --show-toplevel)" - osArch=$(uname | tr '[:upper:]' '[:lower:]') - cpuArch=$(uname -m | sed 's/aarch64/arm64/;s/x86_64/x64/') - if test -f "$dir/node_modules/lefthook-${osArch}-${cpuArch}/bin/lefthook" - then - "$dir/node_modules/lefthook-${osArch}-${cpuArch}/bin/lefthook" "$@" - elif test -f "$dir/node_modules/@evilmartians/lefthook/bin/lefthook-${osArch}-${cpuArch}/lefthook" - then - "$dir/node_modules/@evilmartians/lefthook/bin/lefthook-${osArch}-${cpuArch}/lefthook" "$@" - elif test -f "$dir/node_modules/@evilmartians/lefthook-installer/bin/lefthook" - then - "$dir/node_modules/@evilmartians/lefthook-installer/bin/lefthook" "$@" - elif test -f "$dir/node_modules/lefthook/bin/index.js" - then - "$dir/node_modules/lefthook/bin/index.js" "$@" - - elif bundle exec lefthook -h >/dev/null 2>&1 - then - bundle exec lefthook "$@" - elif yarn lefthook -h >/dev/null 2>&1 - then - yarn lefthook "$@" - elif pnpm lefthook -h >/dev/null 2>&1 - then - pnpm lefthook "$@" - elif swift package plugin lefthook >/dev/null 2>&1 - then - swift package --disable-sandbox plugin lefthook "$@" - elif command -v mint >/dev/null 2>&1 - then - mint run csjones/lefthook-plugin "$@" - elif command -v npx >/dev/null 2>&1 - then - npx lefthook "$@" - else - echo "Can't find lefthook in PATH" - fi - fi -} - -call_lefthook run "prepare-commit-msg" "$@" diff --git a/biome.json b/biome.json index 11d1c49..e8c5dd7 100644 --- a/biome.json +++ b/biome.json @@ -11,6 +11,9 @@ "enabled": true, "rules": { "recommended": true, + "a11y": { + "useSemanticElements": "off" + }, "nursery": { "useSortedClasses": { "level": "warn", diff --git a/bun.lockb b/bun.lockb index 327f8083d0b568d83f2587449f0cceaaee6d1c19..68cb1666a5b8cb1bbfc4f24284c67e21dc64b84a 100755 GIT binary patch delta 70522 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

jV30Yi%xx!(_uO>|$Qk-)hdQZO{+ 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 Pq8|B~M-Ne@i0A(RVo9eo diff --git a/global.d.ts b/global.d.ts index 9ce5fc0..977f992 100644 --- a/global.d.ts +++ b/global.d.ts @@ -1,5 +1,3 @@ // Use type safe message keys with `next-intl` -// eslint-disable-next-line @typescript-eslint/consistent-type-imports type Messages = typeof import('./messages/en.json'); -// eslint-disable-next-line @typescript-eslint/no-empty-interface declare interface IntlMessages extends Messages {} diff --git a/lefthook.yml b/lefthook.yml index cf9b74e..b05d112 100644 --- a/lefthook.yml +++ b/lefthook.yml @@ -1,6 +1,6 @@ pre-commit: commands: check: - glob: "*.{js,ts,cjs,mjs,d.cts,d.mts,jsx,tsx,json,jsonc}" + glob: "*.{js,ts,tsx,json}" stage_fixed: true run: bunx @biomejs/biome check --write --no-errors-on-unmatched --files-ignore-unknown=true --colors=off {staged_files} diff --git a/lighthouserc.cjs b/lighthouserc.cjs new file mode 100644 index 0000000..210334c --- /dev/null +++ b/lighthouserc.cjs @@ -0,0 +1,66 @@ +const PAGES_EXCLUDED = ['news', 'storage']; + +// Do not convert into an ES6 export. +// lighthouse-ci (as of 0.14.0) uses require() to import, and this is not supported with ES6 modules. +module.exports = { + ci: { + collect: { + url: [ + 'http://localhost:3000/en/', // Trailing slash required, else the regex for default lighthouse rules won't catch this one + 'http://localhost:3000/en/about', + 'http://localhost:3000/en/events', + 'http://localhost:3000/en/news', + 'http://localhost:3000/en/news/1', + 'http://localhost:3000/en/storage', + 'http://localhost:3000/en/storage/shopping-cart', + ], + startServerCommand: 'bun run start', + }, + upload: { + target: 'lhci', + serverBaseUrl: 'https://lhci.hackerspace-ntnu.no', // build token is set by the GH Action + }, + assert: { + assertMatrix: [ + { + matchingUrlPattern: `http://.*/en/(?!${PAGES_EXCLUDED.join('|')}).*`, // match all routes, except for pages with special rules. See https://github.com/GoogleChrome/lighthouse-ci/issues/511 and https://github.com/GoogleChrome/lighthouse-ci/issues/208#issuecomment-784501105 + preset: 'lighthouse:recommended', + assertions: { + 'bf-cache': 'off', + 'color-contrast': 'off', + 'heading-order': 'off', + 'largest-contentful-paint': 'off', + 'render-blocking-resources': 'off', + }, + }, + { + matchingUrlPattern: 'http://.*/en/news.*', + preset: 'lighthouse:recommended', + assertions: { + 'bf-cache': 'off', + 'color-contrast': 'off', + 'heading-order': 'off', + 'largest-contentful-paint': 'off', + 'render-blocking-resources': 'off', + interactive: 'off', + 'uses-responsive-images': 'off', // Should be removed when we obtain images from backend + }, + }, + { + matchingUrlPattern: 'http://.*/en/storage.*', + preset: 'lighthouse:recommended', + assertions: { + 'bf-cache': 'off', + 'color-contrast': 'off', + 'heading-order': 'off', + 'largest-contentful-paint': 'off', + 'render-blocking-resources': 'off', + 'unused-javascript': 'off', + 'cumulative-layout-shift': 'off', // We don't always know how many items are in the cart, which can lead to layout shifts when loading completes + 'max-potential-fid': 'off', + }, + }, + ], + }, + }, +}; diff --git a/lighthouserc.yml b/lighthouserc.yml deleted file mode 100644 index 8c8adb7..0000000 --- a/lighthouserc.yml +++ /dev/null @@ -1,37 +0,0 @@ -ci: - collect: - url: - - 'http://localhost:3000/en' - - 'http://localhost:3000/en/events' - - 'http://localhost:3000/en/news' - - 'http://localhost:3000/en/news/1' - - 'http://localhost:3000/en/about' - startServerCommand: 'bun run start' - upload: - target: 'temporary-public-storage' - assert: - preset: 'lighthouse:recommended' - assertions: - first-contentful-paint: - - error - - maxNumericValue: 2000 - aggregationMethod: optimistic - interactive: - - error - - maxNumericValue: 5000 - aggregationMethod: optimistic - bf-cache: 'off' - csp-xss: 'off' - identical-links-same-purpose: 'off' - total-byte-weight: 'off' - color-contrast: 'off' - heading-order: 'off' - mainthread-work-breakdown: 'off' - bootup-time: 'off' - largest-contentful-paint: 'off' - dom-size: 'off' - render-blocking-resources: 'off' - server-response-time: 'off' - uses-responsive-images: 'off' - maskable-icon: 'off' - installable-manifest: 'off' diff --git a/messages/en.json b/messages/en.json index 316e4ba..5c0c922 100644 --- a/messages/en.json +++ b/messages/en.json @@ -9,7 +9,18 @@ "next": "Next", "goToNextPage": "Go to next page", "morePages": "More pages", - "page": "page" + "page": "page", + "category": "category", + "sort": "sort", + "photoOf": "Photo of {name}" + }, + "error": { + "notFound": "404 - Page not found", + "notFoundDescription": "Oops! Looks like this page got lost in cyberspace.", + "error": "Oops! Something went wrong", + "errorDescription": "Don't worry, our best hackers are on it!", + "goToHomepage": "Return to homepage", + "tryAgain": "Try again" }, "layout": { "hackerspaceHome": "Hackerspace homepage", @@ -52,13 +63,15 @@ }, "storage": { "title": "Storage", + "searchPlaceholder": "Search for product...", "card": { "quantityInfo": "{quantity} units", - "addToCart": "Add to cart" + "addToCart": "Add to cart", + "removeFromCart": "Remove from cart" }, "select": { + "ariaLabel": "Select how to filter the storage items", "filters": "Filters", - "defaultPlaceholder": "Sort results", "popularity": "Popularity", "sortDescending": "Inventory (descending)", "sortAscending": "Inventory (ascending)", @@ -72,8 +85,43 @@ "peripherals": "PC peripherals", "miniPC": "Mini PC" }, + "searchParams": { + "popularity": "popularity", + "descending": "descending", + "ascending": "ascending", + "name": "name", + "cables": "cables", + "sensors": "sensors", + "peripherals": "peripherals", + "miniPC": "minipc" + }, "tooltips": { "viewShoppingCart": "View shopping cart" + }, + "shoppingCart": { + "title": "Shopping Cart", + "productId": "Product ID", + "productName": "Product Name", + "location": "Location", + "unitsAvailable": "Units available", + "tableDescription": "A list of your shopping cart items.", + "backToStorage": "Back to storage", + "cartEmpty": "Your shopping cart is empty.", + "clearCart": "Empty shopping cart", + "cancel": "Cancel", + "clear": "Clear", + "clearCartDescription": "Are you sure you want to clear your shopping cart? All items will be removed.", + "borrowNow": "Borrow now", + "amountOfItemARIA": "Select number of this item" + }, + "loanForm": { + "name": "Name", + "email": "Email", + "phoneNumber": "Phone number", + "phoneNumberDescription": "Phone number for contact. Include country code if the number isn't Norwegian.", + "returnBy": "Return by", + "returnByDescription": "Select how long you would like to borrow the item for.", + "submit": "Submit" } } } diff --git a/messages/no.json b/messages/no.json index 79286cb..62a6fd0 100644 --- a/messages/no.json +++ b/messages/no.json @@ -9,7 +9,18 @@ "next": "Neste", "goToNextPage": "Gå til neste side", "morePages": "Flere sider", - "page": "side" + "page": "side", + "category": "kategori", + "sort": "sortering", + "photoOf": "Bilde av {name}" + }, + "error": { + "notFound": "404 - Siden ble ikke funnet", + "notFoundDescription": "Oops! Ser ut som denne siden gikk seg vill i cyberspace.", + "error": "Oops! Noe gikk galt", + "errorDescription": "Ikke bekymre deg, våre beste hackere jobber med saken!", + "goToHomepage": "Gå tilbake til hjemmesiden", + "tryAgain": "Prøv igjen" }, "layout": { "hackerspaceHome": "Hackerspace hjemmeside", @@ -52,13 +63,15 @@ }, "storage": { "title": "Lager", + "searchPlaceholder": "Søk etter produkt...", "card": { "quantityInfo": "{quantity} stk.", - "addToCart": "Legg i handlekurven" + "addToCart": "Legg i handlekurven", + "removeFromCart": "Fjern fra handlekurven" }, "select": { + "ariaLabel": "Velg hvordan du vil filtrere varene i lageret", "filters": "Filtre", - "defaultPlaceholder": "Sorter resultater", "popularity": "Popularitet", "sortDescending": "Lagerbeholdning (synkende)", "sortAscending": "Lagerbeholdning (stigende)", @@ -72,8 +85,43 @@ "peripherals": "PC-tilbehør", "miniPC": "Mini-PC" }, + "searchParams": { + "popularity": "popularitet", + "descending": "synkende", + "ascending": "stigende", + "name": "navn", + "cables": "kabler", + "sensors": "sensorer", + "peripherals": "tilbehoer", + "miniPC": "minipc" + }, "tooltips": { "viewShoppingCart": "Vis handlekurv" + }, + "shoppingCart": { + "title": "Handlekurv", + "productId": "Produkt-ID", + "productName": "Produktnavn", + "location": "Plass", + "unitsAvailable": "Stk tilgjengelig", + "tableDescription": "En liste over handlekurven din.", + "backToStorage": "Tilbake til lageret", + "cartEmpty": "Handlekurven din er tom.", + "clearCart": "Tøm handlekurven", + "cancel": "Avbryt", + "clear": "Tøm", + "clearCartDescription": "Er du sikker på at du vil tømme handlekurven? Alle varer vil bli slettet.", + "borrowNow": "Lån nå", + "amountOfItemARIA": "Velg antallet av denne gjenstanden" + }, + "loanForm": { + "name": "Navn", + "email": "Epost", + "phoneNumber": "Mobilnummer", + "phoneNumberDescription": "Mobilnummer for kontakt. Inkluder landskode hvis mobilnummeret er ikke norsk.", + "returnBy": "Lån fram til", + "returnByDescription": "Velg hvor lenge du ønsker å låne gjenstanden(e)", + "submit": "Send" } } } diff --git a/next.config.js b/next.config.js index 69a418a..819aad4 100644 --- a/next.config.js +++ b/next.config.js @@ -1,11 +1,7 @@ import nextIntl from 'next-intl/plugin'; - -/** - * Run `build` or `dev` with `SKIP_ENV_VALIDATION` to skip env validation. This is especially useful - * for Docker builds. - */ await import('./src/env.js'); -const withNextIntl = nextIntl('./src/lib/locale/i18n.ts'); + +const withNextIntl = nextIntl('./src/lib/locale/request.ts'); /** @type {import("next").NextConfig} */ const config = { diff --git a/package.json b/package.json index 4b7093b..43e37e9 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "type": "module", "scripts": { "prepare": "if [ \"$NODE_ENV\" != \"production\" ]; then lefthook install; fi", - "dev": "next dev --turbo", + "dev": "next dev", "lint": "biome check --write", "prebuild": "next telemetry disable", "build": "next build", @@ -20,10 +20,12 @@ }, "dependencies": { "@aws-sdk/client-s3": "^3.637.0", + "@hookform/resolvers": "^3.9.0", "@lucia-auth/adapter-drizzle": "^1.1.0", "@radix-ui/react-avatar": "^1.1.0", "@radix-ui/react-dialog": "^1.1.1", "@radix-ui/react-dropdown-menu": "^2.1.1", + "@radix-ui/react-label": "^2.1.0", "@radix-ui/react-popover": "^1.1.1", "@radix-ui/react-select": "^2.1.1", "@radix-ui/react-separator": "^1.1.0", @@ -34,42 +36,46 @@ "@trpc/client": "^11.0.0-rc.490", "@trpc/react-query": "^11.0.0-rc.490", "@trpc/server": "^11.0.0-rc.490", - "autoprefixer": "^10.4.19", - "client-only": "^0.0.1", "cmdk": "1.0.0", "country-flag-icons": "^1.5.12", "cva": "^1.0.0-beta.1", + "date-fns": "^4.1.0", "drizzle-orm": "^0.33.0", "lucia": "^3.2.0", "lucide-react": "^0.396.0", "next": "^14.2.10", "next-intl": "^3.18.1", - "next-sitemap": "^4.2.3", "next-themes": "^0.3.0", "nuqs": "^1.17.4", "postgres": "^3.4.4", "react": "^18.3.1", + "react-day-picker": "8.10.1", "react-dom": "^18.3.1", + "react-hook-form": "^7.53.0", "reading-time": "^1.5.0", - "server-only": "^0.0.1", "sharp": "^0.33.4", "superjson": "^2.2.1", "tailwind-merge": "^2.5.2", "zod": "^3.23.8" }, "devDependencies": { - "@biomejs/biome": "1.8.3", + "@biomejs/biome": "^1.9.1", "@fluid-tailwind/tailwind-merge": "^0.0.2", "@types/node": "^20.14.8", "@types/react": "^18.3.3", "@types/react-dom": "^18.3.0", + "autoprefixer": "^10.4.20", + "client-only": "^0.0.1", "drizzle-kit": "^0.24.1", "fluid-tailwind": "^1.0.3", "lefthook": "^1.7.14", + "next-sitemap": "^4.2.3", "postcss": "^8.4.38", + "server-only": "^0.0.1", "tailwind-scrollbar": "^3.1.0", "tailwindcss": "^3.4.4", "tailwindcss-animate": "^1.0.7", + "tailwindcss-radix": "^3.0.5", "typescript": "^5.5.0" }, "packageManager": "bun@1.1.12" diff --git a/postcss.config.cjs b/postcss.config.js similarity index 52% rename from postcss.config.cjs rename to postcss.config.js index e305dd9..2ef30fc 100644 --- a/postcss.config.cjs +++ b/postcss.config.js @@ -1,3 +1,4 @@ +/** @type {import('postcss-load-config').Config} */ const config = { plugins: { tailwindcss: {}, @@ -5,4 +6,4 @@ const config = { }, }; -module.exports = config; +export default config; diff --git a/src/app/[locale]/(default)/news/(header)/layout.tsx b/src/app/[locale]/(default)/news/(main)/layout.tsx similarity index 100% rename from src/app/[locale]/(default)/news/(header)/layout.tsx rename to src/app/[locale]/(default)/news/(main)/layout.tsx diff --git a/src/app/[locale]/(default)/news/(header)/loading.tsx b/src/app/[locale]/(default)/news/(main)/loading.tsx similarity index 81% rename from src/app/[locale]/(default)/news/(header)/loading.tsx rename to src/app/[locale]/(default)/news/(main)/loading.tsx index 0883f7d..8d26a19 100644 --- a/src/app/[locale]/(default)/news/(header)/loading.tsx +++ b/src/app/[locale]/(default)/news/(main)/loading.tsx @@ -1,4 +1,4 @@ -import { PaginationCarouselSkeleton } from '@/components/layout/PaginationCarouselSkeleton'; +import { PaginationCarouselSkeleton } from '@/components/composites/PaginationCarouselSkeleton'; import { CardGridSkeleton } from '@/components/news/CardGridSkeleton'; import { ItemGridSkeleton } from '@/components/news/ItemGridSkeleton'; import { Separator } from '@/components/ui/Separator'; diff --git a/src/app/[locale]/(default)/news/(header)/page.tsx b/src/app/[locale]/(default)/news/(main)/page.tsx similarity index 82% rename from src/app/[locale]/(default)/news/(header)/page.tsx rename to src/app/[locale]/(default)/news/(main)/page.tsx index 30444ab..a7bfa8d 100644 --- a/src/app/[locale]/(default)/news/(header)/page.tsx +++ b/src/app/[locale]/(default)/news/(main)/page.tsx @@ -4,7 +4,7 @@ import { getTranslations, unstable_setRequestLocale } from 'next-intl/server'; import { createSearchParamsCache, parseAsInteger } from 'nuqs/server'; import { Suspense } from 'react'; -import { PaginationCarousel } from '@/components/layout/PaginationCarousel'; +import { PaginationCarousel } from '@/components/composites/PaginationCarousel'; import { CardGrid } from '@/components/news/CardGrid'; import { ItemGrid } from '@/components/news/ItemGrid'; import { ItemGridSkeleton } from '@/components/news/ItemGridSkeleton'; @@ -45,16 +45,8 @@ export default function NewsPage({ ); diff --git a/src/app/[locale]/(default)/news/[article]/page.tsx b/src/app/[locale]/(default)/news/[article]/page.tsx index c4f6e37..a9b96f3 100644 --- a/src/app/[locale]/(default)/news/[article]/page.tsx +++ b/src/app/[locale]/(default)/news/[article]/page.tsx @@ -55,7 +55,7 @@ export default function ArticlePage({ return (

); } diff --git a/src/app/[locale]/(default)/storage/(main)/layout.tsx b/src/app/[locale]/(default)/storage/(main)/layout.tsx new file mode 100644 index 0000000..4bddd7a --- /dev/null +++ b/src/app/[locale]/(default)/storage/(main)/layout.tsx @@ -0,0 +1,87 @@ +import { CategorySelector } from '@/components/composites/CategorySelector'; +import { SearchBar } from '@/components/composites/SearchBar'; +import { SortSelector } from '@/components/composites/SortSelector'; +import { SelectorsSkeleton } from '@/components/storage/SelectorsSkeleton'; +import { ShoppingCartLink } from '@/components/storage/ShoppingCartLink'; +import { useTranslations } from 'next-intl'; +import { unstable_setRequestLocale } from 'next-intl/server'; +import { Suspense } from 'react'; + +type StorageLayoutProps = { + children: React.ReactNode; + params: { locale: string }; +}; + +export default function StorageLayout({ + children, + params: { locale }, +}: StorageLayoutProps) { + unstable_setRequestLocale(locale); + const t = useTranslations('storage'); + const tUi = useTranslations('ui'); + + // This does not make much sense with a backend, most likely the categories in the backend will have a name in both languages and an ID + const categories = [ + { + label: t('combobox.cables'), + value: t('searchParams.cables'), + }, + { + label: t('combobox.sensors'), + value: t('searchParams.sensors'), + }, + { + label: t('combobox.peripherals'), + value: t('searchParams.peripherals'), + }, + { + label: t('combobox.miniPC'), + value: t('searchParams.miniPC'), + }, + ]; + + const filters = [ + { name: t('select.popularity'), urlName: t('searchParams.popularity') }, + { name: t('select.sortDescending'), urlName: t('searchParams.descending') }, + { name: t('select.sortAscending'), urlName: t('searchParams.ascending') }, + { name: t('select.name'), urlName: t('searchParams.name') }, + ]; + + return ( + <> +
+

{t('title')}

+ +
+
+ + }> + + + +
+ {children} + + ); +} diff --git a/src/app/[locale]/(default)/storage/(main)/loading.tsx b/src/app/[locale]/(default)/storage/(main)/loading.tsx new file mode 100644 index 0000000..c510d4f --- /dev/null +++ b/src/app/[locale]/(default)/storage/(main)/loading.tsx @@ -0,0 +1,16 @@ +import { PaginationCarouselSkeleton } from '@/components/composites/PaginationCarouselSkeleton'; +import { ItemCardSkeleton } from '@/components/storage/ItemCardSkeleton'; +import { useId } from 'react'; + +export default function StorageSkeleton() { + return ( + <> +
+ {Array.from({ length: 8 }).map(() => ( + + ))} +
+ + + ); +} diff --git a/src/app/[locale]/(default)/storage/(main)/page.tsx b/src/app/[locale]/(default)/storage/(main)/page.tsx new file mode 100644 index 0000000..9bbb49a --- /dev/null +++ b/src/app/[locale]/(default)/storage/(main)/page.tsx @@ -0,0 +1,54 @@ +import { items } from '@/mock-data/items'; +import { useTranslations } from 'next-intl'; +import { getTranslations, unstable_setRequestLocale } from 'next-intl/server'; +import { createSearchParamsCache, parseAsInteger } from 'nuqs/server'; + +import { PaginationCarousel } from '@/components/composites/PaginationCarousel'; +import { ItemCard } from '@/components/storage/ItemCard'; + +export async function generateMetadata({ + params: { locale }, +}: { + params: { locale: string }; +}) { + const t = await getTranslations({ locale, namespace: 'layout' }); + + return { + title: t('storage'), + }; +} + +export default function StoragePage({ + params: { locale }, + searchParams, +}: { + params: { locale: string }; + searchParams: Record; +}) { + unstable_setRequestLocale(locale); + const t = useTranslations('ui'); + + const itemsPerPage = 12; + + const searchParamsCache = createSearchParamsCache({ + [t('page')]: parseAsInteger.withDefault(1), + }); + + const { [t('page')]: page = 1 } = searchParamsCache.parse(searchParams); + + return ( + <> +
+ {items + .slice((page - 1) * itemsPerPage, page * itemsPerPage) + .map((item) => ( + + ))} +
+ + + ); +} diff --git a/src/app/[locale]/(default)/storage/layout.tsx b/src/app/[locale]/(default)/storage/layout.tsx deleted file mode 100644 index 0a81eae..0000000 --- a/src/app/[locale]/(default)/storage/layout.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import { Button } from '@/components/ui/Button'; -import { - Tooltip, - TooltipContent, - TooltipProvider, - TooltipTrigger, -} from '@/components/ui/Tooltip'; -import { ShoppingCartIcon } from 'lucide-react'; -import { useTranslations } from 'next-intl'; - -export default function StorageLayout({ - children, -}: { - children: React.ReactNode; -}) { - const t = useTranslations('storage'); - - return ( - <> -
-

{t('title')}

- - - - - - -

{t('tooltips.viewShoppingCart')}

-
-
-
-
- {children} - - ); -} diff --git a/src/app/[locale]/(default)/storage/loading.tsx b/src/app/[locale]/(default)/storage/loading.tsx deleted file mode 100644 index f88b271..0000000 --- a/src/app/[locale]/(default)/storage/loading.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import { SkeletonCard } from '@/components/storage/SkeletonCard'; -import { Skeleton } from '@/components/ui/Skeleton'; - -export default function StorageSkeleton() { - return ( - <> -
- - - -
-
- - - - - - - - -
- - ); -} diff --git a/src/app/[locale]/(default)/storage/new/page.tsx b/src/app/[locale]/(default)/storage/new/page.tsx new file mode 100644 index 0000000..6ec7220 --- /dev/null +++ b/src/app/[locale]/(default)/storage/new/page.tsx @@ -0,0 +1,3 @@ +export default function NewItemPage() { + return

New item page

; +} diff --git a/src/app/[locale]/(default)/storage/page.tsx b/src/app/[locale]/(default)/storage/page.tsx deleted file mode 100644 index 223f604..0000000 --- a/src/app/[locale]/(default)/storage/page.tsx +++ /dev/null @@ -1,156 +0,0 @@ -import { PaginationCarousel } from '@/components/layout/PaginationCarousel'; -import { Button } from '@/components/ui/Button'; -import { - Card, - CardDescription, - CardFooter, - CardHeader, - CardTitle, -} from '@/components/ui/Card'; -import { Combobox } from '@/components/ui/Combobox'; -import { SearchBar } from '@/components/ui/SearchBar'; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from '@/components/ui/Select'; -import { items } from '@/mock-data/items'; -import { useTranslations } from 'next-intl'; -import { getTranslations, unstable_setRequestLocale } from 'next-intl/server'; -import Image from 'next/image'; -import { createSearchParamsCache, parseAsInteger } from 'nuqs/server'; - -export async function generateMetadata({ - params: { locale }, -}: { - params: { locale: string }; -}) { - const t = await getTranslations({ locale, namespace: 'layout' }); - - return { - title: t('storage'), - }; -} - -export default function StoragePage({ - params: { locale }, - searchParams, -}: { - params: { locale: string }; - searchParams: Record; -}) { - unstable_setRequestLocale(locale); - const t = useTranslations('storage'); - const t_ui = useTranslations('ui'); - - const itemsPerPage = 12; - - const searchParamsCache = createSearchParamsCache({ - [t_ui('page')]: parseAsInteger.withDefault(1), - }); - - const { [t_ui('page')]: page = 1 } = searchParamsCache.parse(searchParams); - - // TODO: Implement filters and category selection - const categories = [ - { - value: 'cables', - label: t('combobox.cables'), - }, - { - value: 'sensors', - label: t('combobox.sensors'), - }, - { - value: 'peripherals', - label: t('combobox.peripherals'), - }, - { - value: 'miniPC', - label: t('combobox.miniPC'), - }, - ]; - - const filters = [ - 'select.popularity', - 'select.sortDescending', - 'select.sortAscending', - 'select.name', - ] as const; - - return ( - <> -
- - - - -
-
- {items - .slice((page - 1) * itemsPerPage, page * itemsPerPage) - .map((item) => ( - - -
- {`Photo -
- {item.name} - - {item.location} - -
- - - {t('card.quantityInfo', { quantity: item.quantity })} - - - -
- ))} -
- - - ); -} diff --git a/src/app/[locale]/(default)/storage/shopping-cart/layout.tsx b/src/app/[locale]/(default)/storage/shopping-cart/layout.tsx new file mode 100644 index 0000000..8235cb2 --- /dev/null +++ b/src/app/[locale]/(default)/storage/shopping-cart/layout.tsx @@ -0,0 +1,38 @@ +import { Button } from '@/components/ui/Button'; +import { Link } from '@/lib/locale/navigation'; +import { ArrowLeftIcon } from 'lucide-react'; +import { useTranslations } from 'next-intl'; +import { unstable_setRequestLocale } from 'next-intl/server'; + +type ShoppingCartLayoutProps = { + children: React.ReactNode; + params: { locale: string }; +}; + +export default function StorageLayout({ + children, + params: { locale }, +}: ShoppingCartLayoutProps) { + unstable_setRequestLocale(locale); + const t = useTranslations('storage.shoppingCart'); + return ( + <> +
+

+ {t('title')} +

+ +
+ {children} + + ); +} diff --git a/src/app/[locale]/(default)/storage/shopping-cart/loading.tsx b/src/app/[locale]/(default)/storage/shopping-cart/loading.tsx new file mode 100644 index 0000000..27310cd --- /dev/null +++ b/src/app/[locale]/(default)/storage/shopping-cart/loading.tsx @@ -0,0 +1,23 @@ +import { ShoppingCartTableSkeleton } from '@/components/storage/ShoppingCartTableSkeleton'; +import { Skeleton } from '@/components/ui/Skeleton'; +import { useTranslations } from 'next-intl'; + +export default function ShoppingCartSkeleton() { + const t = useTranslations('storage.shoppingCart'); + const tableMessages = { + productId: t('productId'), + productName: t('productName'), + location: t('location'), + unitsAvailable: t('unitsAvailable'), + }; + + return ( + <> + +
+ + +
+ + ); +} diff --git a/src/app/[locale]/(default)/storage/shopping-cart/page.tsx b/src/app/[locale]/(default)/storage/shopping-cart/page.tsx new file mode 100644 index 0000000..f72474e --- /dev/null +++ b/src/app/[locale]/(default)/storage/shopping-cart/page.tsx @@ -0,0 +1,54 @@ +import { BorrowDialog } from '@/components/storage/BorrowDialog'; +import { ShoppingCartClearDialog } from '@/components/storage/ShoppingCartClearDialog'; +import { ShoppingCartTable } from '@/components/storage/ShoppingCartTable'; +import { useTranslations } from 'next-intl'; +import { unstable_setRequestLocale } from 'next-intl/server'; + +export default function StorageShoppingCartPage({ + params: { locale }, +}: { + params: { locale: string }; +}) { + unstable_setRequestLocale(locale); + const t = useTranslations('storage.shoppingCart'); + const tLoanForm = useTranslations('storage.loanForm'); + + const tableMessages = { + tableDescription: t('tableDescription'), + productId: t('productId'), + productName: t('productName'), + location: t('location'), + unitsAvailable: t('unitsAvailable'), + cartEmpty: t('cartEmpty'), + amountOfItemARIA: t('amountOfItemARIA'), + }; + + const borrowNowMessages = { + borrowNow: t('borrowNow'), + name: tLoanForm('name'), + email: tLoanForm('email'), + phoneNumber: tLoanForm('phoneNumber'), + phoneNumberDescription: tLoanForm('phoneNumberDescription'), + returnBy: tLoanForm('returnBy'), + returnByDescription: tLoanForm('returnByDescription'), + submit: tLoanForm('submit'), + }; + + return ( + <> + +
+ + +
+ + ); +} diff --git a/src/app/[locale]/error.tsx b/src/app/[locale]/error.tsx new file mode 100644 index 0000000..a839b0a --- /dev/null +++ b/src/app/[locale]/error.tsx @@ -0,0 +1,44 @@ +'use client'; + +import { Button } from '@/components/ui/Button'; +import { Link } from '@/lib/locale/navigation'; +import { AlertTriangleIcon } from 'lucide-react'; +import { useTranslations } from 'next-intl'; +import { useEffect } from 'react'; + +export default function ErrorPage({ + error, + reset, +}: { + error: Error; + reset: () => void; +}) { + const t = useTranslations('error'); + useEffect(() => { + console.error(error); + }, [error]); + return ( +
+ +

+ {t('error')} +

+

+ {t('errorDescription')} +

+ {error.message && ( +

+ Error: {error.message} +

+ )} +
+ + +
+
+ ); +} diff --git a/src/app/[locale]/not-found.tsx b/src/app/[locale]/not-found.tsx new file mode 100644 index 0000000..e97706f --- /dev/null +++ b/src/app/[locale]/not-found.tsx @@ -0,0 +1,22 @@ +import { Button } from '@/components/ui/Button'; +import { Link } from '@/lib/locale/navigation'; +import { HardDriveIcon } from 'lucide-react'; +import { useTranslations } from 'next-intl'; + +export default function NotFoundPage() { + const t = useTranslations('error'); + return ( +
+ +

+ {t('notFound')} +

+

+ {t('notFoundDescription')} +

+ +
+ ); +} diff --git a/src/app/not-found.tsx b/src/app/not-found.tsx index 3f80952..17d77be 100644 --- a/src/app/not-found.tsx +++ b/src/app/not-found.tsx @@ -1,9 +1,14 @@ 'use client'; import { routing } from '@/lib/locale'; -import { redirect, usePathname } from 'next/navigation'; +import NextError from 'next/error'; -export default function NotFound() { - const pathname = usePathname(); - redirect(`/${routing.defaultLocale}/${pathname}`); +export default function NotFoundPage() { + return ( + + + + + + ); } diff --git a/src/components/composites/CategorySelector.tsx b/src/components/composites/CategorySelector.tsx new file mode 100644 index 0000000..260cec6 --- /dev/null +++ b/src/components/composites/CategorySelector.tsx @@ -0,0 +1,46 @@ +'use client'; + +import { useQueryState } from 'nuqs'; +import { parseAsString } from 'nuqs/server'; +import { Combobox } from '../ui/Combobox'; + +type CategorySelectorProps = { + categories: { + value: string; + label: string; + }[]; + t: { + category: string; + sort: string; + defaultDescription: string; + defaultPlaceholder: string; + }; +}; + +function CategorySelector({ categories, t }: CategorySelectorProps) { + const [category, setCategory] = useQueryState( + t.category, + parseAsString + .withDefault('') + .withOptions({ shallow: false, clearOnDefault: true }), + ); + + function valueCallback(category: string | null) { + setCategory(category); + } + + return ( + + ); +} + +export { CategorySelector }; diff --git a/src/components/composites/ConfirmDialog.tsx b/src/components/composites/ConfirmDialog.tsx new file mode 100644 index 0000000..4a2c1b3 --- /dev/null +++ b/src/components/composites/ConfirmDialog.tsx @@ -0,0 +1,63 @@ +'use client'; + +import { Button, type buttonVariants } from '@/components/ui/Button'; +import { + Dialog, + DialogClose, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from '@/components/ui/Dialog'; +import type { VariantProps } from '@/lib/utils'; +import { useState } from 'react'; + +type ConfirmDialogProps = { + t: { + title: string; + description: string; + cancel: string; + confirm: string; + }; + confirmAction: () => void; + disabled?: boolean; +} & React.HTMLAttributes & + VariantProps; + +function ConfirmDialog({ confirmAction, t, ...props }: ConfirmDialogProps) { + const [open, setOpen] = useState(false); + + return ( + + + + + + + + + ); +} + +export { ConfirmDialog }; diff --git a/src/components/composites/PaginationCarousel.tsx b/src/components/composites/PaginationCarousel.tsx new file mode 100644 index 0000000..9ddd538 --- /dev/null +++ b/src/components/composites/PaginationCarousel.tsx @@ -0,0 +1,23 @@ +import { PaginationCarouselClient } from '@/components/composites/PaginationCarouselClient'; +import { useTranslations } from 'next-intl'; + +function PaginationCarousel({ + ...props +}: { className?: string; totalPages: number }) { + const t = useTranslations('ui'); + return ( + + ); +} + +export { PaginationCarousel }; diff --git a/src/components/layout/PaginationCarousel.tsx b/src/components/composites/PaginationCarouselClient.tsx similarity index 97% rename from src/components/layout/PaginationCarousel.tsx rename to src/components/composites/PaginationCarouselClient.tsx index bdc222c..4693b24 100644 --- a/src/components/layout/PaginationCarousel.tsx +++ b/src/components/composites/PaginationCarouselClient.tsx @@ -11,6 +11,7 @@ import { } from '@/components/ui/Pagination'; import { cx } from '@/lib/utils'; import { parseAsInteger, useQueryState } from 'nuqs'; + type PaginationCarouselProps = { className?: string; totalPages: number; @@ -24,7 +25,7 @@ type PaginationCarouselProps = { }; }; -function PaginationCarousel({ +function PaginationCarouselClient({ className, totalPages, t, @@ -130,4 +131,4 @@ function PaginationCarousel({ ); } -export { PaginationCarousel }; +export { PaginationCarouselClient }; diff --git a/src/components/layout/PaginationCarouselSkeleton.tsx b/src/components/composites/PaginationCarouselSkeleton.tsx similarity index 93% rename from src/components/layout/PaginationCarouselSkeleton.tsx rename to src/components/composites/PaginationCarouselSkeleton.tsx index 9e55181..1f62e88 100644 --- a/src/components/layout/PaginationCarouselSkeleton.tsx +++ b/src/components/composites/PaginationCarouselSkeleton.tsx @@ -7,6 +7,8 @@ import { PaginationPrevious, } from '@/components/ui/Pagination'; import { useTranslations } from 'next-intl'; +import { useId } from 'react'; + type PaginationCarouselSkeletonProps = { className?: string; }; @@ -28,10 +30,10 @@ function PaginationCarouselSkeleton({ tabIndex={-1} /> - {Array.from({ length: 4 }).map((_, index) => ( + {Array.from({ length: 4 }).map(() => ( diff --git a/src/components/composites/SearchBar.tsx b/src/components/composites/SearchBar.tsx new file mode 100644 index 0000000..38b8bf0 --- /dev/null +++ b/src/components/composites/SearchBar.tsx @@ -0,0 +1,20 @@ +import { Input } from '@/components/ui/Input'; +import { cx } from '@/lib/utils'; +import { SearchIcon } from 'lucide-react'; +import * as React from 'react'; + +type SearchBarProps = React.InputHTMLAttributes; + +const SearchBar = React.forwardRef( + ({ className, ...props }, ref) => { + return ( +
+ + +
+ ); + }, +); +SearchBar.displayName = 'SearchBar'; + +export { SearchBar }; diff --git a/src/components/composites/SortSelector.tsx b/src/components/composites/SortSelector.tsx new file mode 100644 index 0000000..2adc1db --- /dev/null +++ b/src/components/composites/SortSelector.tsx @@ -0,0 +1,58 @@ +'use client'; + +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/Select'; +import { parseAsString, useQueryState } from 'nuqs'; + +type SortSelectorProps = { + filters: { + name: string; + urlName: string; + }[]; + t: { + sort: string; + defaultValue: string; + defaultSorting: string; + ariaLabel: string; + }; +}; + +function SortSelector({ filters, t }: SortSelectorProps) { + const [filter, setFilter] = useQueryState( + t.sort, + parseAsString + .withDefault(t.defaultSorting) + .withOptions({ shallow: false, clearOnDefault: true }), + ); + + return ( + + ); +} +export { SortSelector }; diff --git a/src/components/home/HelloWorld.tsx b/src/components/home/HelloWorld.tsx index 4be51a8..3506c1e 100644 --- a/src/components/home/HelloWorld.tsx +++ b/src/components/home/HelloWorld.tsx @@ -1,4 +1,5 @@ 'use client'; + import { api } from '@/lib/api/client'; function HelloWorld() { diff --git a/src/components/layout/Footer.tsx b/src/components/layout/Footer.tsx index 0c83b28..cc7f462 100644 --- a/src/components/layout/Footer.tsx +++ b/src/components/layout/Footer.tsx @@ -6,7 +6,7 @@ import { } from '@/components/assets/icons'; import { IDILogo, NexusLogo } from '@/components/assets/sponsors'; import { LogoLink } from '@/components/layout/LogoLink'; -import { Nav } from '@/components/layout/Nav'; +import { Nav } from '@/components/layout/header/Nav'; import { Button } from '@/components/ui/Button'; import { Link } from '@/lib/locale/navigation'; import { BugIcon, MailIcon } from 'lucide-react'; diff --git a/src/components/layout/Header.tsx b/src/components/layout/Header.tsx index ebae02f..85faf28 100644 --- a/src/components/layout/Header.tsx +++ b/src/components/layout/Header.tsx @@ -1,26 +1,28 @@ import { LogoLink } from '@/components/layout/LogoLink'; -import { MobileSheet } from '@/components/layout/MobileSheet'; -import { Nav } from '@/components/layout/Nav'; -import { DarkModeMenu } from '@/components/settings/DarkModeMenu'; -import { LocaleMenu } from '@/components/settings/LocaleMenu'; -import { ProfileMenu } from '@/components/settings/ProfileMenu'; +import { DarkModeMenu } from '@/components/layout/header/DarkModeMenu'; +import { LocaleMenu } from '@/components/layout/header/LocaleMenu'; +import { MobileSheet } from '@/components/layout/header/MobileSheet'; +import { Nav } from '@/components/layout/header/Nav'; +import { ProfileMenu } from '@/components/layout/header/ProfileMenu'; import { useTranslations } from 'next-intl'; function Header() { const t = useTranslations('layout'); return (
- - +
+ + +
diff --git a/src/components/news/ItemGridSkeleton.tsx b/src/components/news/ItemGridSkeleton.tsx index 51f4699..52db860 100644 --- a/src/components/news/ItemGridSkeleton.tsx +++ b/src/components/news/ItemGridSkeleton.tsx @@ -1,11 +1,11 @@ import { ArticleItemSkeleton } from '@/components/news/ArticleItemSkeleton'; -import * as React from 'react'; +import { useId } from 'react'; function ItemGridSkeleton() { return (
- {Array.from({ length: 6 }).map((_, index) => ( - + {Array.from({ length: 6 }).map(() => ( + ))}
); diff --git a/src/components/providers/IntlErrorProvider.tsx b/src/components/providers/IntlErrorProvider.tsx index afead1f..197c9c3 100644 --- a/src/components/providers/IntlErrorProvider.tsx +++ b/src/components/providers/IntlErrorProvider.tsx @@ -6,12 +6,9 @@ type Props = { }; function IntlErrorProvider({ children, locale }: Props) { - const messages = useMessages(); + const { error } = useMessages(); return ( - + {children} ); diff --git a/src/components/storage/AddToCartButton.tsx b/src/components/storage/AddToCartButton.tsx new file mode 100644 index 0000000..c0009f7 --- /dev/null +++ b/src/components/storage/AddToCartButton.tsx @@ -0,0 +1,73 @@ +'use client'; + +import { Button } from '@/components/ui/Button'; +import { Loader } from '@/components/ui/Loader'; +import { useLocalStorage } from '@/lib/hooks/useLocalStorage'; +import { cx } from 'cva'; + +// TODO: Type must be replaced by the type provided from database ORM. +export type StorageItem = { + id: number; + name: string; + photo_url: string; + status: string; + quantity: number; + location: string; +}; + +export type CartItem = { + id: number; + amount: number; +}; + +type AddToCartButtonProps = { + className?: string; + item: StorageItem; + t: { + addToCart: string; + removeFromCart: string; + }; +}; + +function AddToCartButton({ className, item, t }: AddToCartButtonProps) { + const [cart, setCart, isLoading] = useLocalStorage( + 'shopping-cart', + [], + ); + + if (isLoading) { + return ; + } + + function updateCart() { + if (!cart) return; + + const isInCart = cart.some((cartItem) => cartItem.id === item.id); + + if (isInCart) { + const newCart = cart.filter((cartItem) => cartItem.id !== item.id); + setCart(newCart); + } else { + const newCart = [...cart, { id: item.id, amount: 1 }]; + setCart(newCart); + } + } + + return ( + + ); +} + +export { AddToCartButton }; diff --git a/src/components/storage/BorrowDialog.tsx b/src/components/storage/BorrowDialog.tsx new file mode 100644 index 0000000..bfffe11 --- /dev/null +++ b/src/components/storage/BorrowDialog.tsx @@ -0,0 +1,56 @@ +'use client'; + +import type { CartItem } from '@/components/storage/AddToCartButton'; +import { LoanForm } from '@/components/storage/LoanForm'; +import { Button } from '@/components/ui/Button'; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogTrigger, +} from '@/components/ui/Dialog'; +import { useLocalStorage } from '@/lib/hooks/useLocalStorage'; +import { cx } from '@/lib/utils'; + +type BorrowDialogProps = { + t: { + borrowNow: string; + name: string; + email: string; + phoneNumber: string; + phoneNumberDescription: string; + returnBy: string; + returnByDescription: string; + submit: string; + }; + className?: string; +}; + +function BorrowDialog({ t, className }: BorrowDialogProps) { + const [cart, _, isLoading] = useLocalStorage('shopping-cart'); + + return ( + <> + + + + + + + {t.borrowNow} + + + + + + ); +} + +export { BorrowDialog }; diff --git a/src/components/storage/ItemCard.tsx b/src/components/storage/ItemCard.tsx new file mode 100644 index 0000000..ebd734c --- /dev/null +++ b/src/components/storage/ItemCard.tsx @@ -0,0 +1,57 @@ +import type { StorageItem } from '@/components/storage/AddToCartButton'; +import { AddToCartButton } from '@/components/storage/AddToCartButton'; +import { + Card, + CardDescription, + CardFooter, + CardHeader, + CardTitle, +} from '@/components/ui/Card'; +import { useTranslations } from 'next-intl'; +import Image from 'next/image'; + +function ItemCard({ + item, +}: { + item: StorageItem; +}) { + const t = useTranslations('storage'); + const tUi = useTranslations('ui'); + return ( + + +
+ {tUi('photoOf', +
+ + {item.name} + + {item.location} +
+ +

+ {t('card.quantityInfo', { quantity: item.quantity })} +

+ +
+
+ ); +} + +export { ItemCard }; diff --git a/src/components/storage/ItemCardSkeleton.tsx b/src/components/storage/ItemCardSkeleton.tsx new file mode 100644 index 0000000..242ffa8 --- /dev/null +++ b/src/components/storage/ItemCardSkeleton.tsx @@ -0,0 +1,30 @@ +import { + Card, + CardDescription, + CardFooter, + CardHeader, + CardTitle, +} from '@/components/ui/Card'; +import { Skeleton } from '@/components/ui/Skeleton'; + +export function ItemCardSkeleton() { + return ( + + +
+ +
+ + + + + + +
+ + + + +
+ ); +} diff --git a/src/components/storage/LoanForm.tsx b/src/components/storage/LoanForm.tsx new file mode 100644 index 0000000..81b26ac --- /dev/null +++ b/src/components/storage/LoanForm.tsx @@ -0,0 +1,103 @@ +'use client'; + +import { Button } from '@/components/ui/Button'; +import { Calendar } from '@/components/ui/Calendar'; +import { + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage, +} from '@/components/ui/Form'; +import { Input } from '@/components/ui/Input'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { addDays, addWeeks, endOfWeek } from 'date-fns'; +import { useForm } from 'react-hook-form'; +import { z } from 'zod'; + +const formSchema = z.object({ + phone: z.string().min(1), + returnBy: z.date().min(new Date()), +}); + +type LoanFormProps = { + t: { + borrowNow: string; + name: string; + email: string; + phoneNumber: string; + phoneNumberDescription: string; + returnBy: string; + returnByDescription: string; + submit: string; + }; +}; + +function LoanForm({ t }: LoanFormProps) { + const form = useForm>({ + resolver: zodResolver(formSchema), + defaultValues: { + phone: '', + returnBy: new Date(), + }, + }); + + function onSubmit(values: z.infer) { + // TODO: Add new loan to database + console.log(values); + } + + return ( + <> +
+ + ( + + {t.phoneNumber} + + + + {t.phoneNumberDescription} + + + )} + /> + ( + + {t.returnBy} + + + + {t.returnByDescription} + + + )} + /> + + + + + ); +} + +export { LoanForm }; diff --git a/src/components/storage/SelectorsSkeleton.tsx b/src/components/storage/SelectorsSkeleton.tsx new file mode 100644 index 0000000..8bf78b1 --- /dev/null +++ b/src/components/storage/SelectorsSkeleton.tsx @@ -0,0 +1,13 @@ +import { Skeleton } from '@/components/ui/Skeleton'; +import { useId } from 'react'; + +function SelectorsSkeleton() { + return ( +
+ + +
+ ); +} + +export { SelectorsSkeleton }; diff --git a/src/components/storage/ShoppingCartClearDialog.tsx b/src/components/storage/ShoppingCartClearDialog.tsx new file mode 100644 index 0000000..cd6721b --- /dev/null +++ b/src/components/storage/ShoppingCartClearDialog.tsx @@ -0,0 +1,45 @@ +'use client'; + +import { ConfirmDialog } from '@/components/composites/ConfirmDialog'; +import type { CartItem } from '@/components/storage/AddToCartButton'; +import { useLocalStorage } from '@/lib/hooks/useLocalStorage'; +import { cx } from '@/lib/utils'; +import { XIcon } from 'lucide-react'; + +type ShoppingCartClearDialogProps = { + className?: string; + t: { + clearCart: string; + clearCartDescription: string; + clear: string; + cancel: string; + }; +}; + +function ShoppingCartClearDialog({ + className, + t, +}: ShoppingCartClearDialogProps) { + const [cart, setCart, isLoading] = + useLocalStorage('shopping-cart'); + + return ( + setCart(null)} + t={{ + title: t.clearCart, + description: t.clearCartDescription, + confirm: t.clear, + cancel: t.cancel, + }} + > + + ); +} + +export { ShoppingCartClearDialog }; diff --git a/src/components/storage/ShoppingCartLink.tsx b/src/components/storage/ShoppingCartLink.tsx new file mode 100644 index 0000000..6bd6efd --- /dev/null +++ b/src/components/storage/ShoppingCartLink.tsx @@ -0,0 +1,55 @@ +'use client'; +import type { CartItem } from '@/components/storage/AddToCartButton'; +import { Badge } from '@/components/ui/Badge'; +import { Button } from '@/components/ui/Button'; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from '@/components/ui/Tooltip'; +import { useLocalStorage } from '@/lib/hooks/useLocalStorage'; +import { Link } from '@/lib/locale/navigation'; +import { ShoppingCartIcon } from 'lucide-react'; + +type ShoppingCartLinkProps = { + t: { + viewShoppingCart: string; + }; +}; + +function ShoppingCartLink({ t }: ShoppingCartLinkProps) { + const [cart, _, isLoading] = useLocalStorage('shopping-cart'); + + return ( + + + +
+ + {!isLoading && cart && cart.length > 0 && ( + + {cart.length} + + )} +
+
+ +

{t.viewShoppingCart}

+
+
+
+ ); +} + +export { ShoppingCartLink }; diff --git a/src/components/storage/ShoppingCartTable.tsx b/src/components/storage/ShoppingCartTable.tsx new file mode 100644 index 0000000..703105c --- /dev/null +++ b/src/components/storage/ShoppingCartTable.tsx @@ -0,0 +1,117 @@ +'use client'; + +import type { CartItem } from '@/components/storage/AddToCartButton'; +import { Button } from '@/components/ui/Button'; +import { Input } from '@/components/ui/Input'; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from '@/components/ui/Table'; +import { useLocalStorage } from '@/lib/hooks/useLocalStorage'; +import { XIcon } from 'lucide-react'; + +// TODO: Must be replaced by requesting the data from a database. +import { items } from '@/mock-data/items'; +import { ShoppingCartTableSkeleton } from './ShoppingCartTableSkeleton'; + +type ShoppingCartTableProps = { + t: { + tableDescription: string; + productId: string; + productName: string; + location: string; + unitsAvailable: string; + cartEmpty: string; + amountOfItemARIA: string; + }; +}; + +function ShoppingCartTable({ t }: ShoppingCartTableProps) { + const [cart, setCart, isLoading] = useLocalStorage( + 'shopping-cart', + [], + ); + + if (isLoading) { + return ; + } + + if (!cart || cart.length === 0) { + return

{t.cartEmpty}

; + } + + const itemsInCart = items.filter((item) => + cart.some((cartItem) => cartItem.id === item.id), + ); + + function updateAmountInCart(id: number, newValue: number) { + if (!cart) return; + + const newCart = cart.map((cartItem) => + cartItem.id === id ? { ...cartItem, amount: newValue } : cartItem, + ); + setCart(newCart); + } + + function removeItem(id: number) { + if (!cart) return; + + const newCart = cart.filter((cartItem: CartItem) => cartItem.id !== id); + setCart(newCart); + } + + return ( + + + + + {t.productId} + {t.productName} + {t.location} + {t.unitsAvailable} + + + + + {itemsInCart.map((item) => ( + + + cartItem.id === item.id)?.amount || 0 + } + onChange={(e) => + updateAmountInCart(item.id, Number(e.currentTarget.value)) + } + className='w-[80px]' + aria-label={t.amountOfItemARIA} + /> + + {item.id} + {item.name} + {item.location} + {item.quantity} + + + + + ))} + +
+ ); +} + +export { ShoppingCartTable }; diff --git a/src/components/storage/ShoppingCartTableSkeleton.tsx b/src/components/storage/ShoppingCartTableSkeleton.tsx new file mode 100644 index 0000000..62a3aef --- /dev/null +++ b/src/components/storage/ShoppingCartTableSkeleton.tsx @@ -0,0 +1,69 @@ +import { Button } from '@/components/ui/Button'; +import { Input } from '@/components/ui/Input'; +import { Skeleton } from '@/components/ui/Skeleton'; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from '@/components/ui/Table'; +import { XIcon } from 'lucide-react'; +import { useId } from 'react'; + +type ShoppingCartTableSkeletonProps = { + t: { + productId: string; + productName: string; + location: string; + unitsAvailable: string; + }; +}; + +function ShoppingCartTableSkeleton({ t }: ShoppingCartTableSkeletonProps) { + return ( + + + + + + + {t.productId} + {t.productName} + {t.location} + {t.unitsAvailable} + + + + + {Array.from({ length: 3 }).map(() => ( + + + + + + + + + + + + + + + + + + + + + ))} + +
+ ); +} + +export { ShoppingCartTableSkeleton }; diff --git a/src/components/storage/SkeletonCard.tsx b/src/components/storage/SkeletonCard.tsx deleted file mode 100644 index 8c3cafb..0000000 --- a/src/components/storage/SkeletonCard.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import { Skeleton } from '@/components/ui/Skeleton'; - -export function SkeletonCard() { - return ( -
-
- - - -
-
- - -
-
- ); -} diff --git a/src/components/ui/Calendar.tsx b/src/components/ui/Calendar.tsx new file mode 100644 index 0000000..905dbbc --- /dev/null +++ b/src/components/ui/Calendar.tsx @@ -0,0 +1,68 @@ +'use client'; + +import { ChevronLeftIcon, ChevronRightIcon } from 'lucide-react'; +import type * as React from 'react'; +import { DayPicker } from 'react-day-picker'; + +import { buttonVariants } from '@/components/ui/Button'; +import { cx } from '@/lib/utils'; + +export type CalendarProps = React.ComponentProps; + +function Calendar({ + className, + classNames, + showOutsideDays = true, + locale, + ...props +}: CalendarProps) { + return ( + , + IconRight: ({ ...props }) => , + }} + weekStartsOn={1} + {...props} + /> + ); +} +Calendar.displayName = 'Calendar'; + +export { Calendar }; diff --git a/src/components/ui/Card.tsx b/src/components/ui/Card.tsx index 0479465..ac5b4b8 100644 --- a/src/components/ui/Card.tsx +++ b/src/components/ui/Card.tsx @@ -1,6 +1,10 @@ import { cx } from '@/lib/utils'; import * as React from 'react'; +type CardTitleProps = { + level?: 'h2' | 'h3' | 'h4'; +} & React.HTMLAttributes; + const Card = React.forwardRef< HTMLDivElement, React.HTMLAttributes @@ -28,19 +32,22 @@ const CardHeader = React.forwardRef< )); CardHeader.displayName = 'CardHeader'; -const CardTitle = React.forwardRef< - HTMLParagraphElement, - React.HTMLAttributes ->(({ className, ...props }, ref) => ( -

-)); +const CardTitle = React.forwardRef( + ({ level = 'h3', className, ...props }, ref) => { + const Component = level; + + return ( + + ); + }, +); CardTitle.displayName = 'CardTitle'; const CardDescription = React.forwardRef< diff --git a/src/components/ui/Combobox.tsx b/src/components/ui/Combobox.tsx index 57a4b93..d51c3b2 100644 --- a/src/components/ui/Combobox.tsx +++ b/src/components/ui/Combobox.tsx @@ -27,6 +27,9 @@ type ComboboxProps = { defaultPlaceholder: string; buttonClassName?: string; contentClassName?: string; + valueCallback?: (value: string | null) => void; + initialValue?: string | null; + ariaLabel?: string; }; function Combobox({ @@ -35,9 +38,12 @@ function Combobox({ defaultPlaceholder, buttonClassName, contentClassName, + valueCallback, + initialValue, + ariaLabel, }: ComboboxProps) { const [open, setOpen] = React.useState(false); - const [value, setValue] = React.useState(''); + const [value, setValue] = React.useState(initialValue ?? ''); return ( @@ -46,6 +52,7 @@ function Combobox({ variant='outline' role='combobox' aria-expanded={open} + aria-label={ariaLabel} className={cx('w-[200px] justify-between', buttonClassName)} > {value @@ -65,8 +72,14 @@ function Combobox({ key={choice.value} value={choice.value} onSelect={(currentValue) => { - setValue(currentValue === value ? '' : currentValue); + // Set newValue to null if user selects the same value twice + const newValue = + currentValue === value ? null : currentValue; + setValue(newValue); setOpen(false); + if (valueCallback) { + valueCallback(newValue); + } }} > void; + disabled?: Matcher | Matcher[]; + buttonClassName?: string; +}; + +/** + * This is a sligtly modified version of shadcn's Date Picker built on top of Calendar. + * The component has a state, but also allows adding an additional date callback function which + * provides a way to have side effects and/or state updates on the parent component whenever a new date is selected. + */ +function DatePicker({ + initialDate, + dateCallback, + disabled, + buttonClassName, +}: DatePickerProps) { + const [date, setDate] = React.useState(initialDate ?? new Date()); + + function handleDateChange(date: Date | undefined) { + if (!date) return; + setDate(date); + if (dateCallback) { + dateCallback(date); + } + } + + return ( + + + + + + handleDateChange(date)} + disabled={disabled} + /> + + + ); +} + +export { DatePicker }; diff --git a/src/components/ui/Form.tsx b/src/components/ui/Form.tsx new file mode 100644 index 0000000..c945d9f --- /dev/null +++ b/src/components/ui/Form.tsx @@ -0,0 +1,179 @@ +'use client'; + +import type * as LabelPrimitive from '@radix-ui/react-label'; +import { Slot } from '@radix-ui/react-slot'; +import * as React from 'react'; +import { + Controller, + type ControllerProps, + type FieldPath, + type FieldValues, + FormProvider, + useFormContext, +} from 'react-hook-form'; + +import { Label } from '@/components/ui/Label'; +import { cx } from '@/lib/utils'; + +const Form = FormProvider; + +type FormFieldContextValue< + TFieldValues extends FieldValues = FieldValues, + TName extends FieldPath = FieldPath, +> = { + name: TName; +}; + +const FormFieldContext = React.createContext( + {} as FormFieldContextValue, +); + +const FormField = < + TFieldValues extends FieldValues = FieldValues, + TName extends FieldPath = FieldPath, +>({ + ...props +}: ControllerProps) => { + return ( + + + + ); +}; + +const useFormField = () => { + const fieldContext = React.useContext(FormFieldContext); + const itemContext = React.useContext(FormItemContext); + const { getFieldState, formState } = useFormContext(); + + const fieldState = getFieldState(fieldContext.name, formState); + + if (!fieldContext) { + throw new Error('useFormField should be used within '); + } + + const { id } = itemContext; + + return { + id, + name: fieldContext.name, + formItemId: `${id}-form-item`, + formDescriptionId: `${id}-form-item-description`, + formMessageId: `${id}-form-item-message`, + ...fieldState, + }; +}; + +type FormItemContextValue = { + id: string; +}; + +const FormItemContext = React.createContext( + {} as FormItemContextValue, +); + +const FormItem = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => { + const id = React.useId(); + + return ( + +
+ + ); +}); +FormItem.displayName = 'FormItem'; + +const FormLabel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => { + const { error, formItemId } = useFormField(); + + return ( +