diff --git a/.claude/skills/document-subsystem/SKILL.md b/.claude/skills/document-subsystem/SKILL.md new file mode 100644 index 00000000..87a7c872 --- /dev/null +++ b/.claude/skills/document-subsystem/SKILL.md @@ -0,0 +1,32 @@ +--- +name: document-subsystem +description: Write a detailed reference doc (docs/.md) for one part of the Disscount app (auth, PWA, i18n, shopping-lists, price-history, etc.), in the style of docs/DEPLOYMENT.md. Explore the code first, then document what it is, how it works, automatic vs manual, key files, config/env, libraries, gotchas, and future TODOs. Use when the user asks to document, write docs for, or create a reference for a feature or subsystem. +--- + +# Document a subsystem + +Write a detailed, beginner-friendly reference doc for the part of the app named in the arguments (for example `auth`, `pwa`, `i18n`, `shopping-lists`, `price-history`). If no subsystem was given, ask which one before starting. + +## Steps + +1. **Explore first, do not guess.** Find and read the actual code for this subsystem before writing anything: the relevant routes, components, hooks, context, config, env vars, message files, and libraries. Follow imports until you understand the flow end to end. If something is ambiguous, check the code or ask. + +2. **Write `docs/.md`** (uppercase kebab of the subsystem, e.g. `AUTH.md`, `PWA.md`, `I18N.md`) covering: + - What it is and how it works, end to end (add a Mermaid flow diagram if it helps). + - What is automatic vs what needs manual work (a table). + - The key files and where they live (a table with paths and roles). + - Config, env vars, and feature flags involved. + - Libraries used, with versions read from `frontend/package.json` or `backend/pom.xml`. + - Gotchas and lessons that would bite someone (the traps, with the fix). + - A final "Future improvements & TODOs" section. + +Assume the reader is still learning, so explain the concepts clearly. Use tables and Mermaid diagrams wherever they make it easier to follow. + +## Conventions + +- The repo is PUBLIC: never include secrets, tokens, real IPs, or private URLs; use placeholders. +- No em dashes. +- Do not hard-wrap Markdown; write one physical line per paragraph and per bullet, and let the editor wrap. +- Match the structure and tone of `docs/DEPLOYMENT.md`. + +Do one subsystem per run so the exploration stays focused. The finished file is added to a PR into `dev`, then `master`. diff --git a/.gitignore b/.gitignore index fa63d3ed..12aa6c64 100644 --- a/.gitignore +++ b/.gitignore @@ -66,4 +66,6 @@ frontend/yarn.lock *.bak *.orig -desktop.ini \ No newline at end of file +desktop.ini + +docs/disscount_* \ No newline at end of file diff --git a/AGENTS.md b/AGENTS.md index ade7f624..4166322b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -94,6 +94,15 @@ Never edit the package.json or package-lock.json files directly, but instead use If you need docs about a library, always fetch the most recent documentation from the official website or repository, instead of searching in node modules or other places. +## Internationalization (i18n) + +The app uses `next-intl` (cookie-based locale, no URL routing). Locales: `hr` (default), `en`, `de`, `sl`; catalogs in `frontend/src/i18n/messages/.json`. + +- NEVER hardcode user-facing text. The `i18next/no-literal-string` ESLint rule (`pnpm lint`) fails on literal JSX text and on the `placeholder`, `alt`, `title`, `aria-label`, and `label` attributes. +- Add every new key to ALL four catalogs (`hr` is the reference) and read it with `useTranslations` (client) / `getTranslations` (server). Use `t.rich` for embedded links/icons, ICU `{count, plural, …}` for counts, `useFormatter` for numbers/currency/dates. Emails are localized separately via `getEmailTranslator(locale)`. +- Before committing run `pnpm i18n:check` (catalog parity) and `pnpm exec tsc --noEmit` (verifies every `t("key")` exists). +- Never use em dashes in any copy. + # Backend Development Guidelines NEVER run "mvn spring-boot:run" or any other development server command, because I always already have my dev server running. Also never run build commands. diff --git a/docs/I18N.md b/docs/I18N.md new file mode 100644 index 00000000..e47423e3 --- /dev/null +++ b/docs/I18N.md @@ -0,0 +1,423 @@ +# Disscount: Internationalization (i18n) Guide + +A complete reference for how Disscount is translated into multiple languages, written to be understandable even if you are new to i18n. Keep it up to date as the setup changes. + +*Last verified on 2026-07-04: 652 keys in parity across hr/en/de/sl, `pnpm i18n:check` green, `pnpm exec tsc --noEmit` clean, `no-literal-string` clean.* + +> **Mental model in one sentence:** every user-facing string is a **key** looked up at render time in a per-language JSON catalog; next-intl picks the language from a **cookie** (falling back to the browser's `Accept-Language`), so the URL never changes, and an ESLint rule plus a parity script make sure no string is ever hardcoded or left untranslated. + +--- + +## Table of contents +1. [Quick reference](#1-quick-reference) +2. [How it works (end to end)](#2-how-it-works-end-to-end) +3. [Locale resolution & the language switcher](#3-locale-resolution--the-language-switcher) +4. [Message catalogs & namespaces](#4-message-catalogs--namespaces) +5. [Type safety, fallback & time zone](#5-type-safety-fallback--time-zone) +6. [Emails (localized separately)](#6-emails-localized-separately) +7. [Guardrails: the ESLint rule & parity script](#7-guardrails-the-eslint-rule--parity-script) +8. [Key files](#8-key-files) +9. [What's automatic vs manual](#9-whats-automatic-vs-manual) +10. [How to: add a language / add a string](#10-how-to-add-a-language--add-a-string) +11. [Language vs market (the two-axis design)](#11-language-vs-market-the-two-axis-design) +12. [Gotchas & lessons learned](#12-gotchas--lessons-learned) +13. [Files that had hardcoded Croatian (migration checklist)](#13-files-that-had-hardcoded-croatian-migration-checklist) +14. [Future improvements & TODOs](#14-future-improvements--todos) + +--- + +## 1. Quick reference + +| Thing | Value | +|---|---| +| Library | `next-intl` `^4.13.0` | +| Locales | `hr` (default) · `en` · `de` · `sl` | +| Strategy | Cookie-based, **no** URL routing (URLs stay `/products`, `/shopping-lists`) | +| Cookie | `NEXT_LOCALE` (1 year, `sameSite=lax`) | +| First-visit fallback | `Accept-Language` header, then `hr` | +| Catalog size | **652 keys** per locale, full parity (2,608 strings) | +| Catalogs | `frontend/src/i18n/messages/{hr,en,de,sl}.json` | +| Time zone | fixed `Europe/Zagreb` (avoids date hydration mismatches) | +| Type-safe keys | yes, via `frontend/src/typings/next-intl.d.ts` | +| Lint guard | `i18next/no-literal-string` (fails CI on hardcoded strings) | +| Parity check | `pnpm i18n:check` | +| New env vars | none | + +**Daily workflow when adding text:** add the key to all four catalogs (`hr` is the reference), read it with `useTranslations` / `getTranslations`, then run `pnpm i18n:check` and `pnpm exec tsc --noEmit`. + +--- + +## 2. How it works (end to end) + +Nothing about the language lives in the URL. On every request, a small config function reads the locale, loads the matching catalog, and hands the messages to both Server and Client Components through a provider. Components never hold Croatian text; they hold keys. + +```mermaid +flowchart TD + A[Incoming request] --> B{NEXT_LOCALE cookie valid?} + B -->|yes| C[use cookie locale = explicit choice] + B -->|no| D{Accept-Language matches hr/en/de/sl?} + D -->|yes| E[use browser locale] + D -->|no| F[defaultLocale = hr] + C --> G[request.ts: load messages/<locale>.json] + E --> G + F --> G + G --> H[RootLayout: <html lang> + IntlClientProvider] + H --> I[Server Components: await getTranslations ns] + H --> J[Client Components: useTranslations ns / t.rich / useFormatter] +``` + +The two ways to read a string: + +- **Server Components** call `const t = await getTranslations("namespace")` from `next-intl/server`. +- **Client Components** call `const t = useTranslations("namespace")` from `next-intl`. + +Both return a `t("key")` function. `t.rich("key", { tag: (chunks) => {chunks} })` renders embedded links or icons, and counts use ICU plurals (`{count, plural, one {...} few {...} other {...}}`) so Croatian and Slovenian plural rules are correct, not just singular/plural. + +--- + +## 3. Locale resolution & the language switcher + +`request.ts` resolves the active locale with a clear priority so an explicit choice always wins and a first-time visitor still gets a sensible default: + +1. `NEXT_LOCALE` cookie (the user's explicit choice, written by the switcher). +2. `Accept-Language` header (the browser's preference), matched on the primary subtag so `de-AT` maps to `de`. +3. `defaultLocale` (`hr`). + +The switcher is `frontend/src/components/custom/language-switcher.tsx`. It has two variants driven by a `variant` prop, so the language logic lives in one place: + +| Variant | Where | Look | +|---|---|---| +| `icon` (default) | header (guests only) and footer (always) | icon button; icon goes muted to primary on hover, like the other header/footer icons | +| `sidebar` | mobile sidebar, above the PWA install banner | full-width sidebar row with a `Languages` icon and a translated "Language" label | + +On mobile the header/footer switchers are hidden (`hidden md:flex`) and the sidebar one takes over (`md:hidden`), matching the convention already used for the dashboard and header-only items in the sidebar. + +Selecting a language calls the `setLocale` server action (`frontend/src/i18n/locale-actions.ts`), which writes the `NEXT_LOCALE` cookie, then `router.refresh()` re-renders with the new locale. Because it is a refresh and not a navigation, the sidebar stays open. + +```mermaid +flowchart LR + U[User picks a language] --> S[LanguageSwitcher] + S --> A["setLocale() server action writes NEXT_LOCALE cookie"] + A --> R["router.refresh()"] + R --> Q[next render re-resolves the locale from the cookie] +``` + +The language names in the dropdown are **endonyms** (each language written in itself: Hrvatski, English, Deutsch, Slovenščina). These are intentionally not translated, which is the conventional way to list languages. + +--- + +## 4. Message catalogs & namespaces + +Catalogs live in `frontend/src/i18n/messages/.json` and are organized by feature namespace, mirroring the app's feature folders. `hr.json` is the reference: it defines the full set of keys and also drives the TypeScript types. The other three mirror its keys exactly. + +Representative namespaces: + +| Namespace | Covers | +|---|---| +| `common` | shared verbs and labels (save, cancel, add, search, loading), endonym helpers, placeholders | +| `metadata`, `pages.*` | root and per-page metadata, plus page content (products, watchlist, shopping-lists, statistics, dashboard, digital-cards, suggestions, updates, map, offline, notFound, resetPassword, ...) | +| `navigation` | sidebar and header nav labels, keyed by item `id` | +| `auth` (+ `auth.modal`, `auth.oauthErrors`, ...) | login/signup/forgot forms, auth modal, social-login errors | +| `userMenu`, `settings.*` | user menu and the preferences/profile/security modals | +| `productDetail`, `addToList`, `watchModal`, `scanner` | product page, both modals, camera scanner | +| `shoppingListDetail` (+ `.toasts`) | list detail, item rows, availability tables, mutation-hook toasts | +| `priceHistory`, `statistics` | charts, period buttons, chain stats | +| `accountTypes`, `acquisitionChannels` | constant label maps rendered via i18n | +| `offline`, `pwa` | offline banners, install prompts (rich text) | +| `validation` | Zod message strings | +| `emails` | transactional email copy (see section 6) | + +Data-driven labels (navigation items, account types, acquisition channels) keep their `id` or enum value in the data file and are translated at render time by key, so the data files never store display strings. + +--- + +## 5. Type safety, fallback & time zone + +**Type-safe keys.** `frontend/src/typings/next-intl.d.ts` augments next-intl's `AppConfig` so `Messages` equals the shape of `hr.json`. That means `t("some.key")` is checked at compile time: a typo or a missing key is a TypeScript error, so `pnpm exec tsc --noEmit` passing is a strong guarantee that no key is dangling. + +**Missing-key fallback.** `frontend/src/i18n/message-fallback.ts` provides `getMessageFallback`, which resolves a missing key from the default (`hr`) catalog instead of showing the raw `namespace.key` path, and `onIntlError`, which swallows the "missing message" error so it does not throw. These are wired on the server in `request.ts` and on the client through `intl-client-provider.tsx`. Parity is enforced (section 7), so this is a safety net, not an everyday path. + +**Time zone.** `config.ts` exports a fixed `TIME_ZONE = "Europe/Zagreb"`, applied on the server config, the client provider, and the email translator. A fixed zone (rather than the server's own zone) guarantees the server-rendered HTML and the client's first render format dates identically, which prevents hydration mismatches. Without it, next-intl raises an `ENVIRONMENT_FALLBACK` warning as soon as anything formats a date. + +**The client wrapper.** `getMessageFallback` and `onIntlError` are functions, and functions cannot be passed from a Server Component to a Client Component across the React boundary. That is why `intl-client-provider.tsx` is a small `"use client"` module: it imports those functions and hands them to `NextIntlClientProvider` from inside the client boundary. + +--- + +## 6. Emails (localized separately) + +Transactional emails (verification, password reset, set password, change-email confirmation) are rendered with react-email **outside** a normal request render, so they cannot use `getTranslations` (which is request-bound). Instead they use `getEmailTranslator(locale)` in `frontend/src/emails/email-translator.ts`, which builds a translator with next-intl's `createTranslator` over the `emails` namespace. + +The recipient's locale is resolved at send time by `getRequestLocale()` (`frontend/src/i18n/get-request-locale.ts`), which reads the cookie or `Accept-Language` the same way `request.ts` does. `lib/auth.ts` calls it inside each Better Auth email callback and threads the locale through `EmailService` into the templates. + +```mermaid +flowchart LR + CB[Better Auth email callback] --> GL["getRequestLocale()"] + GL --> ES[EmailService.send...] + ES --> ET["getEmailTranslator(locale)"] + ET --> TPL[react-email template renders in the recipient's language] +``` + +The templates keep their react-email `PreviewProps` (locale defaults to `hr`) so the `pnpm email` preview server still works. + +--- + +## 7. Guardrails: the ESLint rule & parity script + +Two checks make the translation coverage self-defending, so a regression fails CI instead of shipping. + +**`i18next/no-literal-string`** (from `eslint-plugin-i18next` `^6.1.5`, configured in `frontend/eslint.config.mjs`, run by `pnpm lint`). It fails on any hardcoded JSX text and on the user-visible attributes `placeholder`, `alt`, `title`, `aria-label`, and `label`. It is tuned to zero false positives with: + +- `callees.exclude`: skip the key argument of translation and utility calls (`t`, `t*`, `nav.label`, `form.register`, `localeCompare`, and similar), so `t("key")` is not itself flagged. +- `words.exclude`: allow symbol/number-only text, hex color examples, and intentional proper names (Disscount, Jakov Jakovac, GitHub, LinkedIn, N/A). +- `ignores`: the shadcn `ui/` primitives, unused scaffolds (`sidebar-08/`, `shadcn-studio/`, the unused landing `sections/*`), and `emails/` (localized separately). + +Because the rule's `jsx-only` mode also flags string literals inside JSX expression containers, one tailwind class ternary in `store-chain-select.tsx` was hoisted into a module-level helper so its class names live in plain JS rather than JSX. + +**`pnpm i18n:check`** (`frontend/scripts/check-i18n.mjs`). It flattens every catalog to dotted key paths and compares each locale against `hr`, printing any missing or extra keys and exiting non-zero on a mismatch. Suitable for CI. + +`pnpm exec tsc --noEmit` is the third leg: it verifies every `t("key")` actually exists in the catalog shape. + +--- + +## 8. Key files + +| Path | Role | +|---|---| +| `frontend/src/i18n/config.ts` | single source of truth: `locales`, `defaultLocale`, `LOCALE_COOKIE`, `TIME_ZONE`, `isLocale`, `matchLocale` | +| `frontend/src/i18n/request.ts` | `getRequestConfig`: resolves the locale per request, loads the catalog, sets fallback + time zone | +| `frontend/src/i18n/locale-actions.ts` | `"use server"` `setLocale(locale)`: writes the `NEXT_LOCALE` cookie | +| `frontend/src/i18n/get-request-locale.ts` | server helper to resolve the locale outside a render (used by emails) | +| `frontend/src/i18n/message-fallback.ts` | `getMessageFallback` (falls back to `hr`) and `onIntlError` | +| `frontend/src/i18n/intl-client-provider.tsx` | `"use client"` wrapper adding fallback/onError/time zone to `NextIntlClientProvider` | +| `frontend/src/i18n/messages/{hr,en,de,sl}.json` | the message catalogs (`hr` is the reference) | +| `frontend/src/typings/next-intl.d.ts` | type augmentation for compile-time key checking | +| `frontend/src/hooks/use-nav-translation.ts` | translate nav items by `id` (`label` / `short`) | +| `frontend/src/components/custom/language-switcher.tsx` | the switcher (icon + sidebar variants) | +| `frontend/src/emails/email-translator.ts` | `getEmailTranslator`: a `createTranslator` bound to a locale for emails | +| `frontend/next.config.ts` | wires `createNextIntlPlugin("./src/i18n/request.ts")` | +| `frontend/src/app/layout.tsx` | dynamic ``, `generateMetadata`, `IntlClientProvider` | +| `frontend/eslint.config.mjs` | `no-literal-string` rule configuration | +| `frontend/scripts/check-i18n.mjs` | catalog parity checker (`pnpm i18n:check`) | + +--- + +## 9. What's automatic vs manual + +| Automatic | Manual | +|---|---| +| Locale detection on first visit (Accept-Language) | Choosing a language (footer/sidebar switcher) | +| Loading the right catalog per request | Adding a new key to all four catalogs | +| Compile-time checking that every `t("key")` exists | Translating the value in `en` / `de` / `sl` | +| Failing CI on a hardcoded string (`no-literal-string`) | Fixing the flagged string by wrapping it in `t()` | +| Failing CI on a catalog mismatch (`i18n:check`) | Keeping the four catalogs in parity | +| Correct plural forms via ICU + `Intl.PluralRules` | Writing the ICU `{count, plural, ...}` message | +| Falling back to `hr` for a missing key | (nothing: this is a safety net) | +| Localized email language at send time | Adding new email copy to the `emails` namespace | + +--- + +## 10. How to: add a language / add a string + +**Add a new language (about 5 minutes).** + +1. Add the code to `locales` in `frontend/src/i18n/config.ts` (for example `"it"`). +2. Add its endonym to `LANGUAGE_NAMES` in `language-switcher.tsx` (for example `it: "Italiano"`). +3. Create `frontend/src/i18n/messages/it.json` mirroring every key in `hr.json`. +4. Run `pnpm i18n:check` (must pass) and `pnpm exec tsc --noEmit`. + +The switcher, provider, detection, and type-checking pick it up automatically. This is exactly how `sl` was added on top of `hr`/`en`/`de`. + +**Add or change a user-facing string.** + +1. Add the key to a suitable namespace in `hr.json`, then translate it in `en.json`, `de.json`, `sl.json`. +2. Read it in the component: `useTranslations` (client) or `getTranslations` (server). Use `t.rich` for embedded links/icons, ICU plurals for counts, and `useFormatter` for numbers/currency/dates. +3. Never hardcode the text: the ESLint rule will fail on it. +4. Run `pnpm i18n:check` and `pnpm exec tsc --noEmit`. + +--- + +## 11. Language vs market (the two-axis design) + +`docs/disscount_knowledge_base.md` section 19 specifies i18n as **two independent axes**, and only the first is implemented today: + +```ts +language: 'hr' | 'en' | 'de' | 'sl' // implemented (this subsystem) +market: 'HR' | 'AT' | 'SI' // not implemented (future) +``` + +**Language** is what this document covers: the UI text the user reads. **Market** is which country's stores, price sources, currency, and retailer data are shown. They are independent, so combinations like "English language + Croatia market" or "Croatian language + Austria market" are intended. + +When the market axis is built, it should be a **separate** setting (its own cookie/preference), not an overload of `NEXT_LOCALE`. The fixed `Europe/Zagreb` time zone can then derive from the market instead of being constant (all current markets HR/AT/SI happen to share CET). + +--- + +## 12. Gotchas & lessons learned + +| Trap | Fix | +|---|---| +| Reading the cookie / headers opts routes into dynamic rendering | Expected and unavoidable for cookie-based i18n without routing; not a bug | +| `no-literal-string` in `jsx-only` mode also flags `t("key")` arguments and className ternaries | Exclude translation/util callees via `callees.exclude`; hoist class ternaries into plain-JS helpers | +| `getMessageFallback` / `onError` cannot be passed from Server to Client Components (they are functions) | Wire them inside a `"use client"` module (`intl-client-provider.tsx`) | +| next-intl warns `ENVIRONMENT_FALLBACK` when a date is formatted with no time zone | Set a global `TIME_ZONE` on server config, client provider, and email translator | +| Emails render outside a request, so `getTranslations` is unavailable | Use `createTranslator` (via `getEmailTranslator`) with the locale resolved by `getRequestLocale` at send time | +| Croatian and Slovenian have three plural forms, not two | Use ICU `{count, plural, one {} few {} other {}}`, not a binary singular/plural helper | +| `pnpm exec tsc` sometimes runs a deps-status check that fails on ignored build scripts | Run `./node_modules/.bin/tsc --noEmit` directly if that happens | +| Endonyms look like untranslated strings | They are intentional (languages named in themselves); keep them out of the catalogs | +| Currency and dates still render the same in every locale | Known gap: `.toFixed(2)€` and the custom `formatDate` are not yet locale-aware (see TODOs) | +| Em dashes in copy | Never use em dashes anywhere, including catalog values | + +--- + +## 13. Files that had hardcoded Croatian (migration checklist) + +Every file below contained hardcoded user-facing Croatian and was migrated to `t()` / `t.rich()` in this branch. If the app is ever re-internationalized from scratch, use this as the checklist of files to re-verify. It does **not** include the i18n infrastructure files (catalogs, `config.ts`, `request.ts`, the provider, `next-intl.d.ts`, the switcher, `use-nav-translation.ts`), which are new rather than migrated. + +Deliberately **not** migrated (intentional, so do not "fix" them): SEO keyword arrays in `layout.tsx`, `manifest.ts` and `navigation.ts` manifest labels, `constants/name-mappings.ts` (store/city proper nouns), mock content data (`updates/posts.ts`, `suggestions/suggestions.ts`), the `Slovenščina` endonym, and the deferred Zod messages in `lib/api/schemas/*` (see TODOs). + +### Root, layout & legal +- [ ] `app/layout.tsx` +- [ ] `app/not-found.tsx` +- [ ] `app/(root)/page.tsx` +- [ ] `app/(root)/components/sections/hero-section.tsx` +- [ ] `app/(root)/components/sections/hero-actions.tsx` +- [ ] `app/privacy-policy/page.tsx` +- [ ] `app/terms-of-service/page.tsx` +- [ ] `app/data-deletion/page.tsx` +- [ ] `components/custom/legal-page.tsx` + +### Header, footer, sidebar & shared UI +- [ ] `components/custom/header/header.tsx` +- [ ] `components/custom/header/user-menu.tsx` +- [ ] `components/custom/header/notifications-dropdown.tsx` +- [ ] `components/custom/footer.tsx` +- [ ] `components/custom/app-sidebar.tsx` +- [ ] `components/custom/search-bar.tsx` +- [ ] `components/custom/coming-soon.tsx` +- [ ] `components/custom/no-results.tsx` +- [ ] `components/custom/camera-scanner.tsx` +- [ ] `components/custom/view-switcher.tsx` +- [ ] `components/custom/price-history-period-select.tsx` +- [ ] `components/custom/store-chain-select.tsx` +- [ ] `components/custom/store-chain-multi-select.tsx` +- [ ] `components/custom/oauth-error-toast.tsx` +- [ ] `components/custom/offline/offline-indicator.tsx` +- [ ] `components/custom/offline/last-synced-label.tsx` +- [ ] `components/custom/pwa/install-banner.tsx` +- [ ] `components/custom/pwa/install-sidebar-banner.tsx` +- [ ] `components/custom/pwa/install-instructions-sheet.tsx` + +### Auth (forms & modals) +- [ ] `components/custom/header/forms/auth-modal.tsx` +- [ ] `components/custom/header/forms/login-form.tsx` +- [ ] `components/custom/header/forms/signup-form.tsx` +- [ ] `components/custom/header/forms/forgot-password-form.tsx` +- [ ] `components/custom/header/forms/inbox-notice.tsx` +- [ ] `components/custom/header/forms/account-credentials-form.tsx` +- [ ] `components/custom/header/forms/linked-accounts.tsx` +- [ ] `components/custom/header/forms/profile-modal.tsx` +- [ ] `components/custom/header/forms/security-modal.tsx` +- [ ] `components/custom/header/forms/user-preferences-modal.tsx` +- [ ] `app/reset-password/page.tsx` +- [ ] `app/reset-password/reset-password-modal.tsx` + +### Products & product detail +- [ ] `app/products/page.tsx` +- [ ] `app/products/components/products-client.tsx` +- [ ] `app/products/components/product-info-display.tsx` +- [ ] `app/products/components/product-info-table.tsx` +- [ ] `app/products/components/product-action-buttons.tsx` +- [ ] `app/products/components/watchlist-action-button.tsx` +- [ ] `app/products/components/product-item/product-info.tsx` +- [ ] `app/products/components/product-item/product-price.tsx` +- [ ] `app/products/components/forms/add-to-shopping-list-form.tsx` +- [ ] `app/products/components/forms/shopping-list-selector.tsx` +- [ ] `app/products/components/forms/quantity-input.tsx` +- [ ] `app/products/components/forms/mark-as-checked-checkbox.tsx` +- [ ] `app/products/components/forms/watchlist-item-modal.tsx` +- [ ] `app/products/[id]/page.tsx` +- [ ] `app/products/[id]/components/product-detail-client.tsx` +- [ ] `app/products/[id]/components/store-item/store-item.tsx` +- [ ] `app/products/[id]/components/store-item/store-prices-table.tsx` +- [ ] `app/products/[id]/components/price-history/price-history-base.tsx` +- [ ] `app/products/[id]/components/price-history/price-history-chart.tsx` + +### Shopping lists +- [ ] `app/(user)/shopping-lists/page.tsx` +- [ ] `app/(user)/shopping-lists/components/shopping-lists-client.tsx` +- [ ] `app/(user)/shopping-lists/components/shopping-list-item.tsx` +- [ ] `app/(user)/shopping-lists/components/create-shopping-list-button.tsx` +- [ ] `app/(user)/shopping-lists/components/forms/shopping-list-modal.tsx` +- [ ] `app/(user)/shopping-lists/components/forms/delete-shopping-list-dialog.tsx` +- [ ] `app/(user)/shopping-lists/hooks/use-shopping-list-modal.ts` +- [ ] `app/(user)/shopping-lists/[id]/page.tsx` +- [ ] `app/(user)/shopping-lists/[id]/components/shopping-list-detail-client.tsx` +- [ ] `app/(user)/shopping-lists/[id]/components/shopping-list-header.tsx` +- [ ] `app/(user)/shopping-lists/[id]/components/shopping-list-info-table.tsx` +- [ ] `app/(user)/shopping-lists/[id]/components/shopping-list-action-buttons.tsx` +- [ ] `app/(user)/shopping-lists/[id]/components/shopping-list-price-history.tsx` +- [ ] `app/(user)/shopping-lists/[id]/components/items/shopping-list-items.tsx` +- [ ] `app/(user)/shopping-lists/[id]/components/items/shopping-list-item.tsx` +- [ ] `app/(user)/shopping-lists/[id]/components/stores/shopping-list-stores-list.tsx` +- [ ] `app/(user)/shopping-lists/[id]/components/stores/shopping-list-store-card.tsx` +- [ ] `app/(user)/shopping-lists/[id]/components/stores/shopping-list-items-table.tsx` +- [ ] `app/(user)/shopping-lists/[id]/hooks/use-shopping-list-mutations.ts` +- [ ] `app/(user)/shopping-lists/[id]/hooks/use-shopping-list-item-mutations.ts` + +### Watchlist +- [ ] `app/(user)/watchlist/page.tsx` +- [ ] `app/(user)/watchlist/components/watchlist-client.tsx` +- [ ] `app/(user)/watchlist/components/watchlist-item.tsx` +- [ ] `app/(user)/watchlist/components/watchlist-item-discount-info.tsx` +- [ ] `app/(user)/watchlist/components/create-discounted-list-button.tsx` + +### Digital cards +- [ ] `app/(user)/digital-cards/page.tsx` +- [ ] `app/(user)/digital-cards/components/digital-cards-client.tsx` +- [ ] `app/(user)/digital-cards/components/digital-card-item.tsx` +- [ ] `app/(user)/digital-cards/components/forms/digital-card-modal.tsx` + +### Other pages +- [ ] `app/(user)/spending/page.tsx` +- [ ] `app/map/page.tsx` +- [ ] `app/map/components/map-client.tsx` +- [ ] `app/suggestions/page.tsx` +- [ ] `app/suggestions/components/suggestions-client.tsx` +- [ ] `app/suggestions/[id]/page.tsx` +- [ ] `app/suggestions/[id]/components/suggestion-details-client.tsx` +- [ ] `app/updates/page.tsx` +- [ ] `app/updates/components/updates-client.tsx` +- [ ] `app/updates/[id]/page.tsx` +- [ ] `app/offline/page.tsx` +- [ ] `app/offline/components/offline-retry-button.tsx` + +### Dashboard & statistics +- [ ] `app/dashboard/page.tsx` +- [ ] `app/dashboard/components/dashboard-content.tsx` +- [ ] `app/dashboard/components/admin-users-table.tsx` +- [ ] `app/statistics/page.tsx` +- [ ] `app/statistics/components/health-status.tsx` +- [ ] `app/statistics/components/store-item.tsx` +- [ ] `app/statistics/components/stores-list.tsx` + +### Emails & their wiring +- [ ] `emails/verification-email.tsx` +- [ ] `emails/password-reset-email.tsx` +- [ ] `emails/set-password-email.tsx` +- [ ] `emails/change-email-confirmation.tsx` +- [ ] `emails/components/action-email.tsx` +- [ ] `emails/components/email-layout.tsx` +- [ ] `lib/email/email-service.ts` +- [ ] `lib/auth.ts` + +--- + +## 14. Future improvements & TODOs + +- **Locale-aware currency, number, and date formatting.** The knowledge base section 19 explicitly requires this, and it is the largest remaining gap. Prices still render as `.toFixed(2)€` and dates via a custom `formatDate`, so a German user sees Croatian-style numbers. Replace with `useFormatter().number(v, { style: "currency", currency: "EUR" })` and `format.dateTime(...)`, ideally behind one shared helper reused everywhere. +- **Market / country axis.** Implement `market: 'HR' | 'AT' | 'SI'` as a separate setting (see section 11), which controls stores, price sources, and currency independently of language. +- **Zod validation messages in `lib/api/schemas/*`.** These schemas are double-duty (form validation and API-response parsing via the `@/lib/api/types` barrel), so translating them cleanly needs a message-object factory plus passthrough static instances for the DTO schemas. The `validation` namespace keys are already in the catalogs; only the form-only inline schemas (forgot-password, reset-password) are translated so far. +- **Persist the locale to the user profile.** Today the cookie is the source of truth, so a logged-in user on a new device gets detection or the default. A `language` field via better-auth `additionalFields`, synced on login, would make the preference follow the account. +- **Native review of `de` and `sl`.** Current German and Slovenian are solid machine-quality; a native pass (or a TMS like Tolgee/Crowdin/Lokalise if this grows) is the professional next step. +- **Backend-generated content.** Product labels/categories (from the cijene API) and notification content are not covered by frontend i18n; localizing them is a backend concern. +- **SEO in other languages.** Cookie-based i18n means search engines only index the default (`hr`). If ranking `en`/`de`/`sl` content matters later, that requires URL-based routing plus `hreflang` tags and per-locale sitemaps, a deliberate trade-off away from the current simplicity. +- **Update the stale docs.** `disscount_technical_documentation.md` and `disscount_state_of_reality.md` still describe the app as "Croatian only / i18n not wired"; refresh them now that this subsystem is live. diff --git a/frontend/.gitignore b/frontend/.gitignore index 36e82fe4..2eabf9ca 100644 --- a/frontend/.gitignore +++ b/frontend/.gitignore @@ -17,6 +17,10 @@ /.next/ /out/ +# serwist (generated service worker) +public/sw* +public/swe-worker* + # production /build diff --git a/frontend/eslint.config.mjs b/frontend/eslint.config.mjs index f4438352..f9abddc0 100644 --- a/frontend/eslint.config.mjs +++ b/frontend/eslint.config.mjs @@ -1,5 +1,6 @@ import { defineConfig, globalIgnores } from "eslint/config"; import nextVitals from "eslint-config-next/core-web-vitals"; +import i18next from "eslint-plugin-i18next"; const eslintConfig = defineConfig([ ...nextVitals, @@ -11,6 +12,76 @@ const eslintConfig = defineConfig([ "build/**", "next-env.d.ts", ]), + + // Enforce that user-facing JSX text goes through next-intl (no hardcoded + // strings — this is what keeps translations from regressing). Only JSX text + // nodes are checked; plain .ts (schemas/constants/mock data) and technical + // attributes are ignored to avoid noise. + { + files: ["src/**/*.tsx"], + ignores: [ + "src/components/ui/**", // shadcn primitives (library code) + "src/components/custom/sidebar-08/**", // unused scaffold + "src/components/shadcn-studio/**", // unused scaffold + "src/emails/**", // localized via the email service, not next-intl + // Unused landing-page scaffolds (only HeroSection is rendered). + "src/app/(root)/components/sections/features-section.tsx", + "src/app/(root)/components/sections/features-section2.tsx", + "src/app/(root)/components/sections/pricing-section.tsx", + "src/app/(root)/components/sections/stats-section.tsx", + "src/app/(root)/components/sections/stores-section.tsx", + ], + plugins: { i18next }, + rules: { + "i18next/no-literal-string": [ + "error", + { + mode: "jsx-only", + message: + "Hardcoded string. Use next-intl (useTranslations) instead.", + // Check JSX text plus these user-visible attributes only. + "jsx-attributes": { + include: ["placeholder", "alt", "title", "aria-label", "label"], + }, + // Don't flag the key argument of translation/util calls, e.g. + // t("key"), tCommon("key"), nav.label("id"), form.register("email"), + // "x".localeCompare(y, "hr"). + callees: { + exclude: [ + "^t$", + "^t[A-Z]\\w*$", + "^tv$", + "rich", + "label", + "short", + "localeCompare", + "register", + "watch", + "setValue", + "getValues", + "useTranslations", + "getTranslations", + ], + }, + words: { + exclude: [ + // symbol / number / emoji-only text (separators, prices, etc.) + "^[^A-Za-zČĆŽŠĐčćžšđ]*$", + // hex color examples (e.g. #ffffff placeholder) + "^#[0-9A-Fa-f]{3,8}$", + // brand + proper names + universal tokens (not translated) + "Disscount", + "disscount", + "Jakov Jakovac", + "GitHub", + "LinkedIn", + "N/A", + ], + }, + }, + ], + }, + }, ]); export default eslintConfig; diff --git a/frontend/next.config.ts b/frontend/next.config.ts index 826c6e9e..a436f5fe 100644 --- a/frontend/next.config.ts +++ b/frontend/next.config.ts @@ -1,6 +1,23 @@ import { withSentryConfig } from "@sentry/nextjs"; +import withSerwistInit from "@serwist/next"; +import createNextIntlPlugin from "next-intl/plugin"; +import { randomUUID } from "node:crypto"; import type { NextConfig } from "next"; +// Points at the per-request locale + messages config (src/i18n/request.ts). +const withNextIntl = createNextIntlPlugin("./src/i18n/request.ts"); + +const withSerwist = withSerwistInit({ + swSrc: "src/app/sw.ts", + swDest: "public/sw.js", + // Service worker only runs in production builds; in dev it would fight + // Turbopack/HMR. The React Query persistence layer still works in dev. + disable: process.env.NODE_ENV === "development", + // Precache the offline fallback page so it's available with no connection. + additionalPrecacheEntries: [{ url: "/offline", revision: randomUUID() }], + reloadOnOnline: true, +}); + const nextConfig: NextConfig = { // Emit a minimal self-contained server build (.next/standalone) for a lean Docker image. output: "standalone", @@ -60,7 +77,8 @@ const nextConfig: NextConfig = { }, }; -export default withSentryConfig(nextConfig, { +export default withSerwist( + withSentryConfig(withNextIntl(nextConfig), { // For all available options, see: // https://www.npmjs.com/package/@sentry/webpack-plugin#options @@ -90,4 +108,5 @@ export default withSentryConfig(nextConfig, { removeDebugLogging: true, }, }, -}); + }), +); diff --git a/frontend/package.json b/frontend/package.json index c8dd52d3..31d0d9ac 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -5,9 +5,10 @@ "license": "BUSL-1.1", "scripts": { "dev": "next dev", - "build": "next build", + "build": "next build --webpack", "start": "next start", "lint": "eslint .", + "i18n:check": "node scripts/check-i18n.mjs", "email": "email dev --dir src/emails --port 3366" }, "dependencies": { @@ -26,8 +27,11 @@ "@radix-ui/react-tabs": "^1.1.13", "@radix-ui/react-tooltip": "^1.2.8", "@sentry/nextjs": "^10.62.0", + "@serwist/next": "^9.5.11", + "@tanstack/query-async-storage-persister": "^5.101.1", "@tanstack/react-query": "^5.90.12", "@tanstack/react-query-devtools": "^5.91.1", + "@tanstack/react-query-persist-client": "^5.101.1", "@tanstack/react-virtual": "^3.13.13", "@yudiel/react-qr-scanner": "^2.4.1", "axios": "^1.13.2", @@ -37,10 +41,12 @@ "cmdk": "^1.1.1", "disscount": "link:", "drizzle-orm": "0.45.2", + "idb-keyval": "^6.2.5", "kysely": "0.28.17", "lucide-react": "^0.561.0", "motion": "^12.23.26", "next": "16.2.9", + "next-intl": "^4.13.0", "pg": "8.21.0", "radix-ui": "^1.4.3", "react": "19.2.4", @@ -68,6 +74,9 @@ "drizzle-kit": "^0.31.10", "eslint": "^9", "eslint-config-next": "16.2.2", + "eslint-plugin-i18next": "^6.1.5", + "serwist": "^9.5.11", + "sharp": "^0.35.2", "tailwindcss": "^4", "tw-animate-css": "^1.4.0", "typescript": "^5.7" diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index e06b4cc6..9de76339 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -10,7 +10,7 @@ importers: dependencies: '@daveyplate/better-auth-ui': specifier: ^3.2.13 - version: 3.4.0(7cba73d98a64fa1f7427e1b78929c147) + version: 3.4.0(2e97e51b7de348ab45b8706c958356e1) '@hookform/resolvers': specifier: ^5.2.2 version: 5.4.0(react-hook-form@7.80.0(react@19.2.4)) @@ -53,12 +53,21 @@ importers: '@sentry/nextjs': specifier: ^10.62.0 version: 10.62.0(@opentelemetry/core@2.8.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.8.0(@opentelemetry/api@1.9.1))(next@16.2.9(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4)(webpack@5.108.1) + '@serwist/next': + specifier: ^9.5.11 + version: 9.5.11(next@16.2.9(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4)(typescript@5.9.3)(webpack@5.108.1) + '@tanstack/query-async-storage-persister': + specifier: ^5.101.1 + version: 5.101.1 '@tanstack/react-query': specifier: ^5.90.12 version: 5.101.1(react@19.2.4) '@tanstack/react-query-devtools': specifier: ^5.91.1 version: 5.101.1(@tanstack/react-query@5.101.1(react@19.2.4))(react@19.2.4) + '@tanstack/react-query-persist-client': + specifier: ^5.101.1 + version: 5.101.1(@tanstack/react-query@5.101.1(react@19.2.4))(react@19.2.4) '@tanstack/react-virtual': specifier: ^3.13.13 version: 3.14.4(react-dom@19.2.4(react@19.2.4))(react@19.2.4) @@ -86,6 +95,9 @@ importers: drizzle-orm: specifier: 0.45.2 version: 0.45.2(@opentelemetry/api@1.9.1)(@types/pg@8.20.0)(kysely@0.28.17)(pg@8.21.0) + idb-keyval: + specifier: ^6.2.5 + version: 6.2.5 kysely: specifier: 0.28.17 version: 0.28.17 @@ -98,6 +110,9 @@ importers: next: specifier: 16.2.9 version: 16.2.9(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + next-intl: + specifier: ^4.13.0 + version: 4.13.0(next@16.2.9(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4)(typescript@5.9.3) pg: specifier: 8.21.0 version: 8.21.0 @@ -174,6 +189,15 @@ importers: eslint-config-next: specifier: 16.2.2 version: 16.2.2(@typescript-eslint/parser@8.62.0(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3) + eslint-plugin-i18next: + specifier: ^6.1.5 + version: 6.1.5 + serwist: + specifier: ^9.5.11 + version: 9.5.11(browserslist@4.28.4)(typescript@5.9.3) + sharp: + specifier: ^0.35.2 + version: 0.35.2 tailwindcss: specifier: ^4 version: 4.3.1 @@ -969,6 +993,18 @@ packages: '@floating-ui/utils@0.2.11': resolution: {integrity: sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==} + '@formatjs/fast-memoize@3.1.6': + resolution: {integrity: sha512-H5aexk1Le7T9TPmscacZ+1pR6CTa2n1wq+HDVGXhH8TzUlQQpeXzZs91dRtmFHrbeNbjPFPfQujUqm7MHgVoXQ==} + + '@formatjs/icu-messageformat-parser@3.5.12': + resolution: {integrity: sha512-YyzzxVgYJ8DELmmkhn0Yr0rUj0dTJFf9Jp628K3S0ysInBWxLVDOS8i3RP91cCp4DMK4WYb4cVMhWA9i4knSJg==} + + '@formatjs/icu-skeleton-parser@2.1.10': + resolution: {integrity: sha512-XuSva+8ZGawk8VnD5VD6UeH8KarQ/Z022zgjHDoHmlNiAewstXuuzXc0Hk5pGFSdG+nNw5bfJKXqj1ZXHn9yUA==} + + '@formatjs/intl-localematcher@0.8.10': + resolution: {integrity: sha512-P/IC3qws3jH+1fEs+o0RIFgXKRaQlFehjS5W0FPAqdo6hgzawLl+eD0q0JjheQ3XtoOe5n8WSYfX06KQZI/QJA==} + '@hcaptcha/react-hcaptcha@2.0.2': resolution: {integrity: sha512-VbuH6VJ6m3BHmVBHs0fL9t+suZd7PQEqCzqL2BiUbBvbHI3XfvSgdiug2QiEPN8zskbPTIV/FfGPF53JCckrow==} @@ -1010,70 +1046,145 @@ packages: cpu: [arm64] os: [darwin] + '@img/sharp-darwin-arm64@0.35.2': + resolution: {integrity: sha512-eEieHsMksAW4IiO5NzauESRl2D2qz3J/kwUxUrSfV06A93eEaRfMpHXyUb1mAqrR7i8U9A0GRqE9pjn6u1Jjpg==} + engines: {node: '>=20.9.0'} + cpu: [arm64] + os: [darwin] + '@img/sharp-darwin-x64@0.34.5': resolution: {integrity: sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [darwin] + '@img/sharp-darwin-x64@0.35.2': + resolution: {integrity: sha512-BaktuGPCeHJMARpodR8jK4uKiZrPAy9WrfQW0sdI37clracq8Bp01AYS3SZgi5FS/y5twa9t4+LIuuxQjqRrWw==} + engines: {node: '>=20.9.0'} + cpu: [x64] + os: [darwin] + + '@img/sharp-freebsd-wasm32@0.35.2': + resolution: {integrity: sha512-YoAxdnd8hPUkvLHd3bWY+YA8nw3xM/RyRopYucNsWHVSan8NLVM3X2volsfoRDcXdUJPg6tXahSd7HXPK7lRnw==} + engines: {node: '>=20.9.0'} + os: [freebsd] + '@img/sharp-libvips-darwin-arm64@1.2.4': resolution: {integrity: sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==} cpu: [arm64] os: [darwin] + '@img/sharp-libvips-darwin-arm64@1.3.1': + resolution: {integrity: sha512-4V/M3roRMTYjiwZY9IOVQOE8OyeCxFAkYmyZDrZl51uOKjibm3oeEJ4WAmLxutAfzFbC9jqUiPs2gbnGflH+7g==} + cpu: [arm64] + os: [darwin] + '@img/sharp-libvips-darwin-x64@1.2.4': resolution: {integrity: sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==} cpu: [x64] os: [darwin] + '@img/sharp-libvips-darwin-x64@1.3.1': + resolution: {integrity: sha512-c0/DxItpJv2+dGhgycJBBgotdqruGYDvA79drdh0MD1dFpy7JzJ/PlXwi1H4rFf0eTy8tgbI91aHDnZIceY3jQ==} + cpu: [x64] + os: [darwin] + '@img/sharp-libvips-linux-arm64@1.2.4': resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==} cpu: [arm64] os: [linux] libc: [glibc] + '@img/sharp-libvips-linux-arm64@1.3.1': + resolution: {integrity: sha512-JznefmcK9j1JKPz8AkQDh89kjojubyfOasWBPKfzMIhPwsgDy9evpE/naJTXXXmghS1iFwR8u/kTwh/I2/+GCw==} + cpu: [arm64] + os: [linux] + libc: [glibc] + '@img/sharp-libvips-linux-arm@1.2.4': resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==} cpu: [arm] os: [linux] libc: [glibc] + '@img/sharp-libvips-linux-arm@1.3.1': + resolution: {integrity: sha512-aGGy9aWzXgHBG7HNyQPWorZthlp7+x6fDRoPAQbGO3ThcttuTyKIx3NuSHb6zb4gBNq6/yNn9f1cy9nFKS/Vmg==} + cpu: [arm] + os: [linux] + libc: [glibc] + '@img/sharp-libvips-linux-ppc64@1.2.4': resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==} cpu: [ppc64] os: [linux] libc: [glibc] + '@img/sharp-libvips-linux-ppc64@1.3.1': + resolution: {integrity: sha512-1EkwGNCZk6iWNCMWqrvdJ+r1j0PT1zIz60CNPhYnJlK/zyeWqlsPZIe+ocBVqPF8k/Ssee/NCk+tE9Ryrko6ng==} + cpu: [ppc64] + os: [linux] + libc: [glibc] + '@img/sharp-libvips-linux-riscv64@1.2.4': resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==} cpu: [riscv64] os: [linux] libc: [glibc] + '@img/sharp-libvips-linux-riscv64@1.3.1': + resolution: {integrity: sha512-Ilays+w2bXdnxzxtQdmXR62u8o8GYa3eL4+Gr+1KiE4xperMZUslRaVPJwwPkzlHEjGfXAfRVAa/7CYCtSqsBw==} + cpu: [riscv64] + os: [linux] + libc: [glibc] + '@img/sharp-libvips-linux-s390x@1.2.4': resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==} cpu: [s390x] os: [linux] libc: [glibc] + '@img/sharp-libvips-linux-s390x@1.3.1': + resolution: {integrity: sha512-VfBwVHQTbRoj4XlpA/KLZ7ltgMpz+4WSejFzQ+GnoImjo1PtEJ59QB2qR1xQEeRPYIkNrPIm2L4cICMvz4C2ew==} + cpu: [s390x] + os: [linux] + libc: [glibc] + '@img/sharp-libvips-linux-x64@1.2.4': resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==} cpu: [x64] os: [linux] libc: [glibc] + '@img/sharp-libvips-linux-x64@1.3.1': + resolution: {integrity: sha512-+c8ukgwU62DS54nCAjw7keOfHUkmr0B5QHEdcOqRnodF/MNXJbVI8Eopoj4B/0H8Asr65I+A4Amrn7a85/md6A==} + cpu: [x64] + os: [linux] + libc: [glibc] + '@img/sharp-libvips-linuxmusl-arm64@1.2.4': resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==} cpu: [arm64] os: [linux] libc: [musl] + '@img/sharp-libvips-linuxmusl-arm64@1.3.1': + resolution: {integrity: sha512-qlKb/pwbkAi1WMsJrYHk7CuDrd12s27U2QnRhFYUoJNrRCmkosMTttuRFat/DDB3IlDm5qE1TJgZ4JDnHX8Ldw==} + cpu: [arm64] + os: [linux] + libc: [musl] + '@img/sharp-libvips-linuxmusl-x64@1.2.4': resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==} cpu: [x64] os: [linux] libc: [musl] + '@img/sharp-libvips-linuxmusl-x64@1.3.1': + resolution: {integrity: sha512-yO21HwoUVLN8Qa+/SBjQLMYwBWAVJjeGPNe+hc0OUeMeifEtJqu5a1c4HayE1nNpDih9y3/KkoltfkDodmKAlg==} + cpu: [x64] + os: [linux] + libc: [musl] + '@img/sharp-linux-arm64@0.34.5': resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} @@ -1081,6 +1192,13 @@ packages: os: [linux] libc: [glibc] + '@img/sharp-linux-arm64@0.35.2': + resolution: {integrity: sha512-af12Pnd0ZGu2HfP8NayB0kk6eC/lrfbQE6HlR4jD+34wdJ1Vw9TF6TMn6ZvffT+WgqVsl0hRbmNvz2u/23VmwA==} + engines: {node: '>=20.9.0'} + cpu: [arm64] + os: [linux] + libc: [glibc] + '@img/sharp-linux-arm@0.34.5': resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} @@ -1088,6 +1206,13 @@ packages: os: [linux] libc: [glibc] + '@img/sharp-linux-arm@0.35.2': + resolution: {integrity: sha512-SE4kzF2mepn6z+6E7L6lsV8FzuLL6IPQdyX8ZiwROAG/G8td+hP/m7FsFPwidtrF19gvajuC9l6TxAVcsA4S7A==} + engines: {node: '>=20.9.0'} + cpu: [arm] + os: [linux] + libc: [glibc] + '@img/sharp-linux-ppc64@0.34.5': resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} @@ -1095,6 +1220,13 @@ packages: os: [linux] libc: [glibc] + '@img/sharp-linux-ppc64@0.35.2': + resolution: {integrity: sha512-hYSBm7zcNtDCozCxQHYZJiu63b/bXsgRZuOxCIBZsStMM9Vap47iFHdbX4kCvQsblPB/k+clhELpdQJHQLSHvg==} + engines: {node: '>=20.9.0'} + cpu: [ppc64] + os: [linux] + libc: [glibc] + '@img/sharp-linux-riscv64@0.34.5': resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} @@ -1102,6 +1234,13 @@ packages: os: [linux] libc: [glibc] + '@img/sharp-linux-riscv64@0.35.2': + resolution: {integrity: sha512-qQt0Kc13+Hoan/Awq/qMSQw3L+RI1NCRPgD5cUJ/1WSSmIoysLOc72jlRM3E0OHN9Yr313jgeQ2T+zW+F03QFA==} + engines: {node: '>=20.9.0'} + cpu: [riscv64] + os: [linux] + libc: [glibc] + '@img/sharp-linux-s390x@0.34.5': resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} @@ -1109,6 +1248,13 @@ packages: os: [linux] libc: [glibc] + '@img/sharp-linux-s390x@0.35.2': + resolution: {integrity: sha512-E4fLLfRPzDLlEeDaTzI98OFLcv++WL5ChLLMwPoVd0CIoZQqupBSNbOisPL5am9XsbQ9T84+iiMpUvbFtkunbA==} + engines: {node: '>=20.9.0'} + cpu: [s390x] + os: [linux] + libc: [glibc] + '@img/sharp-linux-x64@0.34.5': resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} @@ -1116,6 +1262,13 @@ packages: os: [linux] libc: [glibc] + '@img/sharp-linux-x64@0.35.2': + resolution: {integrity: sha512-gi0zFJJRLswfCZmHtJdikXPOc5u7qamSOS3NHedLqLd4W8Q0NqjdBr6TTRIgsfFjqfTsHFgdfvJ9LwqSgcHiAA==} + engines: {node: '>=20.9.0'} + cpu: [x64] + os: [linux] + libc: [glibc] + '@img/sharp-linuxmusl-arm64@0.34.5': resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} @@ -1123,6 +1276,13 @@ packages: os: [linux] libc: [musl] + '@img/sharp-linuxmusl-arm64@0.35.2': + resolution: {integrity: sha512-siWbOW1u6HFnFLrp0waKyW7VEf7jYvcDWdrXEFa8AkdAQgEvuu5Fz8/Y70w9EeqAdwDtfU012BhEHHaDqvQNzg==} + engines: {node: '>=20.9.0'} + cpu: [arm64] + os: [linux] + libc: [musl] + '@img/sharp-linuxmusl-x64@0.34.5': resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} @@ -1130,29 +1290,63 @@ packages: os: [linux] libc: [musl] + '@img/sharp-linuxmusl-x64@0.35.2': + resolution: {integrity: sha512-YBqMMcjDi4QGYiSn4vNOYBhmlC4z5AXqkOUUqI2e0AFA4urNv4ESgOgwNl3K+4etQhha0twXlzeF20bbULm9Yg==} + engines: {node: '>=20.9.0'} + cpu: [x64] + os: [linux] + libc: [musl] + '@img/sharp-wasm32@0.34.5': resolution: {integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [wasm32] + '@img/sharp-wasm32@0.35.2': + resolution: {integrity: sha512-Mrv4JQNYVQ94xH+jzZ9r+gowleN8mv2FTgKT+PI6bx5C0G8TdNYndu161pg2i7uoBwxy2ImPMHrJOM2LZef7Bw==} + engines: {node: '>=20.9.0'} + + '@img/sharp-webcontainers-wasm32@0.35.2': + resolution: {integrity: sha512-QNV27pxs9wpApEiCfvHM1RDoP1w1+2KrUWWDPEhEwg+latvOrfuhWrHWZKwdSFwU6jh3myjw/yOCRsUIuOft3g==} + engines: {node: '>=20.9.0'} + cpu: [wasm32] + '@img/sharp-win32-arm64@0.34.5': resolution: {integrity: sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [win32] + '@img/sharp-win32-arm64@0.35.2': + resolution: {integrity: sha512-BiVRYc/t6/Vl3e1hBx0hugG4oN9Pydf4fgMSpxTQJmwGUg/YoXTWHiFeRymHfCZzifxu4F4rpk/I67D0LQ20wQ==} + engines: {node: '>=20.9.0'} + cpu: [arm64] + os: [win32] + '@img/sharp-win32-ia32@0.34.5': resolution: {integrity: sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [ia32] os: [win32] + '@img/sharp-win32-ia32@0.35.2': + resolution: {integrity: sha512-YYEhx9PImCC7T0tI8JDMi4DB9LwLCXCU5OWNYEXAxh5Q1ShKkyC6byxzoBJ3gEFDnH2lQckWuDe70G7mB2XJog==} + engines: {node: ^20.9.0} + cpu: [ia32] + os: [win32] + '@img/sharp-win32-x64@0.34.5': resolution: {integrity: sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [win32] + '@img/sharp-win32-x64@0.35.2': + resolution: {integrity: sha512-imoOyBcoM/iiUr4J6VPpCNjPnjvP/Gks95898yB8YqoGGYmHYbOyCuNv9FMhFgtaiHFGbHW8bxKqRV6VjtXThQ==} + engines: {node: '>=20.9.0'} + cpu: [x64] + os: [win32] + '@inquirer/core@6.0.0': resolution: {integrity: sha512-fKi63Khkisgda3ohnskNf5uZJj+zXOaBvOllHsOkdsXRA/ubQLJQrZchFFi57NKbZzkTunXiBMdvWOv71alonw==} engines: {node: '>=14.18.0'} @@ -1385,6 +1579,94 @@ packages: resolution: {integrity: sha512-/UhIkaZgPutTFmQ7RnIJGgDXZmtEJ7Dvi86xNTFWcnRxVRNk/aotsqDJYeEvDP+FSMB2SdW+pQzNMcWP0rwuNA==} engines: {node: '>=14'} + '@parcel/watcher-android-arm64@2.5.6': + resolution: {integrity: sha512-YQxSS34tPF/6ZG7r/Ih9xy+kP/WwediEUsqmtf0cuCV5TPPKw/PQHRhueUo6JdeFJaqV3pyjm0GdYjZotbRt/A==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [android] + + '@parcel/watcher-darwin-arm64@2.5.6': + resolution: {integrity: sha512-Z2ZdrnwyXvvvdtRHLmM4knydIdU9adO3D4n/0cVipF3rRiwP+3/sfzpAwA/qKFL6i1ModaabkU7IbpeMBgiVEA==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [darwin] + + '@parcel/watcher-darwin-x64@2.5.6': + resolution: {integrity: sha512-HgvOf3W9dhithcwOWX9uDZyn1lW9R+7tPZ4sug+NGrGIo4Rk1hAXLEbcH1TQSqxts0NYXXlOWqVpvS1SFS4fRg==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [darwin] + + '@parcel/watcher-freebsd-x64@2.5.6': + resolution: {integrity: sha512-vJVi8yd/qzJxEKHkeemh7w3YAn6RJCtYlE4HPMoVnCpIXEzSrxErBW5SJBgKLbXU3WdIpkjBTeUNtyBVn8TRng==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [freebsd] + + '@parcel/watcher-linux-arm-glibc@2.5.6': + resolution: {integrity: sha512-9JiYfB6h6BgV50CCfasfLf/uvOcJskMSwcdH1PHH9rvS1IrNy8zad6IUVPVUfmXr+u+Km9IxcfMLzgdOudz9EQ==} + engines: {node: '>= 10.0.0'} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@parcel/watcher-linux-arm-musl@2.5.6': + resolution: {integrity: sha512-Ve3gUCG57nuUUSyjBq/MAM0CzArtuIOxsBdQ+ftz6ho8n7s1i9E1Nmk/xmP323r2YL0SONs1EuwqBp2u1k5fxg==} + engines: {node: '>= 10.0.0'} + cpu: [arm] + os: [linux] + libc: [musl] + + '@parcel/watcher-linux-arm64-glibc@2.5.6': + resolution: {integrity: sha512-f2g/DT3NhGPdBmMWYoxixqYr3v/UXcmLOYy16Bx0TM20Tchduwr4EaCbmxh1321TABqPGDpS8D/ggOTaljijOA==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@parcel/watcher-linux-arm64-musl@2.5.6': + resolution: {integrity: sha512-qb6naMDGlbCwdhLj6hgoVKJl2odL34z2sqkC7Z6kzir8b5W65WYDpLB6R06KabvZdgoHI/zxke4b3zR0wAbDTA==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@parcel/watcher-linux-x64-glibc@2.5.6': + resolution: {integrity: sha512-kbT5wvNQlx7NaGjzPFu8nVIW1rWqV780O7ZtkjuWaPUgpv2NMFpjYERVi0UYj1msZNyCzGlaCWEtzc+exjMGbQ==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@parcel/watcher-linux-x64-musl@2.5.6': + resolution: {integrity: sha512-1JRFeC+h7RdXwldHzTsmdtYR/Ku8SylLgTU/reMuqdVD7CtLwf0VR1FqeprZ0eHQkO0vqsbvFLXUmYm/uNKJBg==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@parcel/watcher-win32-arm64@2.5.6': + resolution: {integrity: sha512-3ukyebjc6eGlw9yRt678DxVF7rjXatWiHvTXqphZLvo7aC5NdEgFufVwjFfY51ijYEWpXbqF5jtrK275z52D4Q==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [win32] + + '@parcel/watcher-win32-ia32@2.5.6': + resolution: {integrity: sha512-k35yLp1ZMwwee3Ez/pxBi5cf4AoBKYXj00CZ80jUz5h8prpiaQsiRPKQMxoLstNuqe2vR4RNPEAEcjEFzhEz/g==} + engines: {node: '>= 10.0.0'} + cpu: [ia32] + os: [win32] + + '@parcel/watcher-win32-x64@2.5.6': + resolution: {integrity: sha512-hbQlYcCq5dlAX9Qx+kFb0FHue6vbjlf0FrNzSKdYK2APUf7tGfGxQCk2ihEREmbR6ZMc0MVAD5RIX/41gpUzTw==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [win32] + + '@parcel/watcher@2.5.6': + resolution: {integrity: sha512-tmmZ3lQxAe/k/+rNnXQRawJ4NjxO2hqiOLTHvWchtGZULp4RyFeh6aU4XdOYBFe2KE1oShQTv4AblOs2iOrNnQ==} + engines: {node: '>= 10.0.0'} + '@peculiar/asn1-android@2.8.0': resolution: {integrity: sha512-skLbS+IOGv1lUgDqtChr8xvtvEr3HMse/JGBaL2r1J1o/n7a8wqOrovMtlRq/UXLhxvmLaONP67hwtshgzwfzA==} @@ -2485,6 +2767,9 @@ packages: '@rtsao/scc@1.1.0': resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==} + '@schummar/icu-type-parser@1.21.5': + resolution: {integrity: sha512-bXHSaW5jRTmke9Vd0h5P7BtWZG9Znqb8gSDxZnxaGSJnGwPLDPfS+3g0BKzeWqzgZPsIVZkM7m2tbo18cm5HBw==} + '@selderee/plugin-htmlparser2@0.11.0': resolution: {integrity: sha512-P33hHGdldxGabLFjPPpaTxVolMrzrcegejx+0GxjrIb9Zv48D8yAIA/QTDR2dFl7Uz7urX8aX6+5bCZslr+gWQ==} @@ -2635,6 +2920,57 @@ packages: peerDependencies: webpack: '>=5.0.0' + '@serwist/build@9.5.11': + resolution: {integrity: sha512-PQfW+LhADYFOOp0PhEnjlgJCyKor6cYa06d3rID1OpiKzkmCApJV1WYfdTBB96jXaWv6OWcWSbSV4tqDLxvaVA==} + engines: {node: '>=18.0.0'} + peerDependencies: + typescript: '>=5.0.0' + peerDependenciesMeta: + typescript: + optional: true + + '@serwist/next@9.5.11': + resolution: {integrity: sha512-omT32H7U21ihCymSvOG9QeRJBuOEomJx4JdzKhUoqOW3DR10tH3m84VOHj3BvK0OcA7av3qj5FsyNFBB+f0n8A==} + engines: {node: '>=18.0.0'} + peerDependencies: + '@serwist/cli': ^9.5.11 + next: '>=14.0.0' + react: '>=18.0.0' + typescript: '>=5.0.0' + peerDependenciesMeta: + '@serwist/cli': + optional: true + typescript: + optional: true + + '@serwist/utils@9.5.11': + resolution: {integrity: sha512-zqxmwuHqWA3OwN82Wo8gFZ9QBemygJP3cap5JWAOG4UyJZgUZfmBXAXj+IMaD4eKZ/6pqrxHHDZ9uSWZmJ1mXA==} + peerDependencies: + browserslist: '>=4' + peerDependenciesMeta: + browserslist: + optional: true + + '@serwist/webpack-plugin@9.5.11': + resolution: {integrity: sha512-SlvO3A1UMcc1htCzMtLCtPQK6yISCO7B859ixLv7EiY/yayXjVxGm9vHqkJYpQ768PWyjEZXRY/X6EGRMA6wJQ==} + engines: {node: '>=18.0.0'} + peerDependencies: + typescript: '>=5.0.0' + webpack: 4.4.0 || ^5.9.0 + peerDependenciesMeta: + typescript: + optional: true + webpack: + optional: true + + '@serwist/window@9.5.11': + resolution: {integrity: sha512-OrH9srhmifUvY36NuukHSZby24XTEk4pHh3pfY0GBQzA9ouU1fYh+ORWhKxH7/wkVHRr3sc4YAhjtpfL14PjjQ==} + peerDependencies: + typescript: '>=5.0.0' + peerDependenciesMeta: + typescript: + optional: true + '@simplewebauthn/browser@13.3.0': resolution: {integrity: sha512-BE/UWv6FOToAdVk0EokzkqQQDOWtNydYlY6+OrmiZ5SCNmb41VehttboTetUM3T/fr6EAFYVXjz4My2wg230rQ==} @@ -2654,9 +2990,102 @@ packages: '@standard-schema/utils@0.3.0': resolution: {integrity: sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==} + '@swc/core-darwin-arm64@1.15.43': + resolution: {integrity: sha512-v1aVuvXdo/BHxJzco9V2xpHrvwWmhfS8t6gziY5wJxd+Z2h8AeJRnAwPD8itCDaGXVBwJ/CaKfxEzTkG0Va0OA==} + engines: {node: '>=10'} + cpu: [arm64] + os: [darwin] + + '@swc/core-darwin-x64@1.15.43': + resolution: {integrity: sha512-lp3d4Lamc8dt5huYdGLSR+9hLxmfr1jb0l+4XXG2zPqZwYWRN9R0U2qYoTrggiU2RWW0oV9VbWM3kBnqIc2kdQ==} + engines: {node: '>=10'} + cpu: [x64] + os: [darwin] + + '@swc/core-linux-arm-gnueabihf@1.15.43': + resolution: {integrity: sha512-JWTQQELtsG5GgphDrr/XqqmM2pDN3cZqbMS0Mrg+iTiXL3F74sn/S2IyYE/5u4h2KLkTf9qQ7dXyxsbx7YzkeA==} + engines: {node: '>=10'} + cpu: [arm] + os: [linux] + + '@swc/core-linux-arm64-gnu@1.15.43': + resolution: {integrity: sha512-B4otJRdPWIsmiSBf0uG7Z/+vMWmkufjz5MmYxubwKuZazDW14Zd3symga1N62QR4RT+kEFeHEgsXfZGyn/w0hw==} + engines: {node: '>=10'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@swc/core-linux-arm64-musl@1.15.43': + resolution: {integrity: sha512-6zB6OnpViBxYy4tgY3v2i6AZY9fwkcHZ032UOwtwUuW1d19sdT07qF0kZe6/3UR1tUaK6jjg2rmVcUIBCEYVjQ==} + engines: {node: '>=10'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@swc/core-linux-ppc64-gnu@1.15.43': + resolution: {integrity: sha512-coxE1ZWdB3uSDVNoEtYNrRi/1epvckZx9cTJ8ICUxTMTxGk+yvQ/Twacp3ruZSaMPGCriUjP86C37VhaT6nyRg==} + engines: {node: '>=10'} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@swc/core-linux-s390x-gnu@1.15.43': + resolution: {integrity: sha512-lXfLhs+LpBsD5inuYx+YDH5WsPPBQ95KPUiy8P5wq9ob9xKDZFqwNfU2QW6bGO8NqRO/H9JQomTSt5Yyh+FGfA==} + engines: {node: '>=10'} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@swc/core-linux-x64-gnu@1.15.43': + resolution: {integrity: sha512-07XnKwTmKy8TGOZG3D9fRnLWGynxPjwQnZLVmBFbo6F+7vHYzBIOuwXEhemrChBWb6yDNZsVCcMWCPX6FDD2xg==} + engines: {node: '>=10'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@swc/core-linux-x64-musl@1.15.43': + resolution: {integrity: sha512-TJc+bsSIaBh+hZvZ5GRtW/K1bw66TJ9vsUwvVIsZdiWxU5ObLwZvfcnZ3UpgVfMnFibRes9uriJrQNBHEEogRQ==} + engines: {node: '>=10'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@swc/core-win32-arm64-msvc@1.15.43': + resolution: {integrity: sha512-jfd7s2/bUQYkOHLs+LWQNKZdmDa8+sufKLllhpWAhVQ2GDCwsHe3vR/j+OSiItZNtkzFuaawa3+SAKz9y5gYfw==} + engines: {node: '>=10'} + cpu: [arm64] + os: [win32] + + '@swc/core-win32-ia32-msvc@1.15.43': + resolution: {integrity: sha512-rLAE8JvucqEW1ZGohxPQrQWPBQeJG4+ypKbWfdlU/qmKScvCkxf9/Jxnzki1dkUQCQ7P5Enp13RlvqOlvx/32g==} + engines: {node: '>=10'} + cpu: [ia32] + os: [win32] + + '@swc/core-win32-x64-msvc@1.15.43': + resolution: {integrity: sha512-h8MLDHZcfIukwQWj03rIJZx1I0E81AYj2X7J/nGErG4nz+QAv6G1Z+peotvinL3lqpbo32tLYSMFo32/ySzxKg==} + engines: {node: '>=10'} + cpu: [x64] + os: [win32] + + '@swc/core@1.15.43': + resolution: {integrity: sha512-1CuKjFkPxIgGdeHVuNbkxmBxkcbdc08u0aiI43pFq6yY1tTVKmXT9hFEooyyKs/sJ3xf1GPHyEwTtk9Xl8dvQw==} + engines: {node: '>=10'} + peerDependencies: + '@swc/helpers': '>=0.5.17' + peerDependenciesMeta: + '@swc/helpers': + optional: true + + '@swc/counter@0.1.3': + resolution: {integrity: sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==} + '@swc/helpers@0.5.15': resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==} + '@swc/types@0.1.27': + resolution: {integrity: sha512-K6h3iUlqeM946U4sXFYeahefR1YBbXJvko+hv8WS8/0BNJ4OHiHRywMnQUJCqkR7Y9+hqQ1TvEpiKqUhz7NEFg==} + '@tailwindcss/node@4.3.1': resolution: {integrity: sha512-6NDaqRoAMSXD1mr/RXu0HBvNE9a2n5tHPsxu9XHLws8o4Twes5rBM2205SUUiJ9goAtadrN6xTGX0UDEwp/N4A==} @@ -2749,18 +3178,33 @@ packages: '@tailwindcss/postcss@4.3.1': resolution: {integrity: sha512-dNJuNbdEJT/SWRuXTYP1WSamelsz3ztkUsdtWQPjrexysrTpaEPM40P/71knXiXLYEojqPOEGitVLLpPMS5T6A==} + '@tanstack/query-async-storage-persister@5.101.1': + resolution: {integrity: sha512-QuHhCTLyQjiVFUYRW5xtgeuiP1NEmqL5wnuFgPGeO6SjTLKeN9VnAhAKsKzde3AcEZEe4PL7FehfKbVhM/9kLA==} + '@tanstack/query-core@5.101.1': resolution: {integrity: sha512-Y6Y92dkXtNqx67m2pMSxUsA3zOCwv862JexZRP8/EPwvKXMPu9m8rv43spiXWzOUIggQ3SQApttALStzhA8B4g==} + '@tanstack/query-core@5.101.2': + resolution: {integrity: sha512-hH5MLoJhF7KaIGd7q3xTXGXvslI+GYlM1Z/35aSHHWaCJWB7XvTSHYuV3eM7tw+aE0mT/xMro4M4Q9rCGHT0lw==} + '@tanstack/query-devtools@5.101.1': resolution: {integrity: sha512-37RQ9U2PxlXQiv1era2t+uHgVhmiyvxqTMu30+KoVf0rufiucu6rpGRKFJk61Wh5OAZFKqCQd6lxTzFWfLZiuQ==} + '@tanstack/query-persist-client-core@5.101.1': + resolution: {integrity: sha512-rR5Er6jmdI3Oo8o6Wc0ceM6glDU4umgePu2IxM3Gy2UvPqcQONduxxxSzU1+F17mpS09XHqHKmj0Irhfb2cGYg==} + '@tanstack/react-query-devtools@5.101.1': resolution: {integrity: sha512-OXFR9XKdEslraq3cpl3kCUeNvTIq/xGWEZiFZdn2bLB/q4WxSALMEDKYZ5yYjMQytsfnQxwQYqV4qtVEf0nuog==} peerDependencies: '@tanstack/react-query': ^5.101.1 react: ^18 || ^19 + '@tanstack/react-query-persist-client@5.101.1': + resolution: {integrity: sha512-bBSne+3+28EZ/Ch5a06J0YQi6d5yaT2G+vyHAyhzwzYA1FAEAFe6Ou6TsNVQaT6ef6pFShU73FL/NB4guTWNng==} + peerDependencies: + '@tanstack/react-query': ^5.101.1 + react: ^18 || ^19 + '@tanstack/react-query@5.101.1': resolution: {integrity: sha512-ZnONUuQKJe1bJMStXUL1s5uKN9FcfC28j5cK+iDZcdSHtUv1wtin1cGc/Oewhf2Oc4eKY7lggtpvT/AbMmhHew==} peerDependencies: @@ -2883,6 +3327,9 @@ packages: '@types/react@19.2.14': resolution: {integrity: sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==} + '@types/trusted-types@2.0.7': + resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} + '@types/use-sync-external-store@0.0.6': resolution: {integrity: sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==} @@ -3422,6 +3869,11 @@ packages: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} + browserslist@4.28.2: + resolution: {integrity: sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + browserslist@4.28.4: resolution: {integrity: sha512-MTc8i/x9jBQd1iMw2CFGS+rwMa07eYjLR0CCTLDACl9xhxy+nIs3KeML/biicXtk9JrZ6dnnTatmc7ErPXIxqw==} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} @@ -3528,6 +3980,10 @@ packages: resolution: {integrity: sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==} engines: {node: '>= 12'} + common-tags@1.8.2: + resolution: {integrity: sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA==} + engines: {node: '>=4.0.0'} + commondir@1.0.1: resolution: {integrity: sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==} @@ -4002,6 +4458,10 @@ packages: eslint-import-resolver-webpack: optional: true + eslint-plugin-i18next@6.1.5: + resolution: {integrity: sha512-xCTfstbK9ZpQ6UFT5S1s6zbZMhKn2o2jRSQYiU2UkV7wt8y1m3WjkUYGkGlipgRdFInYI9+LAqE+JQU/L73HmA==} + engines: {node: '>=18.10.0'} + eslint-plugin-import@2.32.0: resolution: {integrity: sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==} engines: {node: '>=4'} @@ -4355,6 +4815,15 @@ packages: resolution: {integrity: sha512-ag87y7cJJ9/3+GxFr8Oy4O5faDsGRGnBGsJj/YjOSsSx/5eadKLYTMPlzuR6obgoCDDm0abAAZitXXQkMOPSpA==} engines: {node: '>= 20'} + icu-minify@4.13.0: + resolution: {integrity: sha512-SIFMeUHZJjzS5RvIGvybKvWoHjDm9cGVEs2EpJ8PmywOdJLWyblPm7TdPLLoUtkJtwQD7iGhl2WMptZ+N0on+w==} + + idb-keyval@6.2.5: + resolution: {integrity: sha512-eKQkTnS0relYsSOYomx8ozIbmdsQCKUdhyuIaQ2DZgKuaxtyQQMkyD/wlnQN32pO3yutN1b1L8uqwcDKaJd7/Q==} + + idb@8.0.3: + resolution: {integrity: sha512-LtwtVyVYO5BqRvcsKuB2iUMnHwPVByPCXFXOpuU96IZPPoPN6xjOGxZQ74pgSVVLQWtUOYgyeL4GE98BY5D3wg==} + ieee754@1.2.1: resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} @@ -4398,6 +4867,9 @@ packages: resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==} engines: {node: '>=12'} + intl-messageformat@11.2.9: + resolution: {integrity: sha512-cGzymZerpDhVXRKjKLgXKda9gI29TU2o88L7gwNMHp3WZVxA/0c5tX52udXbW9JklDApolvMXZG6Dhhdz5eirA==} + ip-address@10.2.0: resolution: {integrity: sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==} engines: {node: '>= 12'} @@ -4607,6 +5079,9 @@ packages: resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==} engines: {node: '>=6'} + kolorist@1.8.0: + resolution: {integrity: sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==} + kysely@0.28.17: resolution: {integrity: sha512-nbD8lB9EB3wNdMhOCdx5Li8DxnLbvKByylRLcJ1h+4SkrowVeECAyZlyiKMThF7xFdRz0jSQ2MoJr+wXux2y0Q==} engines: {node: '>=20.0.0'} @@ -4714,6 +5189,9 @@ packages: lodash.merge@4.6.2: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + lodash.sortby@4.7.0: + resolution: {integrity: sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==} + log-symbols@7.0.1: resolution: {integrity: sha512-ja1E3yCr9i/0hmBVaM0bfwDjnGy8I/s6PP4DFp+yP+a+mrHO4Rm7DtmnqROTUkHIkqffC84YY7AeqX6oFk0WFg==} engines: {node: '>=18'} @@ -4909,6 +5387,10 @@ packages: resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==} engines: {node: '>= 0.6'} + negotiator@1.0.0: + resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} + engines: {node: '>= 0.6'} + neo-async@2.6.2: resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} @@ -4916,6 +5398,19 @@ packages: resolution: {integrity: sha512-eonl3sLUha+S1GzTPxychyhnUzKyeQkZ7jLjKrBagJgPla13F+uQ71HgpFefyHgqrjEbCPkDArxYsjY8/+gLKA==} engines: {node: '>= 0.4.0'} + next-intl-swc-plugin-extractor@4.13.0: + resolution: {integrity: sha512-6S/fJI0KXvLCL8nhBo9P8eGaJPzmwJBTCzX0NaUIj0VyU8U89d//T+vjMLdNIXl5MlLaYH7B9MbAjb8Mvu+tqQ==} + + next-intl@4.13.0: + resolution: {integrity: sha512-OvNq2v5XLx4EkQOsAhVE9g+6zdb83XHusADCXXtIW4LILYnjEVaeINdr1lkVWKSjzwNUiMSlH5N4K0OQTRiv6A==} + peerDependencies: + next: ^12.0.0 || ^13.0.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || >=19.0.0-rc <19.0.0 || ^19.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + next@16.2.9: resolution: {integrity: sha512-MEOJiq/UvuezAdqVSceHbqDgZt1kDw2tpGVOlsdIoJsQdbN2JY2hpVG4xnXGkbdJUOEWhnRfiu/O4Hpc9Juwww==} engines: {node: '>=20.9.0'} @@ -4937,6 +5432,9 @@ packages: sass: optional: true + node-addon-api@7.1.1: + resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==} + node-exports-info@1.6.2: resolution: {integrity: sha512-kXs9Go0cah0qHVV2v389IXQLdLCeE1xfFtjOAF+iobu0OIoG1pje8At2vMHyaPMiPMnG/LWP50twML21eMcAag==} engines: {node: '>= 0.4'} @@ -5111,6 +5609,9 @@ packages: engines: {node: '>=18'} hasBin: true + po-parser@2.1.1: + resolution: {integrity: sha512-ECF4zHLbUItpUgE3OTtLKlPjeBN+fKEczj2zYjDfCGOzicNs0GK3Vg2IoAYwx7LH/XYw43fZQP6xnZ4TkNxSLQ==} + possible-typed-array-names@1.1.0: resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==} engines: {node: '>= 0.4'} @@ -5154,6 +5655,10 @@ packages: engines: {node: '>=14'} hasBin: true + pretty-bytes@6.1.1: + resolution: {integrity: sha512-mQUvGU6aUFQ+rNvTIAcZuWGRT9a6f6Yrg9bHs4ImKF+HZCEK+plBvnAZYSIQztknZF2qnzNtr6F8s0+IuptdlQ==} + engines: {node: ^14.13.1 || >=16.0.0} + prismjs@1.30.0: resolution: {integrity: sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==} engines: {node: '>=6'} @@ -5367,6 +5872,10 @@ packages: resolution: {integrity: sha512-QT7FVMXfWOYFbeRBF6nu+I6tr2Tf3u0q8RIEjNob/heKY/nh7drD/k7eeMFmSQgnTtCzLDcCu/XEnpW2wk4xCQ==} engines: {node: '>=9.3.0 || >=8.10.0 <9.0.0'} + requireindex@1.1.0: + resolution: {integrity: sha512-LBnkqsDE7BZKvqylbmn7lTIVdpx4K/QCduRATpO5R+wtPmky/a8pN1bO2D6wXppn1497AJF9mNjqAXr6bdl9jg==} + engines: {node: '>=0.10.5'} + reselect@5.2.0: resolution: {integrity: sha512-AgZ3UOZm3YndfrJ4OYjgrT7bmCm/1iqkjvEfH/oYjzh6PD2qw4QuT3jjnXIrpdt4MTpMXclMT3lXbmRY+XRakw==} @@ -5445,6 +5954,11 @@ packages: resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} hasBin: true + semver@7.7.4: + resolution: {integrity: sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==} + engines: {node: '>=10'} + hasBin: true + semver@7.8.5: resolution: {integrity: sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==} engines: {node: '>=10'} @@ -5453,6 +5967,14 @@ packages: server-only@0.0.1: resolution: {integrity: sha512-qepMx2JxAa5jjfzxG79yPPq+8BuFToHd1hm7kI+Z4zAq1ftQiP7HcxMhDDItrbtwVeLg/cY2JnKnrcFkmiswNA==} + serwist@9.5.11: + resolution: {integrity: sha512-Bq6uwJFd4ET60BWI77v3VbazKHv6k7lECOiiCFwKyBu/slaCn0GHJ5L5RfsuJUKrnbD9lYUCDo6sqaKRM5M2vA==} + peerDependencies: + typescript: '>=5.0.0' + peerDependenciesMeta: + typescript: + optional: true + set-cookie-parser@3.1.1: resolution: {integrity: sha512-vM9SUhjsUYs6UeJUmygc5Ofm5eQGe85riob5ju6XCgFGJI5PLV4nrDAQpQjd+LkFBpAkADn5BQQpZ9EUNkyLuA==} @@ -5472,6 +5994,10 @@ packages: resolution: {integrity: sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + sharp@0.35.2: + resolution: {integrity: sha512-FVtFjtBCMiJS6yb5CX7Sop45WFMpeGw6oRKuJnXYgf/f1ms/D7LE/ZUSNxnW7rZ/dbslQWYkoqFHGPaDBtaK4w==} + engines: {node: '>=20.9.0'} + shebang-command@2.0.0: resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} engines: {node: '>=8'} @@ -5550,6 +6076,11 @@ packages: resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} engines: {node: '>=0.10.0'} + source-map@0.8.0-beta.0: + resolution: {integrity: sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==} + engines: {node: '>= 8'} + deprecated: The work that was done in this beta branch won't be included in future versions + split2@4.2.0: resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} engines: {node: '>= 10.x'} @@ -5699,6 +6230,9 @@ packages: tr46@0.0.3: resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} + tr46@1.0.1: + resolution: {integrity: sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==} + tree-kill@1.2.2: resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} hasBin: true @@ -5746,6 +6280,10 @@ packages: resolution: {integrity: sha512-Ne2YiiGN8bmrmJJEuTWTLJR32nh/JdL1+PSicowtNb0WFpn59GK8/lfD61bVtzguz7b3PBt74nxpv/Pw5po5Rg==} engines: {node: '>=8'} + type-fest@5.6.0: + resolution: {integrity: sha512-8ZiHFm91orbSAe2PSAiSVBVko18pbhbiB3U9GglSzF/zCGkR+rxpHx6sEMCUm4kxY4LjDIUGgCfUMtwfZfjfUA==} + engines: {node: '>=20'} + type-fest@5.7.0: resolution: {integrity: sha512-1URUxUqfHFM1c+zfSPsa3gnkO7Aq21qyH75SIduNYz4SzY964rn1X2vCMQaHSHhktiw+0kPa2iyb6PUpXqB6Vg==} engines: {node: '>=20'} @@ -5826,6 +6364,11 @@ packages: '@types/react': optional: true + use-intl@4.13.0: + resolution: {integrity: sha512-fAFDrWaASxlhXOipcOyb5VDD+YONqj6+8O8EcG/J7RBoOUF3A8YahRWLN+mBxYMrlMQB8N6Voqk5X+YC+HSL0A==} + peerDependencies: + react: ^17.0.0 || ^18.0.0 || >=19.0.0-rc <19.0.0 || ^19.0.0 + use-sidecar@1.1.3: resolution: {integrity: sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==} engines: {node: '>=10'} @@ -5874,6 +6417,9 @@ packages: webidl-conversions@3.0.1: resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} + webidl-conversions@4.0.2: + resolution: {integrity: sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==} + webpack-sources@3.5.0: resolution: {integrity: sha512-HPuy+uuoTCaaoEoI1LQ3JN9+vrPBvEesnnX1jADHy728cHSMlq4wUc4afYqahq2B1mhQVZxCXOkNTnXltr+2vQ==} engines: {node: '>=10.13.0'} @@ -5898,6 +6444,9 @@ packages: whatwg-url@5.0.0: resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} + whatwg-url@7.1.0: + resolution: {integrity: sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==} + when-exit@2.1.5: resolution: {integrity: sha512-VGkKJ564kzt6Ms1dbgPP/yuIoQCrsFAnRbptpC5wOEsDaNsbCB2bnfnaA8i/vRs5tjUSEOtIuvl9/MyVsvQZCg==} @@ -5979,6 +6528,9 @@ packages: peerDependencies: zod: ^3.25.0 || ^4.0.0 + zod@4.4.1: + resolution: {integrity: sha512-a6ENMBBGZBsnlSebQ/eKCguSBeGKSf4O7BPnqVPmYGtpBYI7VSqoVqw+QcB7kPRjbqPwhYTpFbVj/RqNz/CT0Q==} + zod@4.4.3: resolution: {integrity: sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==} @@ -6249,21 +6801,21 @@ snapshots: picocolors: 1.1.1 sisteransi: 1.0.5 - '@daveyplate/better-auth-tanstack@1.3.6(@tanstack/query-core@5.101.1)(@tanstack/react-query@5.101.1(react@19.2.4))(better-auth@1.6.14(@opentelemetry/api@1.9.1)(drizzle-kit@0.31.10)(drizzle-orm@0.45.2(@opentelemetry/api@1.9.1)(@types/pg@8.20.0)(kysely@0.28.17)(pg@8.21.0))(next@16.2.9(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(pg@8.21.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + '@daveyplate/better-auth-tanstack@1.3.6(@tanstack/query-core@5.101.2)(@tanstack/react-query@5.101.1(react@19.2.4))(better-auth@1.6.14(@opentelemetry/api@1.9.1)(drizzle-kit@0.31.10)(drizzle-orm@0.45.2(@opentelemetry/api@1.9.1)(@types/pg@8.20.0)(kysely@0.28.17)(pg@8.21.0))(next@16.2.9(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(pg@8.21.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: - '@tanstack/query-core': 5.101.1 + '@tanstack/query-core': 5.101.2 '@tanstack/react-query': 5.101.1(react@19.2.4) better-auth: 1.6.14(@opentelemetry/api@1.9.1)(drizzle-kit@0.31.10)(drizzle-orm@0.45.2(@opentelemetry/api@1.9.1)(@types/pg@8.20.0)(kysely@0.28.17)(pg@8.21.0))(next@16.2.9(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(pg@8.21.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) react: 19.2.4 react-dom: 19.2.4(react@19.2.4) - '@daveyplate/better-auth-ui@3.4.0(7cba73d98a64fa1f7427e1b78929c147)': + '@daveyplate/better-auth-ui@3.4.0(2e97e51b7de348ab45b8706c958356e1)': dependencies: '@better-auth/api-key': 1.6.22(@better-auth/core@1.6.14(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.1)(better-call@1.3.5(zod@4.4.3))(jose@6.2.3)(kysely@0.28.17)(nanostores@1.3.0))(@better-auth/utils@0.4.2)(better-auth@1.6.14(@opentelemetry/api@1.9.1)(drizzle-kit@0.31.10)(drizzle-orm@0.45.2(@opentelemetry/api@1.9.1)(@types/pg@8.20.0)(kysely@0.28.17)(pg@8.21.0))(next@16.2.9(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(pg@8.21.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(better-call@2.0.2(zod@4.4.3)) '@better-auth/passkey': 1.6.22(@better-auth/core@1.6.14(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.1.21)(@opentelemetry/api@1.9.1)(better-call@1.3.5(zod@4.4.3))(jose@6.2.3)(kysely@0.28.17)(nanostores@1.3.0))(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.1.21)(better-auth@1.6.14(@opentelemetry/api@1.9.1)(drizzle-kit@0.31.10)(drizzle-orm@0.45.2(@opentelemetry/api@1.9.1)(@types/pg@8.20.0)(kysely@0.28.17)(pg@8.21.0))(next@16.2.9(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(pg@8.21.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(better-call@1.3.5(zod@4.4.3))(nanostores@1.3.0) '@better-fetch/fetch': 1.3.1 '@captchafox/react': 1.12.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@daveyplate/better-auth-tanstack': 1.3.6(@tanstack/query-core@5.101.1)(@tanstack/react-query@5.101.1(react@19.2.4))(better-auth@1.6.14(@opentelemetry/api@1.9.1)(drizzle-kit@0.31.10)(drizzle-orm@0.45.2(@opentelemetry/api@1.9.1)(@types/pg@8.20.0)(kysely@0.28.17)(pg@8.21.0))(next@16.2.9(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(pg@8.21.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@daveyplate/better-auth-tanstack': 1.3.6(@tanstack/query-core@5.101.2)(@tanstack/react-query@5.101.1(react@19.2.4))(better-auth@1.6.14(@opentelemetry/api@1.9.1)(drizzle-kit@0.31.10)(drizzle-orm@0.45.2(@opentelemetry/api@1.9.1)(@types/pg@8.20.0)(kysely@0.28.17)(pg@8.21.0))(next@16.2.9(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(pg@8.21.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@hcaptcha/react-hcaptcha': 2.0.2 '@hookform/resolvers': 5.4.0(react-hook-form@7.80.0(react@19.2.4)) '@instantdb/react': 1.0.49(react@19.2.4) @@ -6629,6 +7181,18 @@ snapshots: '@floating-ui/utils@0.2.11': {} + '@formatjs/fast-memoize@3.1.6': {} + + '@formatjs/icu-messageformat-parser@3.5.12': + dependencies: + '@formatjs/icu-skeleton-parser': 2.1.10 + + '@formatjs/icu-skeleton-parser@2.1.10': {} + + '@formatjs/intl-localematcher@0.8.10': + dependencies: + '@formatjs/fast-memoize': 3.1.6 + '@hcaptcha/react-hcaptcha@2.0.2': {} '@hexagon/base64@1.1.28': {} @@ -6654,103 +7218,206 @@ snapshots: '@humanwhocodes/retry@0.4.3': {} - '@img/colour@1.1.0': - optional: true + '@img/colour@1.1.0': {} '@img/sharp-darwin-arm64@0.34.5': optionalDependencies: '@img/sharp-libvips-darwin-arm64': 1.2.4 optional: true + '@img/sharp-darwin-arm64@0.35.2': + optionalDependencies: + '@img/sharp-libvips-darwin-arm64': 1.3.1 + optional: true + '@img/sharp-darwin-x64@0.34.5': optionalDependencies: '@img/sharp-libvips-darwin-x64': 1.2.4 optional: true + '@img/sharp-darwin-x64@0.35.2': + optionalDependencies: + '@img/sharp-libvips-darwin-x64': 1.3.1 + optional: true + + '@img/sharp-freebsd-wasm32@0.35.2': + dependencies: + '@img/sharp-wasm32': 0.35.2 + optional: true + '@img/sharp-libvips-darwin-arm64@1.2.4': optional: true + '@img/sharp-libvips-darwin-arm64@1.3.1': + optional: true + '@img/sharp-libvips-darwin-x64@1.2.4': optional: true + '@img/sharp-libvips-darwin-x64@1.3.1': + optional: true + '@img/sharp-libvips-linux-arm64@1.2.4': optional: true + '@img/sharp-libvips-linux-arm64@1.3.1': + optional: true + '@img/sharp-libvips-linux-arm@1.2.4': optional: true + '@img/sharp-libvips-linux-arm@1.3.1': + optional: true + '@img/sharp-libvips-linux-ppc64@1.2.4': optional: true + '@img/sharp-libvips-linux-ppc64@1.3.1': + optional: true + '@img/sharp-libvips-linux-riscv64@1.2.4': optional: true + '@img/sharp-libvips-linux-riscv64@1.3.1': + optional: true + '@img/sharp-libvips-linux-s390x@1.2.4': optional: true + '@img/sharp-libvips-linux-s390x@1.3.1': + optional: true + '@img/sharp-libvips-linux-x64@1.2.4': optional: true + '@img/sharp-libvips-linux-x64@1.3.1': + optional: true + '@img/sharp-libvips-linuxmusl-arm64@1.2.4': optional: true + '@img/sharp-libvips-linuxmusl-arm64@1.3.1': + optional: true + '@img/sharp-libvips-linuxmusl-x64@1.2.4': optional: true + '@img/sharp-libvips-linuxmusl-x64@1.3.1': + optional: true + '@img/sharp-linux-arm64@0.34.5': optionalDependencies: '@img/sharp-libvips-linux-arm64': 1.2.4 optional: true + '@img/sharp-linux-arm64@0.35.2': + optionalDependencies: + '@img/sharp-libvips-linux-arm64': 1.3.1 + optional: true + '@img/sharp-linux-arm@0.34.5': optionalDependencies: '@img/sharp-libvips-linux-arm': 1.2.4 optional: true + '@img/sharp-linux-arm@0.35.2': + optionalDependencies: + '@img/sharp-libvips-linux-arm': 1.3.1 + optional: true + '@img/sharp-linux-ppc64@0.34.5': optionalDependencies: '@img/sharp-libvips-linux-ppc64': 1.2.4 optional: true + '@img/sharp-linux-ppc64@0.35.2': + optionalDependencies: + '@img/sharp-libvips-linux-ppc64': 1.3.1 + optional: true + '@img/sharp-linux-riscv64@0.34.5': optionalDependencies: '@img/sharp-libvips-linux-riscv64': 1.2.4 optional: true + '@img/sharp-linux-riscv64@0.35.2': + optionalDependencies: + '@img/sharp-libvips-linux-riscv64': 1.3.1 + optional: true + '@img/sharp-linux-s390x@0.34.5': optionalDependencies: '@img/sharp-libvips-linux-s390x': 1.2.4 optional: true + '@img/sharp-linux-s390x@0.35.2': + optionalDependencies: + '@img/sharp-libvips-linux-s390x': 1.3.1 + optional: true + '@img/sharp-linux-x64@0.34.5': optionalDependencies: '@img/sharp-libvips-linux-x64': 1.2.4 optional: true + '@img/sharp-linux-x64@0.35.2': + optionalDependencies: + '@img/sharp-libvips-linux-x64': 1.3.1 + optional: true + '@img/sharp-linuxmusl-arm64@0.34.5': optionalDependencies: '@img/sharp-libvips-linuxmusl-arm64': 1.2.4 optional: true + '@img/sharp-linuxmusl-arm64@0.35.2': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-arm64': 1.3.1 + optional: true + '@img/sharp-linuxmusl-x64@0.34.5': optionalDependencies: '@img/sharp-libvips-linuxmusl-x64': 1.2.4 optional: true + '@img/sharp-linuxmusl-x64@0.35.2': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-x64': 1.3.1 + optional: true + '@img/sharp-wasm32@0.34.5': dependencies: '@emnapi/runtime': 1.11.1 optional: true + '@img/sharp-wasm32@0.35.2': + dependencies: + '@emnapi/runtime': 1.11.1 + optional: true + + '@img/sharp-webcontainers-wasm32@0.35.2': + dependencies: + '@img/sharp-wasm32': 0.35.2 + optional: true + '@img/sharp-win32-arm64@0.34.5': optional: true + '@img/sharp-win32-arm64@0.35.2': + optional: true + '@img/sharp-win32-ia32@0.34.5': optional: true + '@img/sharp-win32-ia32@0.35.2': + optional: true + '@img/sharp-win32-x64@0.34.5': optional: true + '@img/sharp-win32-x64@0.35.2': + optional: true + '@inquirer/core@6.0.0': dependencies: '@inquirer/type': 1.5.5 @@ -6992,6 +7659,66 @@ snapshots: '@opentelemetry/semantic-conventions@1.41.1': {} + '@parcel/watcher-android-arm64@2.5.6': + optional: true + + '@parcel/watcher-darwin-arm64@2.5.6': + optional: true + + '@parcel/watcher-darwin-x64@2.5.6': + optional: true + + '@parcel/watcher-freebsd-x64@2.5.6': + optional: true + + '@parcel/watcher-linux-arm-glibc@2.5.6': + optional: true + + '@parcel/watcher-linux-arm-musl@2.5.6': + optional: true + + '@parcel/watcher-linux-arm64-glibc@2.5.6': + optional: true + + '@parcel/watcher-linux-arm64-musl@2.5.6': + optional: true + + '@parcel/watcher-linux-x64-glibc@2.5.6': + optional: true + + '@parcel/watcher-linux-x64-musl@2.5.6': + optional: true + + '@parcel/watcher-win32-arm64@2.5.6': + optional: true + + '@parcel/watcher-win32-ia32@2.5.6': + optional: true + + '@parcel/watcher-win32-x64@2.5.6': + optional: true + + '@parcel/watcher@2.5.6': + dependencies: + detect-libc: 2.1.2 + is-glob: 4.0.3 + node-addon-api: 7.1.1 + picomatch: 4.0.4 + optionalDependencies: + '@parcel/watcher-android-arm64': 2.5.6 + '@parcel/watcher-darwin-arm64': 2.5.6 + '@parcel/watcher-darwin-x64': 2.5.6 + '@parcel/watcher-freebsd-x64': 2.5.6 + '@parcel/watcher-linux-arm-glibc': 2.5.6 + '@parcel/watcher-linux-arm-musl': 2.5.6 + '@parcel/watcher-linux-arm64-glibc': 2.5.6 + '@parcel/watcher-linux-arm64-musl': 2.5.6 + '@parcel/watcher-linux-x64-glibc': 2.5.6 + '@parcel/watcher-linux-x64-musl': 2.5.6 + '@parcel/watcher-win32-arm64': 2.5.6 + '@parcel/watcher-win32-ia32': 2.5.6 + '@parcel/watcher-win32-x64': 2.5.6 + '@peculiar/asn1-android@2.8.0': dependencies: '@peculiar/asn1-schema': 2.8.0 @@ -8088,6 +8815,8 @@ snapshots: '@rtsao/scc@1.1.0': {} + '@schummar/icu-type-parser@1.21.5': {} + '@selderee/plugin-htmlparser2@0.11.0': dependencies: domhandler: 5.0.3 @@ -8273,6 +9002,68 @@ snapshots: - encoding - supports-color + '@serwist/build@9.5.11(browserslist@4.28.2)(typescript@5.9.3)': + dependencies: + '@serwist/utils': 9.5.11(browserslist@4.28.2) + common-tags: 1.8.2 + glob: 13.0.6 + pretty-bytes: 6.1.1 + source-map: 0.8.0-beta.0 + type-fest: 5.6.0 + zod: 4.4.1 + optionalDependencies: + typescript: 5.9.3 + transitivePeerDependencies: + - browserslist + + '@serwist/next@9.5.11(next@16.2.9(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4)(typescript@5.9.3)(webpack@5.108.1)': + dependencies: + '@serwist/build': 9.5.11(browserslist@4.28.2)(typescript@5.9.3) + '@serwist/utils': 9.5.11(browserslist@4.28.2) + '@serwist/webpack-plugin': 9.5.11(browserslist@4.28.2)(typescript@5.9.3)(webpack@5.108.1) + '@serwist/window': 9.5.11(browserslist@4.28.2)(typescript@5.9.3) + browserslist: 4.28.2 + glob: 13.0.6 + kolorist: 1.8.0 + next: 16.2.9(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + react: 19.2.4 + semver: 7.7.4 + serwist: 9.5.11(browserslist@4.28.2)(typescript@5.9.3) + zod: 4.4.1 + optionalDependencies: + typescript: 5.9.3 + transitivePeerDependencies: + - webpack + + '@serwist/utils@9.5.11(browserslist@4.28.2)': + optionalDependencies: + browserslist: 4.28.2 + + '@serwist/utils@9.5.11(browserslist@4.28.4)': + optionalDependencies: + browserslist: 4.28.4 + + '@serwist/webpack-plugin@9.5.11(browserslist@4.28.2)(typescript@5.9.3)(webpack@5.108.1)': + dependencies: + '@serwist/build': 9.5.11(browserslist@4.28.2)(typescript@5.9.3) + '@serwist/utils': 9.5.11(browserslist@4.28.2) + pretty-bytes: 6.1.1 + zod: 4.4.1 + optionalDependencies: + typescript: 5.9.3 + webpack: 5.108.1 + transitivePeerDependencies: + - browserslist + + '@serwist/window@9.5.11(browserslist@4.28.2)(typescript@5.9.3)': + dependencies: + '@types/trusted-types': 2.0.7 + serwist: 9.5.11(browserslist@4.28.2)(typescript@5.9.3) + optionalDependencies: + typescript: 5.9.3 + transitivePeerDependencies: + - browserslist + '@simplewebauthn/browser@13.3.0': {} '@simplewebauthn/server@13.3.2': @@ -8294,10 +9085,70 @@ snapshots: '@standard-schema/utils@0.3.0': {} + '@swc/core-darwin-arm64@1.15.43': + optional: true + + '@swc/core-darwin-x64@1.15.43': + optional: true + + '@swc/core-linux-arm-gnueabihf@1.15.43': + optional: true + + '@swc/core-linux-arm64-gnu@1.15.43': + optional: true + + '@swc/core-linux-arm64-musl@1.15.43': + optional: true + + '@swc/core-linux-ppc64-gnu@1.15.43': + optional: true + + '@swc/core-linux-s390x-gnu@1.15.43': + optional: true + + '@swc/core-linux-x64-gnu@1.15.43': + optional: true + + '@swc/core-linux-x64-musl@1.15.43': + optional: true + + '@swc/core-win32-arm64-msvc@1.15.43': + optional: true + + '@swc/core-win32-ia32-msvc@1.15.43': + optional: true + + '@swc/core-win32-x64-msvc@1.15.43': + optional: true + + '@swc/core@1.15.43': + dependencies: + '@swc/counter': 0.1.3 + '@swc/types': 0.1.27 + optionalDependencies: + '@swc/core-darwin-arm64': 1.15.43 + '@swc/core-darwin-x64': 1.15.43 + '@swc/core-linux-arm-gnueabihf': 1.15.43 + '@swc/core-linux-arm64-gnu': 1.15.43 + '@swc/core-linux-arm64-musl': 1.15.43 + '@swc/core-linux-ppc64-gnu': 1.15.43 + '@swc/core-linux-s390x-gnu': 1.15.43 + '@swc/core-linux-x64-gnu': 1.15.43 + '@swc/core-linux-x64-musl': 1.15.43 + '@swc/core-win32-arm64-msvc': 1.15.43 + '@swc/core-win32-ia32-msvc': 1.15.43 + '@swc/core-win32-x64-msvc': 1.15.43 + + '@swc/counter@0.1.3': {} + '@swc/helpers@0.5.15': dependencies: tslib: 2.8.1 + '@swc/types@0.1.27': + dependencies: + '@swc/counter': 0.1.3 + '@tailwindcss/node@4.3.1': dependencies: '@jridgewell/remapping': 2.3.5 @@ -8367,16 +9218,33 @@ snapshots: postcss: 8.5.15 tailwindcss: 4.3.1 + '@tanstack/query-async-storage-persister@5.101.1': + dependencies: + '@tanstack/query-core': 5.101.1 + '@tanstack/query-persist-client-core': 5.101.1 + '@tanstack/query-core@5.101.1': {} + '@tanstack/query-core@5.101.2': {} + '@tanstack/query-devtools@5.101.1': {} + '@tanstack/query-persist-client-core@5.101.1': + dependencies: + '@tanstack/query-core': 5.101.1 + '@tanstack/react-query-devtools@5.101.1(@tanstack/react-query@5.101.1(react@19.2.4))(react@19.2.4)': dependencies: '@tanstack/query-devtools': 5.101.1 '@tanstack/react-query': 5.101.1(react@19.2.4) react: 19.2.4 + '@tanstack/react-query-persist-client@5.101.1(@tanstack/react-query@5.101.1(react@19.2.4))(react@19.2.4)': + dependencies: + '@tanstack/query-persist-client-core': 5.101.1 + '@tanstack/react-query': 5.101.1(react@19.2.4) + react: 19.2.4 + '@tanstack/react-query@5.101.1(react@19.2.4)': dependencies: '@tanstack/query-core': 5.101.1 @@ -8509,6 +9377,8 @@ snapshots: dependencies: csstype: 3.2.3 + '@types/trusted-types@2.0.7': {} + '@types/use-sync-external-store@0.0.6': {} '@types/wrap-ansi@3.0.0': {} @@ -9045,6 +9915,14 @@ snapshots: dependencies: fill-range: 7.1.1 + browserslist@4.28.2: + dependencies: + baseline-browser-mapping: 2.10.40 + caniuse-lite: 1.0.30001799 + electron-to-chromium: 1.5.379 + node-releases: 2.0.50 + update-browserslist-db: 1.2.3(browserslist@4.28.2) + browserslist@4.28.4: dependencies: baseline-browser-mapping: 2.10.40 @@ -9144,6 +10022,8 @@ snapshots: commander@8.3.0: {} + common-tags@1.8.2: {} + commondir@1.0.1: {} compare-versions@6.1.1: {} @@ -9680,6 +10560,10 @@ snapshots: transitivePeerDependencies: - supports-color + eslint-plugin-i18next@6.1.5: + dependencies: + requireindex: 1.1.0 + eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.62.0(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.7.0)): dependencies: '@rtsao/scc': 1.1.0 @@ -10115,6 +10999,14 @@ snapshots: - kerberos - supports-color + icu-minify@4.13.0: + dependencies: + '@formatjs/icu-messageformat-parser': 3.5.12 + + idb-keyval@6.2.5: {} + + idb@8.0.3: {} + ieee754@1.2.1: {} ignore@5.3.2: {} @@ -10152,6 +11044,11 @@ snapshots: internmap@2.0.3: {} + intl-messageformat@11.2.9: + dependencies: + '@formatjs/fast-memoize': 3.1.6 + '@formatjs/icu-messageformat-parser': 3.5.12 + ip-address@10.2.0: {} is-array-buffer@3.0.5: @@ -10352,6 +11249,8 @@ snapshots: kleur@4.1.5: {} + kolorist@1.8.0: {} + kysely@0.28.17: {} language-subtag-registry@0.3.23: {} @@ -10426,6 +11325,8 @@ snapshots: lodash.merge@4.6.2: {} + lodash.sortby@4.7.0: {} + log-symbols@7.0.1: dependencies: is-unicode-supported: 2.1.0 @@ -10538,10 +11439,31 @@ snapshots: negotiator@0.6.3: {} + negotiator@1.0.0: {} + neo-async@2.6.2: {} netmask@2.1.1: {} + next-intl-swc-plugin-extractor@4.13.0: {} + + next-intl@4.13.0(next@16.2.9(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4)(typescript@5.9.3): + dependencies: + '@formatjs/intl-localematcher': 0.8.10 + '@parcel/watcher': 2.5.6 + '@swc/core': 1.15.43 + icu-minify: 4.13.0 + negotiator: 1.0.0 + next: 16.2.9(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + next-intl-swc-plugin-extractor: 4.13.0 + po-parser: 2.1.1 + react: 19.2.4 + use-intl: 4.13.0(react@19.2.4) + optionalDependencies: + typescript: 5.9.3 + transitivePeerDependencies: + - '@swc/helpers' + next@16.2.9(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4): dependencies: '@next/env': 16.2.9 @@ -10567,6 +11489,8 @@ snapshots: - '@babel/core' - babel-plugin-macros + node-addon-api@7.1.1: {} + node-exports-info@1.6.2: dependencies: array.prototype.flatmap: 1.3.3 @@ -10750,6 +11674,8 @@ snapshots: optionalDependencies: fsevents: 2.3.2 + po-parser@2.1.1: {} + possible-typed-array-names@1.1.0: {} postal-mime@2.7.4: {} @@ -10782,6 +11708,8 @@ snapshots: prettier@3.8.5: {} + pretty-bytes@6.1.1: {} + prismjs@1.30.0: {} progress@2.0.3: {} @@ -11082,6 +12010,8 @@ snapshots: transitivePeerDependencies: - supports-color + requireindex@1.1.0: {} + reselect@5.2.0: {} resend@6.16.0(@react-email/render@2.0.9(react-dom@19.2.4(react@19.2.4))(react@19.2.4)): @@ -11187,10 +12117,30 @@ snapshots: semver@6.3.1: {} + semver@7.7.4: {} + semver@7.8.5: {} server-only@0.0.1: {} + serwist@9.5.11(browserslist@4.28.2)(typescript@5.9.3): + dependencies: + '@serwist/utils': 9.5.11(browserslist@4.28.2) + idb: 8.0.3 + optionalDependencies: + typescript: 5.9.3 + transitivePeerDependencies: + - browserslist + + serwist@9.5.11(browserslist@4.28.4)(typescript@5.9.3): + dependencies: + '@serwist/utils': 9.5.11(browserslist@4.28.4) + idb: 8.0.3 + optionalDependencies: + typescript: 5.9.3 + transitivePeerDependencies: + - browserslist + set-cookie-parser@3.1.1: {} set-function-length@1.2.2: @@ -11247,6 +12197,38 @@ snapshots: '@img/sharp-win32-x64': 0.34.5 optional: true + sharp@0.35.2: + dependencies: + '@img/colour': 1.1.0 + detect-libc: 2.1.2 + semver: 7.8.5 + optionalDependencies: + '@img/sharp-darwin-arm64': 0.35.2 + '@img/sharp-darwin-x64': 0.35.2 + '@img/sharp-freebsd-wasm32': 0.35.2 + '@img/sharp-libvips-darwin-arm64': 1.3.1 + '@img/sharp-libvips-darwin-x64': 1.3.1 + '@img/sharp-libvips-linux-arm': 1.3.1 + '@img/sharp-libvips-linux-arm64': 1.3.1 + '@img/sharp-libvips-linux-ppc64': 1.3.1 + '@img/sharp-libvips-linux-riscv64': 1.3.1 + '@img/sharp-libvips-linux-s390x': 1.3.1 + '@img/sharp-libvips-linux-x64': 1.3.1 + '@img/sharp-libvips-linuxmusl-arm64': 1.3.1 + '@img/sharp-libvips-linuxmusl-x64': 1.3.1 + '@img/sharp-linux-arm': 0.35.2 + '@img/sharp-linux-arm64': 0.35.2 + '@img/sharp-linux-ppc64': 0.35.2 + '@img/sharp-linux-riscv64': 0.35.2 + '@img/sharp-linux-s390x': 0.35.2 + '@img/sharp-linux-x64': 0.35.2 + '@img/sharp-linuxmusl-arm64': 0.35.2 + '@img/sharp-linuxmusl-x64': 0.35.2 + '@img/sharp-webcontainers-wasm32': 0.35.2 + '@img/sharp-win32-arm64': 0.35.2 + '@img/sharp-win32-ia32': 0.35.2 + '@img/sharp-win32-x64': 0.35.2 + shebang-command@2.0.0: dependencies: shebang-regex: 3.0.0 @@ -11348,6 +12330,10 @@ snapshots: source-map@0.6.1: {} + source-map@0.8.0-beta.0: + dependencies: + whatwg-url: 7.1.0 + split2@4.2.0: {} stable-hash@0.0.5: {} @@ -11510,6 +12496,10 @@ snapshots: tr46@0.0.3: {} + tr46@1.0.1: + dependencies: + punycode: 2.3.1 + tree-kill@1.2.2: {} ts-api-utils@2.5.0(typescript@5.9.3): @@ -11553,6 +12543,10 @@ snapshots: type-fest@0.7.1: {} + type-fest@5.6.0: + dependencies: + tagged-tag: 1.0.0 + type-fest@5.7.0: dependencies: tagged-tag: 1.0.0 @@ -11655,6 +12649,12 @@ snapshots: '@unrs/resolver-binding-win32-ia32-msvc': 1.12.2 '@unrs/resolver-binding-win32-x64-msvc': 1.12.2 + update-browserslist-db@1.2.3(browserslist@4.28.2): + dependencies: + browserslist: 4.28.2 + escalade: 3.2.0 + picocolors: 1.1.1 + update-browserslist-db@1.2.3(browserslist@4.28.4): dependencies: browserslist: 4.28.4 @@ -11672,6 +12672,14 @@ snapshots: optionalDependencies: '@types/react': 19.2.14 + use-intl@4.13.0(react@19.2.4): + dependencies: + '@formatjs/fast-memoize': 3.1.6 + '@schummar/icu-type-parser': 1.21.5 + icu-minify: 4.13.0 + intl-messageformat: 11.2.9 + react: 19.2.4 + use-sidecar@1.1.3(@types/react@19.2.14)(react@19.2.4): dependencies: detect-node-es: 1.1.0 @@ -11731,6 +12739,8 @@ snapshots: webidl-conversions@3.0.1: {} + webidl-conversions@4.0.2: {} + webpack-sources@3.5.0: {} webpack-virtual-modules@0.6.2: @@ -11783,6 +12793,12 @@ snapshots: tr46: 0.0.3 webidl-conversions: 3.0.1 + whatwg-url@7.1.0: + dependencies: + lodash.sortby: 4.7.0 + tr46: 1.0.1 + webidl-conversions: 4.0.2 + when-exit@2.1.5: {} which-boxed-primitive@1.1.1: @@ -11871,6 +12887,8 @@ snapshots: dependencies: zod: 4.4.3 + zod@4.4.1: {} + zod@4.4.3: {} zxing-wasm@3.1.0(@types/emscripten@1.41.5): diff --git a/frontend/pnpm-workspace.yaml b/frontend/pnpm-workspace.yaml index 0d2b6a23..76c46b88 100644 --- a/frontend/pnpm-workspace.yaml +++ b/frontend/pnpm-workspace.yaml @@ -1,6 +1,8 @@ allowBuilds: '@openapitools/openapi-generator-cli': set this to true or false + '@parcel/watcher': set this to true or false '@sentry/cli': set this to true or false + '@swc/core': set this to true or false core-js: set this to true or false esbuild: set this to true or false sharp: set this to true or false diff --git a/frontend/public/icons/apple-touch-icon-180.png b/frontend/public/icons/apple-touch-icon-180.png new file mode 100644 index 00000000..f5f23d0c Binary files /dev/null and b/frontend/public/icons/apple-touch-icon-180.png differ diff --git a/frontend/public/icons/icon-192.png b/frontend/public/icons/icon-192.png new file mode 100644 index 00000000..955b8566 Binary files /dev/null and b/frontend/public/icons/icon-192.png differ diff --git a/frontend/public/icons/icon-512.png b/frontend/public/icons/icon-512.png new file mode 100644 index 00000000..6511f954 Binary files /dev/null and b/frontend/public/icons/icon-512.png differ diff --git a/frontend/public/icons/icon-maskable-512.png b/frontend/public/icons/icon-maskable-512.png new file mode 100644 index 00000000..0e7dd3b6 Binary files /dev/null and b/frontend/public/icons/icon-maskable-512.png differ diff --git a/frontend/public/screenshots/screenshot-narrow.png b/frontend/public/screenshots/screenshot-narrow.png new file mode 100644 index 00000000..f108aa24 Binary files /dev/null and b/frontend/public/screenshots/screenshot-narrow.png differ diff --git a/frontend/public/screenshots/screenshot-wide.png b/frontend/public/screenshots/screenshot-wide.png new file mode 100644 index 00000000..8fe69dda Binary files /dev/null and b/frontend/public/screenshots/screenshot-wide.png differ diff --git a/frontend/public/splash/apple-splash-1080-2340.png b/frontend/public/splash/apple-splash-1080-2340.png new file mode 100644 index 00000000..5636deb8 Binary files /dev/null and b/frontend/public/splash/apple-splash-1080-2340.png differ diff --git a/frontend/public/splash/apple-splash-1125-2436.png b/frontend/public/splash/apple-splash-1125-2436.png new file mode 100644 index 00000000..9d458fcd Binary files /dev/null and b/frontend/public/splash/apple-splash-1125-2436.png differ diff --git a/frontend/public/splash/apple-splash-1170-2532.png b/frontend/public/splash/apple-splash-1170-2532.png new file mode 100644 index 00000000..1db208d7 Binary files /dev/null and b/frontend/public/splash/apple-splash-1170-2532.png differ diff --git a/frontend/public/splash/apple-splash-1179-2556.png b/frontend/public/splash/apple-splash-1179-2556.png new file mode 100644 index 00000000..c1a01432 Binary files /dev/null and b/frontend/public/splash/apple-splash-1179-2556.png differ diff --git a/frontend/public/splash/apple-splash-1206-2622.png b/frontend/public/splash/apple-splash-1206-2622.png new file mode 100644 index 00000000..e6b7f149 Binary files /dev/null and b/frontend/public/splash/apple-splash-1206-2622.png differ diff --git a/frontend/public/splash/apple-splash-1242-2208.png b/frontend/public/splash/apple-splash-1242-2208.png new file mode 100644 index 00000000..a019eda0 Binary files /dev/null and b/frontend/public/splash/apple-splash-1242-2208.png differ diff --git a/frontend/public/splash/apple-splash-1242-2688.png b/frontend/public/splash/apple-splash-1242-2688.png new file mode 100644 index 00000000..aef04bc6 Binary files /dev/null and b/frontend/public/splash/apple-splash-1242-2688.png differ diff --git a/frontend/public/splash/apple-splash-1284-2778.png b/frontend/public/splash/apple-splash-1284-2778.png new file mode 100644 index 00000000..1dfffd43 Binary files /dev/null and b/frontend/public/splash/apple-splash-1284-2778.png differ diff --git a/frontend/public/splash/apple-splash-1290-2796.png b/frontend/public/splash/apple-splash-1290-2796.png new file mode 100644 index 00000000..330bb22e Binary files /dev/null and b/frontend/public/splash/apple-splash-1290-2796.png differ diff --git a/frontend/public/splash/apple-splash-1320-2868.png b/frontend/public/splash/apple-splash-1320-2868.png new file mode 100644 index 00000000..a7216d58 Binary files /dev/null and b/frontend/public/splash/apple-splash-1320-2868.png differ diff --git a/frontend/public/splash/apple-splash-1536-2048.png b/frontend/public/splash/apple-splash-1536-2048.png new file mode 100644 index 00000000..075103c7 Binary files /dev/null and b/frontend/public/splash/apple-splash-1536-2048.png differ diff --git a/frontend/public/splash/apple-splash-1620-2160.png b/frontend/public/splash/apple-splash-1620-2160.png new file mode 100644 index 00000000..7b0e0ce0 Binary files /dev/null and b/frontend/public/splash/apple-splash-1620-2160.png differ diff --git a/frontend/public/splash/apple-splash-1640-2360.png b/frontend/public/splash/apple-splash-1640-2360.png new file mode 100644 index 00000000..8fbda9dc Binary files /dev/null and b/frontend/public/splash/apple-splash-1640-2360.png differ diff --git a/frontend/public/splash/apple-splash-1668-2224.png b/frontend/public/splash/apple-splash-1668-2224.png new file mode 100644 index 00000000..30ef9295 Binary files /dev/null and b/frontend/public/splash/apple-splash-1668-2224.png differ diff --git a/frontend/public/splash/apple-splash-1668-2388.png b/frontend/public/splash/apple-splash-1668-2388.png new file mode 100644 index 00000000..9d6118c7 Binary files /dev/null and b/frontend/public/splash/apple-splash-1668-2388.png differ diff --git a/frontend/public/splash/apple-splash-2048-2732.png b/frontend/public/splash/apple-splash-2048-2732.png new file mode 100644 index 00000000..56ef87bb Binary files /dev/null and b/frontend/public/splash/apple-splash-2048-2732.png differ diff --git a/frontend/public/splash/apple-splash-750-1334.png b/frontend/public/splash/apple-splash-750-1334.png new file mode 100644 index 00000000..ecbf99f0 Binary files /dev/null and b/frontend/public/splash/apple-splash-750-1334.png differ diff --git a/frontend/public/splash/apple-splash-828-1792.png b/frontend/public/splash/apple-splash-828-1792.png new file mode 100644 index 00000000..0353d191 Binary files /dev/null and b/frontend/public/splash/apple-splash-828-1792.png differ diff --git a/frontend/scripts/check-i18n.mjs b/frontend/scripts/check-i18n.mjs new file mode 100644 index 00000000..a67c1162 --- /dev/null +++ b/frontend/scripts/check-i18n.mjs @@ -0,0 +1,73 @@ +// Verifies that every locale catalog in src/i18n/messages/ has exactly the same +// set of keys as the reference locale (hr). Run via `pnpm i18n:check`. +// Exits non-zero on any missing/extra key so CI fails when a translation is +// forgotten in one language. +import { readFileSync } from "node:fs"; +import { fileURLToPath } from "node:url"; +import { dirname, join } from "node:path"; + +const MESSAGES_DIR = join( + dirname(fileURLToPath(import.meta.url)), + "..", + "src", + "i18n", + "messages", +); + +const REFERENCE = "hr"; +const LOCALES = ["hr", "en", "de", "sl"]; + +function flattenKeys(obj, prefix = "") { + const keys = []; + + for (const [key, value] of Object.entries(obj)) { + const path = prefix ? `${prefix}.${key}` : key; + + if (value && typeof value === "object" && !Array.isArray(value)) { + keys.push(...flattenKeys(value, path)); + } else { + keys.push(path); + } + } + + return keys.sort(); +} + +function loadKeys(locale) { + const raw = readFileSync(join(MESSAGES_DIR, `${locale}.json`), "utf8"); + return flattenKeys(JSON.parse(raw)); +} + +const reference = loadKeys(REFERENCE); +let hasError = false; + +for (const locale of LOCALES) { + if (locale === REFERENCE) continue; + + const keys = loadKeys(locale); + const missing = reference.filter((key) => !keys.includes(key)); + const extra = keys.filter((key) => !reference.includes(key)); + + if (missing.length > 0) { + hasError = true; + console.error(`\n❌ ${locale}.json is MISSING ${missing.length} key(s):`); + missing.forEach((key) => console.error(` - ${key}`)); + } + + if (extra.length > 0) { + hasError = true; + console.error(`\n❌ ${locale}.json has ${extra.length} EXTRA key(s):`); + extra.forEach((key) => console.error(` + ${key}`)); + } +} + +if (hasError) { + console.error( + "\ni18n parity check failed. Every locale must define the same keys as hr.\n", + ); + process.exit(1); +} + +console.log( + `✅ i18n parity OK — ${reference.length} keys across ${LOCALES.join(", ")}.`, +); diff --git a/frontend/scripts/generate-ios-splash.mjs b/frontend/scripts/generate-ios-splash.mjs new file mode 100644 index 00000000..74286450 --- /dev/null +++ b/frontend/scripts/generate-ios-splash.mjs @@ -0,0 +1,46 @@ +// One-off generator for iOS launch (splash) screens. Run from the frontend dir: +// node scripts/generate-ios-splash.mjs +// iOS ignores the web manifest for launch screens and instead shows the image +// referenced by a device-matched . +// The device list is shared with the component +// (src/constants/ios-splash-screens.json) so the tags and files never drift. +import sharp from "sharp"; +import { readFile, mkdir } from "node:fs/promises"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +const ROOT = path.join(path.dirname(fileURLToPath(import.meta.url)), ".."); +const SRC_LOGO = path.join(ROOT, "public/disscount-logo.png"); +const OUT = path.join(ROOT, "public/splash"); + +const BACKGROUND = { r: 255, g: 255, b: 255, alpha: 1 }; +const LOGO_RATIO = 0.35; // logo occupies ~35% of the shorter edge + +const devices = JSON.parse( + await readFile(path.join(ROOT, "src/constants/ios-splash-screens.json"), "utf8"), +); + +await mkdir(OUT, { recursive: true }); + +for (const device of devices) { + const pxWidth = device.width * device.ratio; + const pxHeight = device.height * device.ratio; + const logoSize = Math.round(Math.min(pxWidth, pxHeight) * LOGO_RATIO); + + const logo = await sharp(SRC_LOGO) + .resize(logoSize, logoSize, { + fit: "contain", + background: { r: 0, g: 0, b: 0, alpha: 0 }, + }) + .png() + .toBuffer(); + + await sharp({ + create: { width: pxWidth, height: pxHeight, channels: 4, background: BACKGROUND }, + }) + .composite([{ input: logo, gravity: "centre" }]) + .png() + .toFile(path.join(OUT, `apple-splash-${pxWidth}-${pxHeight}.png`)); +} + +console.log(`Generated ${devices.length} iOS splash screens in public/splash/`); diff --git a/frontend/scripts/generate-pwa-icons.mjs b/frontend/scripts/generate-pwa-icons.mjs new file mode 100644 index 00000000..ae0c66cb --- /dev/null +++ b/frontend/scripts/generate-pwa-icons.mjs @@ -0,0 +1,47 @@ +// One-off generator for the PWA icon set. Run from the frontend dir: +// node scripts/generate-pwa-icons.mjs +// Produces the icons referenced by app/manifest.ts and layout metadata. +import sharp from "sharp"; +import { mkdir } from "node:fs/promises"; +import path from "node:path"; + +const SRC = "public/disscount-logo.png"; +const OUT = "public/icons"; + +const transparent = { r: 0, g: 0, b: 0, alpha: 0 }; +const white = { r: 255, g: 255, b: 255, alpha: 1 }; + +// "any"-purpose icon: the logo on a transparent canvas. +async function generateTransparent(size, file) { + await sharp(SRC) + .resize(size, size, { fit: "contain", background: transparent }) + .png() + .toFile(path.join(OUT, file)); +} + +// maskable / apple icon: the logo centered on a solid background with padding +// so OS masks (circle, squircle) never clip it, and iOS gets no transparency. +async function generateOnBackground(size, logoRatio, background, file) { + const logoSize = Math.round(size * logoRatio); + + const logo = await sharp(SRC) + .resize(logoSize, logoSize, { fit: "contain", background: transparent }) + .png() + .toBuffer(); + + await sharp({ + create: { width: size, height: size, channels: 4, background }, + }) + .composite([{ input: logo, gravity: "centre" }]) + .png() + .toFile(path.join(OUT, file)); +} + +await mkdir(OUT, { recursive: true }); + +await generateTransparent(192, "icon-192.png"); +await generateTransparent(512, "icon-512.png"); +await generateOnBackground(512, 0.66, white, "icon-maskable-512.png"); +await generateOnBackground(180, 0.82, white, "apple-touch-icon-180.png"); + +console.log(`Generated PWA icons in ${OUT}/`); diff --git a/frontend/scripts/generate-pwa-screenshots.mjs b/frontend/scripts/generate-pwa-screenshots.mjs new file mode 100644 index 00000000..9d221d36 --- /dev/null +++ b/frontend/scripts/generate-pwa-screenshots.mjs @@ -0,0 +1,67 @@ +// One-off generator for the manifest `screenshots` (branded promo cards shown +// in Chrome's richer install dialog). Run from the frontend dir: +// node scripts/generate-pwa-screenshots.mjs +// These are placeholder brand cards — swap them for real app captures whenever +// you have them; the manifest wiring stays the same. +import sharp from "sharp"; +import { mkdir } from "node:fs/promises"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +const ROOT = path.join(path.dirname(fileURLToPath(import.meta.url)), ".."); +const SRC_LOGO = path.join(ROOT, "public/disscount-logo.png"); +const OUT = path.join(ROOT, "public/screenshots"); + +const NAVY = "#143a5a"; +const GRAY = "#6b7280"; +const GREEN = "#2ec50d"; +const TAGLINE = "Pronađi najbolje cijene u Hrvatskoj"; + +async function generateScreenshot({ width, height, logoSize, logoTop, fontSize, file }) { + const centerX = width / 2; + const wordmarkY = logoTop + logoSize + fontSize; + const accentY = wordmarkY + fontSize * 0.35; + const taglineY = accentY + fontSize * 0.9; + + const svg = ` + + + Disscount + + ${TAGLINE} + `; + + const logo = await sharp(SRC_LOGO) + .resize(logoSize, logoSize, { fit: "contain", background: { r: 0, g: 0, b: 0, alpha: 0 } }) + .png() + .toBuffer(); + + await sharp(Buffer.from(svg)) + .composite([{ input: logo, top: logoTop, left: Math.round((width - logoSize) / 2) }]) + .png() + .toFile(path.join(OUT, file)); +} + +await mkdir(OUT, { recursive: true }); + +await generateScreenshot({ + width: 1080, + height: 1920, + logoSize: 420, + logoTop: 560, + fontSize: 110, + file: "screenshot-narrow.png", +}); + +await generateScreenshot({ + width: 1920, + height: 1080, + logoSize: 300, + logoTop: 230, + fontSize: 96, + file: "screenshot-wide.png", +}); + +console.log("Generated PWA screenshots in public/screenshots/"); diff --git a/frontend/src/app/(root)/components/sections/hero-actions.tsx b/frontend/src/app/(root)/components/sections/hero-actions.tsx index 01073169..fb44f3aa 100644 --- a/frontend/src/app/(root)/components/sections/hero-actions.tsx +++ b/frontend/src/app/(root)/components/sections/hero-actions.tsx @@ -2,6 +2,7 @@ import { useCallback, Suspense } from "react"; import { useRouter } from "next/navigation"; +import { useTranslations } from "next-intl"; import { Button } from "@/components/ui/button"; import { ScanBarcode } from "lucide-react"; import { Card } from "@/components/ui/card"; @@ -11,6 +12,7 @@ import SearchBar from "@/components/custom/search-bar"; import SearchBarSkeleton from "@/components/custom/search-bar-skeleton"; export default function HeroActions() { + const t = useTranslations("hero"); const { openScanner } = useCameraScanner(); const router = useRouter(); @@ -28,7 +30,7 @@ export default function HeroActions() { }>

- ili + {t("or")}

@@ -53,7 +55,7 @@ export default function HeroActions() { className="w-full text-lg" > - Skeniraj barkod + {t("scanBarcode")}
diff --git a/frontend/src/app/(root)/components/sections/hero-section.tsx b/frontend/src/app/(root)/components/sections/hero-section.tsx index d6ab2be8..5c1923b9 100644 --- a/frontend/src/app/(root)/components/sections/hero-section.tsx +++ b/frontend/src/app/(root)/components/sections/hero-section.tsx @@ -1,29 +1,19 @@ import Image from "next/image"; +import { getTranslations } from "next-intl/server"; import HeroActions from "@/app/(root)/components/sections/hero-actions"; -const tagLines: string[] = [ - "Usporedi trgovine i cijene!", - "Uštedi pri svakoj kupnji!", - "Nikad ne propusti akciju!", - "Zaboravi kartice - olakšaj novčanik!", - "Izradi i podijeli popis za kupnju!", - "Prati povijest cijena!", - "Skeniraj barkod i usporedi cijene!", - "Uživaj u pametnom kupovanju!", - "Pronađi najbolje ponude u Hrvatskoj!", - "Kupuj kvalitetno i jeftino!", - "Kupuj pametno, uštedi više!", - "Tvoj partner u pametnoj kupovini!", - "Zaboravi na kataloge i letke!", -]; - -function getTagLine(): string { - const randomIndex = Math.floor(Math.random() * tagLines.length); - return tagLines[randomIndex]; +function pickRandom(taglines: string[]): string { + const randomIndex = Math.floor(Math.random() * taglines.length); + return taglines[randomIndex]; } -export default function HeroSection() { - const tagLine: string = getTagLine(); +export default async function HeroSection() { + const t = await getTranslations("hero"); + const tCommon = await getTranslations("common"); + + // `taglines` is an array message; `t.raw` returns it untransformed. + const taglines = t.raw("taglines") as string[]; + const tagLine = pickRandom(taglines); return (
@@ -32,7 +22,7 @@ export default function HeroSection() { {/* App logo */} Disscount logo { + const t = await getTranslations("pages.home"); + + return { + title: t("metaTitle"), + description: t("metaDescription"), + }; +} export default function Home() { return ( diff --git a/frontend/src/app/(user)/digital-cards/components/digital-card-item.tsx b/frontend/src/app/(user)/digital-cards/components/digital-card-item.tsx index bc4f4d9b..b9f25d56 100644 --- a/frontend/src/app/(user)/digital-cards/components/digital-card-item.tsx +++ b/frontend/src/app/(user)/digital-cards/components/digital-card-item.tsx @@ -13,6 +13,7 @@ import { } from "@/components/ui/dropdown-menu"; import { digitalCardService } from "@/lib/api"; import { toast } from "sonner"; +import { useTranslations } from "next-intl"; import { useQueryClient } from "@tanstack/react-query"; import { formatDate } from "@/utils/strings"; import { useUser } from "@/context/user-context"; @@ -27,19 +28,21 @@ export default function DigitalCardItem({ handleEdit, }: IDigitalCardItemProps) { const queryClient = useQueryClient(); + const t = useTranslations("pages.digitalCards"); + const tCommon = useTranslations("common"); const deleteDigitalCardMutation = digitalCardService.useDeleteDigitalCard(); async function handleDelete(d: DigitalCardDto) { - if (confirm(`Jeste li sigurni da želite obrisati karticu "${d.title}"?`)) { + if (confirm(t("deleteConfirm", { title: d.title }))) { deleteDigitalCardMutation.mutate(d.id, { onSuccess: async () => { - toast.success("Kartica obrisana."); + toast.success(t("deleted")); await queryClient.invalidateQueries({ queryKey: ["digitalCards"], }); }, onError: (error: Error) => { - toast.error("Greška prilikom brisanja: " + error.message); + toast.error(t("deleteError", { message: error.message })); }, }); } @@ -69,7 +72,7 @@ export default function DigitalCardItem({ handleEdit(digitalCard)}> - Uredi + {t("edit")} - Obriši + {tCommon("delete")} @@ -96,7 +99,7 @@ export default function DigitalCardItem({ )}

- Stvoreno {formatDate(digitalCard.createdAt)} + {t("createdLabel", { date: formatDate(digitalCard.createdAt) })}

diff --git a/frontend/src/app/(user)/digital-cards/components/digital-cards-client.tsx b/frontend/src/app/(user)/digital-cards/components/digital-cards-client.tsx index 609a0788..e5073774 100644 --- a/frontend/src/app/(user)/digital-cards/components/digital-cards-client.tsx +++ b/frontend/src/app/(user)/digital-cards/components/digital-cards-client.tsx @@ -3,6 +3,7 @@ import { useState, Suspense } from "react"; import { usePathname } from "next/navigation"; import { Search, Plus, Loader2, CreditCard } from "lucide-react"; +import { useTranslations } from "next-intl"; import { Button } from "@/components/ui/button"; import SearchBar from "@/components/custom/search-bar"; import SearchBarSkeleton from "@/components/custom/search-bar-skeleton"; @@ -21,6 +22,7 @@ import { useQueryClient } from "@tanstack/react-query"; import BlockLoadingSpinner from "@/components/custom/block-loading-spinner"; export default function DigitalCardsClient({ query }: { query: string }) { + const t = useTranslations("pages.digitalCards"); const pathname = usePathname(); const queryClient = useQueryClient(); const [isModalOpen, setIsModalOpen] = useState(false); @@ -64,13 +66,13 @@ export default function DigitalCardsClient({ query }: { query: string }) { setIsModalOpen(true)} icon={} - label="Dodaj digitalnu karticu" + label={t("add")} />
}>

{query.length > 0 - ? `Rezultati pretrage za "${query}" (${matchingDigitalCards.length})` - : `Moje digitalne kartice${ - isUserLoading ? "" : ` (${matchingDigitalCards.length})` - }`} + ? t("searchResults", { + query, + count: matchingDigitalCards.length, + }) + : t("heading", { + count: isUserLoading + ? "" + : ` (${matchingDigitalCards.length})`, + })}

{/* */} @@ -119,12 +126,10 @@ export default function DigitalCardsClient({ query }: { query: string }) {

- Nema digitalnih kartica + {t("emptyTitle")}

-

- Stvorite svoju prvu digitalnu karticu… -

+

{t("emptyDescription")}

)} diff --git a/frontend/src/app/(user)/digital-cards/components/forms/digital-card-modal.tsx b/frontend/src/app/(user)/digital-cards/components/forms/digital-card-modal.tsx index fb7dbb82..4993f902 100644 --- a/frontend/src/app/(user)/digital-cards/components/forms/digital-card-modal.tsx +++ b/frontend/src/app/(user)/digital-cards/components/forms/digital-card-modal.tsx @@ -3,6 +3,7 @@ import { useForm } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; import { Save } from "lucide-react"; +import { useTranslations } from "next-intl"; import { Dialog, DialogContent, @@ -51,6 +52,8 @@ export default function DigitalCardModal({ digitalCard, }: IDigitalCardModalProps) { const queryClient = useQueryClient(); + const t = useTranslations("pages.digitalCards.modal"); + const tCommon = useTranslations("common"); const createMutation = digitalCardService.useCreateDigitalCard(); const updateMutation = digitalCardService.useUpdateDigitalCard(); @@ -73,7 +76,7 @@ export default function DigitalCardModal({ { id: digitalCard.id, data }, { onSuccess: async () => { - toast.success("Digitalna kartica je uspješno ažurirana!"); + toast.success(t("updated")); await queryClient.invalidateQueries({ queryKey: ["digitalCards"], }); @@ -84,8 +87,7 @@ export default function DigitalCardModal({ const message = error instanceof Error ? error.message : String(error); form.setError("root", { - message: - message || "Došlo je do greške prilikom ažuriranja kartice", + message: message || t("updateError"), }); }, } @@ -93,7 +95,7 @@ export default function DigitalCardModal({ } else { createMutation.mutate(data, { onSuccess: async () => { - toast.success("Digitalna kartica je uspješno kreirana!"); + toast.success(t("created")); await queryClient.invalidateQueries({ queryKey: ["digitalCards"], }); @@ -104,7 +106,7 @@ export default function DigitalCardModal({ const message = error instanceof Error ? error.message : String(error); form.setError("root", { - message: message || "Došlo je do greške prilikom kreiranja kartice", + message: message || t("createError"), }); }, }); @@ -123,7 +125,7 @@ export default function DigitalCardModal({ - {digitalCard ? "Uredi digitalnu karticu" : "Nova digitalna kartica"} + {digitalCard ? t("editTitle") : t("createTitle")} @@ -140,7 +142,7 @@ export default function DigitalCardModal({ name="title" render={({ field }) => ( - Naziv kartice + {t("titleLabel")} @@ -154,7 +156,7 @@ export default function DigitalCardModal({ name="value" render={({ field }) => ( - Vrijednost/Kod + {t("valueLabel")} @@ -169,22 +171,28 @@ export default function DigitalCardModal({ name="type" render={({ field }) => ( - Tip kartice + {t("typeLabel")} @@ -197,21 +205,23 @@ export default function DigitalCardModal({ name="codeType" render={({ field }) => ( - Tip koda + {t("codeTypeLabel")} @@ -225,7 +235,7 @@ export default function DigitalCardModal({ name="color" render={({ field }) => ( - Boja kartice + {t("colorLabel")} ( - Napomena + {t("noteLabel")}