Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions .claude/skills/document-subsystem/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
---
name: document-subsystem
description: Write a detailed reference doc (docs/<NAME>.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/<NAME>.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`.
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -66,4 +66,6 @@ frontend/yarn.lock
*.bak
*.orig

desktop.ini
desktop.ini

docs/disscount_*
9 changes: 9 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/<locale>.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.
Expand Down
4 changes: 4 additions & 0 deletions frontend/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@
/.next/
/out/

# serwist (generated service worker)
public/sw*
public/swe-worker*

# production
/build

Expand Down
71 changes: 71 additions & 0 deletions frontend/eslint.config.mjs
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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;
23 changes: 21 additions & 2 deletions frontend/next.config.ts
Original file line number Diff line number Diff line change
@@ -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",
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -90,4 +108,5 @@ export default withSentryConfig(nextConfig, {
removeDebugLogging: true,
},
},
});
}),
);
11 changes: 10 additions & 1 deletion frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Comment on lines 6 to 12
},
"dependencies": {
Expand All @@ -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",
Expand All @@ -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",
Expand Down Expand Up @@ -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"
Expand Down
Loading