diff --git a/.github/workflows/code-quality.yml b/.github/workflows/code-quality.yml index 841aaa2..16f4c25 100644 --- a/.github/workflows/code-quality.yml +++ b/.github/workflows/code-quality.yml @@ -39,14 +39,9 @@ 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: @@ -59,5 +54,5 @@ jobs: env: LHCI_GITHUB_APP_TOKEN: ${{ secrets.LHCI_GITHUB_APP_TOKEN }} run: | - bun add -g @lhci/cli@0.14.x + bun add -g @lhci/cli@0.13.x lhci autorun diff --git a/.github/workflows/deploy-script.yml b/.github/workflows/deploy-script.yml index 04ca6cf..38045f4 100644 --- a/.github/workflows/deploy-script.yml +++ b/.github/workflows/deploy-script.yml @@ -25,7 +25,7 @@ on: jobs: script: name: Script - runs-on: self-hosted + runs-on: ubuntu-latest environment: ${{ inputs.environment }} steps: - uses: appleboy/ssh-action@v1.0.3 diff --git a/.gitignore b/.gitignore index aa2843f..b8a2932 100644 --- a/.gitignore +++ b/.gitignore @@ -20,6 +20,3 @@ public/robots.txt # data /data - -# Ignore husky files, see PR #54 -/.husky/* diff --git a/.husky/_/pre-commit b/.husky/_/pre-commit new file mode 100755 index 0000000..3fbf5f9 --- /dev/null +++ b/.husky/_/pre-commit @@ -0,0 +1,60 @@ +#!/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 new file mode 100755 index 0000000..e8e8dda --- /dev/null +++ b/.husky/_/prepare-commit-msg @@ -0,0 +1,60 @@ +#!/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 e8c5dd7..11d1c49 100644 --- a/biome.json +++ b/biome.json @@ -11,9 +11,6 @@ "enabled": true, "rules": { "recommended": true, - "a11y": { - "useSemanticElements": "off" - }, "nursery": { "useSortedClasses": { "level": "warn", diff --git a/bun.lockb b/bun.lockb index 68cb166..327f808 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/global.d.ts b/global.d.ts index 977f992..9ce5fc0 100644 --- a/global.d.ts +++ b/global.d.ts @@ -1,3 +1,5 @@ // 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 b05d112..cf9b74e 100644 --- a/lefthook.yml +++ b/lefthook.yml @@ -1,6 +1,6 @@ pre-commit: commands: check: - glob: "*.{js,ts,tsx,json}" + glob: "*.{js,ts,cjs,mjs,d.cts,d.mts,jsx,tsx,json,jsonc}" 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 deleted file mode 100644 index 210334c..0000000 --- a/lighthouserc.cjs +++ /dev/null @@ -1,66 +0,0 @@ -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 new file mode 100644 index 0000000..8c8adb7 --- /dev/null +++ b/lighthouserc.yml @@ -0,0 +1,37 @@ +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 5c0c922..316e4ba 100644 --- a/messages/en.json +++ b/messages/en.json @@ -9,18 +9,7 @@ "next": "Next", "goToNextPage": "Go to next page", "morePages": "More pages", - "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" + "page": "page" }, "layout": { "hackerspaceHome": "Hackerspace homepage", @@ -63,15 +52,13 @@ }, "storage": { "title": "Storage", - "searchPlaceholder": "Search for product...", "card": { "quantityInfo": "{quantity} units", - "addToCart": "Add to cart", - "removeFromCart": "Remove from cart" + "addToCart": "Add to cart" }, "select": { - "ariaLabel": "Select how to filter the storage items", "filters": "Filters", + "defaultPlaceholder": "Sort results", "popularity": "Popularity", "sortDescending": "Inventory (descending)", "sortAscending": "Inventory (ascending)", @@ -85,43 +72,8 @@ "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 62a6fd0..79286cb 100644 --- a/messages/no.json +++ b/messages/no.json @@ -9,18 +9,7 @@ "next": "Neste", "goToNextPage": "Gå til neste side", "morePages": "Flere sider", - "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" + "page": "side" }, "layout": { "hackerspaceHome": "Hackerspace hjemmeside", @@ -63,15 +52,13 @@ }, "storage": { "title": "Lager", - "searchPlaceholder": "Søk etter produkt...", "card": { "quantityInfo": "{quantity} stk.", - "addToCart": "Legg i handlekurven", - "removeFromCart": "Fjern fra handlekurven" + "addToCart": "Legg i handlekurven" }, "select": { - "ariaLabel": "Velg hvordan du vil filtrere varene i lageret", "filters": "Filtre", + "defaultPlaceholder": "Sorter resultater", "popularity": "Popularitet", "sortDescending": "Lagerbeholdning (synkende)", "sortAscending": "Lagerbeholdning (stigende)", @@ -85,43 +72,8 @@ "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 819aad4..69a418a 100644 --- a/next.config.js +++ b/next.config.js @@ -1,7 +1,11 @@ import nextIntl from 'next-intl/plugin'; -await import('./src/env.js'); -const withNextIntl = nextIntl('./src/lib/locale/request.ts'); +/** + * 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'); /** @type {import("next").NextConfig} */ const config = { diff --git a/package.json b/package.json index 43e37e9..4b7093b 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", + "dev": "next dev --turbo", "lint": "biome check --write", "prebuild": "next telemetry disable", "build": "next build", @@ -20,12 +20,10 @@ }, "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", @@ -36,46 +34,42 @@ "@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.9.1", + "@biomejs/biome": "1.8.3", "@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.js b/postcss.config.cjs similarity index 52% rename from postcss.config.js rename to postcss.config.cjs index 2ef30fc..e305dd9 100644 --- a/postcss.config.js +++ b/postcss.config.cjs @@ -1,4 +1,3 @@ -/** @type {import('postcss-load-config').Config} */ const config = { plugins: { tailwindcss: {}, @@ -6,4 +5,4 @@ const config = { }, }; -export default config; +module.exports = config; diff --git a/src/app/[locale]/(default)/news/(main)/layout.tsx b/src/app/[locale]/(default)/news/(header)/layout.tsx similarity index 100% rename from src/app/[locale]/(default)/news/(main)/layout.tsx rename to src/app/[locale]/(default)/news/(header)/layout.tsx diff --git a/src/app/[locale]/(default)/news/(main)/loading.tsx b/src/app/[locale]/(default)/news/(header)/loading.tsx similarity index 81% rename from src/app/[locale]/(default)/news/(main)/loading.tsx rename to src/app/[locale]/(default)/news/(header)/loading.tsx index 8d26a19..0883f7d 100644 --- a/src/app/[locale]/(default)/news/(main)/loading.tsx +++ b/src/app/[locale]/(default)/news/(header)/loading.tsx @@ -1,4 +1,4 @@ -import { PaginationCarouselSkeleton } from '@/components/composites/PaginationCarouselSkeleton'; +import { PaginationCarouselSkeleton } from '@/components/layout/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/(main)/page.tsx b/src/app/[locale]/(default)/news/(header)/page.tsx similarity index 82% rename from src/app/[locale]/(default)/news/(main)/page.tsx rename to src/app/[locale]/(default)/news/(header)/page.tsx index a7bfa8d..30444ab 100644 --- a/src/app/[locale]/(default)/news/(main)/page.tsx +++ b/src/app/[locale]/(default)/news/(header)/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/composites/PaginationCarousel'; +import { PaginationCarousel } from '@/components/layout/PaginationCarousel'; import { CardGrid } from '@/components/news/CardGrid'; import { ItemGrid } from '@/components/news/ItemGrid'; import { ItemGridSkeleton } from '@/components/news/ItemGridSkeleton'; @@ -45,8 +45,16 @@ 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 a9b96f3..c4f6e37 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 (
-
+
{`${article.views} ${t('views')}`} -
{article.content}
+
{article.content}
); } diff --git a/src/app/[locale]/(default)/storage/(main)/layout.tsx b/src/app/[locale]/(default)/storage/(main)/layout.tsx deleted file mode 100644 index 4bddd7a..0000000 --- a/src/app/[locale]/(default)/storage/(main)/layout.tsx +++ /dev/null @@ -1,87 +0,0 @@ -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 deleted file mode 100644 index c510d4f..0000000 --- a/src/app/[locale]/(default)/storage/(main)/loading.tsx +++ /dev/null @@ -1,16 +0,0 @@ -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 deleted file mode 100644 index 9bbb49a..0000000 --- a/src/app/[locale]/(default)/storage/(main)/page.tsx +++ /dev/null @@ -1,54 +0,0 @@ -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 new file mode 100644 index 0000000..0a81eae --- /dev/null +++ b/src/app/[locale]/(default)/storage/layout.tsx @@ -0,0 +1,38 @@ +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 new file mode 100644 index 0000000..f88b271 --- /dev/null +++ b/src/app/[locale]/(default)/storage/loading.tsx @@ -0,0 +1,24 @@ +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 deleted file mode 100644 index 6ec7220..0000000 --- a/src/app/[locale]/(default)/storage/new/page.tsx +++ /dev/null @@ -1,3 +0,0 @@ -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 new file mode 100644 index 0000000..223f604 --- /dev/null +++ b/src/app/[locale]/(default)/storage/page.tsx @@ -0,0 +1,156 @@ +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 deleted file mode 100644 index 8235cb2..0000000 --- a/src/app/[locale]/(default)/storage/shopping-cart/layout.tsx +++ /dev/null @@ -1,38 +0,0 @@ -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 deleted file mode 100644 index 27310cd..0000000 --- a/src/app/[locale]/(default)/storage/shopping-cart/loading.tsx +++ /dev/null @@ -1,23 +0,0 @@ -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 deleted file mode 100644 index f72474e..0000000 --- a/src/app/[locale]/(default)/storage/shopping-cart/page.tsx +++ /dev/null @@ -1,54 +0,0 @@ -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 deleted file mode 100644 index a839b0a..0000000 --- a/src/app/[locale]/error.tsx +++ /dev/null @@ -1,44 +0,0 @@ -'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 deleted file mode 100644 index e97706f..0000000 --- a/src/app/[locale]/not-found.tsx +++ /dev/null @@ -1,22 +0,0 @@ -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 17d77be..3f80952 100644 --- a/src/app/not-found.tsx +++ b/src/app/not-found.tsx @@ -1,14 +1,9 @@ 'use client'; import { routing } from '@/lib/locale'; -import NextError from 'next/error'; +import { redirect, usePathname } from 'next/navigation'; -export default function NotFoundPage() { - return ( - - - - - - ); +export default function NotFound() { + const pathname = usePathname(); + redirect(`/${routing.defaultLocale}/${pathname}`); } diff --git a/src/components/composites/CategorySelector.tsx b/src/components/composites/CategorySelector.tsx deleted file mode 100644 index 260cec6..0000000 --- a/src/components/composites/CategorySelector.tsx +++ /dev/null @@ -1,46 +0,0 @@ -'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 deleted file mode 100644 index 4a2c1b3..0000000 --- a/src/components/composites/ConfirmDialog.tsx +++ /dev/null @@ -1,63 +0,0 @@ -'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 deleted file mode 100644 index 9ddd538..0000000 --- a/src/components/composites/PaginationCarousel.tsx +++ /dev/null @@ -1,23 +0,0 @@ -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/composites/SearchBar.tsx b/src/components/composites/SearchBar.tsx deleted file mode 100644 index 38b8bf0..0000000 --- a/src/components/composites/SearchBar.tsx +++ /dev/null @@ -1,20 +0,0 @@ -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 deleted file mode 100644 index 2adc1db..0000000 --- a/src/components/composites/SortSelector.tsx +++ /dev/null @@ -1,58 +0,0 @@ -'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 3506c1e..4be51a8 100644 --- a/src/components/home/HelloWorld.tsx +++ b/src/components/home/HelloWorld.tsx @@ -1,5 +1,4 @@ '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 cc7f462..0c83b28 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/header/Nav'; +import { Nav } from '@/components/layout/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 85faf28..ebae02f 100644 --- a/src/components/layout/Header.tsx +++ b/src/components/layout/Header.tsx @@ -1,28 +1,26 @@ import { LogoLink } from '@/components/layout/LogoLink'; -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 { 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 { 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 52db860..51f4699 100644 --- a/src/components/news/ItemGridSkeleton.tsx +++ b/src/components/news/ItemGridSkeleton.tsx @@ -1,11 +1,11 @@ import { ArticleItemSkeleton } from '@/components/news/ArticleItemSkeleton'; -import { useId } from 'react'; +import * as React from 'react'; function ItemGridSkeleton() { return (
- {Array.from({ length: 6 }).map(() => ( - + {Array.from({ length: 6 }).map((_, index) => ( + ))}
); diff --git a/src/components/providers/IntlErrorProvider.tsx b/src/components/providers/IntlErrorProvider.tsx index 197c9c3..afead1f 100644 --- a/src/components/providers/IntlErrorProvider.tsx +++ b/src/components/providers/IntlErrorProvider.tsx @@ -6,9 +6,12 @@ type Props = { }; function IntlErrorProvider({ children, locale }: Props) { - const { error } = useMessages(); + const messages = useMessages(); return ( - + {children} ); diff --git a/src/components/layout/header/DarkModeMenu.tsx b/src/components/settings/DarkModeMenu.tsx similarity index 100% rename from src/components/layout/header/DarkModeMenu.tsx rename to src/components/settings/DarkModeMenu.tsx diff --git a/src/components/layout/header/LocaleMenu.tsx b/src/components/settings/LocaleMenu.tsx similarity index 100% rename from src/components/layout/header/LocaleMenu.tsx rename to src/components/settings/LocaleMenu.tsx diff --git a/src/components/layout/header/ProfileMenu.tsx b/src/components/settings/ProfileMenu.tsx similarity index 100% rename from src/components/layout/header/ProfileMenu.tsx rename to src/components/settings/ProfileMenu.tsx diff --git a/src/components/storage/AddToCartButton.tsx b/src/components/storage/AddToCartButton.tsx deleted file mode 100644 index c0009f7..0000000 --- a/src/components/storage/AddToCartButton.tsx +++ /dev/null @@ -1,73 +0,0 @@ -'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 deleted file mode 100644 index bfffe11..0000000 --- a/src/components/storage/BorrowDialog.tsx +++ /dev/null @@ -1,56 +0,0 @@ -'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 deleted file mode 100644 index ebd734c..0000000 --- a/src/components/storage/ItemCard.tsx +++ /dev/null @@ -1,57 +0,0 @@ -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 deleted file mode 100644 index 242ffa8..0000000 --- a/src/components/storage/ItemCardSkeleton.tsx +++ /dev/null @@ -1,30 +0,0 @@ -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 deleted file mode 100644 index 81b26ac..0000000 --- a/src/components/storage/LoanForm.tsx +++ /dev/null @@ -1,103 +0,0 @@ -'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 deleted file mode 100644 index 8bf78b1..0000000 --- a/src/components/storage/SelectorsSkeleton.tsx +++ /dev/null @@ -1,13 +0,0 @@ -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 deleted file mode 100644 index cd6721b..0000000 --- a/src/components/storage/ShoppingCartClearDialog.tsx +++ /dev/null @@ -1,45 +0,0 @@ -'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 deleted file mode 100644 index 6bd6efd..0000000 --- a/src/components/storage/ShoppingCartLink.tsx +++ /dev/null @@ -1,55 +0,0 @@ -'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 deleted file mode 100644 index 703105c..0000000 --- a/src/components/storage/ShoppingCartTable.tsx +++ /dev/null @@ -1,117 +0,0 @@ -'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 deleted file mode 100644 index 62a3aef..0000000 --- a/src/components/storage/ShoppingCartTableSkeleton.tsx +++ /dev/null @@ -1,69 +0,0 @@ -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 new file mode 100644 index 0000000..8c3cafb --- /dev/null +++ b/src/components/storage/SkeletonCard.tsx @@ -0,0 +1,17 @@ +import { Skeleton } from '@/components/ui/Skeleton'; + +export function SkeletonCard() { + return ( +
+
+ + + +
+
+ + +
+
+ ); +} diff --git a/src/components/ui/Calendar.tsx b/src/components/ui/Calendar.tsx deleted file mode 100644 index 905dbbc..0000000 --- a/src/components/ui/Calendar.tsx +++ /dev/null @@ -1,68 +0,0 @@ -'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 ac5b4b8..0479465 100644 --- a/src/components/ui/Card.tsx +++ b/src/components/ui/Card.tsx @@ -1,10 +1,6 @@ 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 @@ -32,22 +28,19 @@ const CardHeader = React.forwardRef< )); CardHeader.displayName = 'CardHeader'; -const CardTitle = React.forwardRef( - ({ level = 'h3', className, ...props }, ref) => { - const Component = level; - - return ( - - ); - }, -); +const CardTitle = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +

+)); CardTitle.displayName = 'CardTitle'; const CardDescription = React.forwardRef< diff --git a/src/components/ui/Combobox.tsx b/src/components/ui/Combobox.tsx index d51c3b2..57a4b93 100644 --- a/src/components/ui/Combobox.tsx +++ b/src/components/ui/Combobox.tsx @@ -27,9 +27,6 @@ type ComboboxProps = { defaultPlaceholder: string; buttonClassName?: string; contentClassName?: string; - valueCallback?: (value: string | null) => void; - initialValue?: string | null; - ariaLabel?: string; }; function Combobox({ @@ -38,12 +35,9 @@ function Combobox({ defaultPlaceholder, buttonClassName, contentClassName, - valueCallback, - initialValue, - ariaLabel, }: ComboboxProps) { const [open, setOpen] = React.useState(false); - const [value, setValue] = React.useState(initialValue ?? ''); + const [value, setValue] = React.useState(''); return ( @@ -52,7 +46,6 @@ function Combobox({ variant='outline' role='combobox' aria-expanded={open} - aria-label={ariaLabel} className={cx('w-[200px] justify-between', buttonClassName)} > {value @@ -72,14 +65,8 @@ function Combobox({ key={choice.value} value={choice.value} onSelect={(currentValue) => { - // Set newValue to null if user selects the same value twice - const newValue = - currentValue === value ? null : currentValue; - setValue(newValue); + setValue(currentValue === value ? '' : currentValue); 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 deleted file mode 100644 index c945d9f..0000000 --- a/src/components/ui/Form.tsx +++ /dev/null @@ -1,179 +0,0 @@ -'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 ( -