Skip to content
Merged
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
9 changes: 4 additions & 5 deletions .env.docker.example
Original file line number Diff line number Diff line change
Expand Up @@ -35,12 +35,11 @@ THE_GAMES_DB_API_KEY="your_tgdb_api_key_here"
TUNNEL_TOKEN="your_cloudflare_tunnel_token_here"

# ===========================================
# RECAPTCHA (Optional)
# CLOUDFLARE TURNSTILE (Optional)
# ===========================================
# Google reCAPTCHA for form protection
NEXT_PUBLIC_RECAPTCHA_SITE_KEY="your_recaptcha_site_key_here"
RECAPTCHA_SECRET_KEY="your_recaptcha_secret_key_here"
NEXT_PUBLIC_DISABLE_RECAPTCHA="false"
# Human verification
NEXT_PUBLIC_TURNSTILE_SITE_KEY="your_turnstile_site_key_here"
TURNSTILE_SECRET_KEY="your_turnstile_secret_key_here"

# ===========================================
# EMAIL SERVICES (Optional)
Expand Down
7 changes: 3 additions & 4 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,9 @@ NEXT_PUBLIC_THE_GAMES_DB_API_KEY="The-Games-DB-Public-API-KEY"
NEXT_PUBLIC_IGDB_CLIENT_ID="IGDB-Client-ID"
IGDB_CLIENT_KEY="IGDB-Client-Secret"

# reCAPTCHA v3
NEXT_PUBLIC_RECAPTCHA_SITE_KEY=site-key-here
RECAPTCHA_SECRET_KEY=secret-key-here
NEXT_PUBLIC_DISABLE_RECAPTCHA=false
# Cloudflare Turnstile
NEXT_PUBLIC_TURNSTILE_SITE_KEY=0x4XXXXXXXXXXXXXXXXXXXXX
TURNSTILE_SECRET_KEY=0x4XXXXXXXXXXXXXXXXXXXXXXXX-XXXXXXX

# Email Providers (For the future)
EMAIL_ENABLED=false
Expand Down
7 changes: 3 additions & 4 deletions .env.test.example
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,9 @@ DATABASE_DIRECT_URL="postgres://postgres:pooler.supabase.com:5432/postgres"
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY="pk_test_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
CLERK_SECRET_KEY="sk_test_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"

# reCAPTCHA V3
NEXT_PUBLIC_RECAPTCHA_SITE_KEY=
RECAPTCHA_SECRET_KEY=
NEXT_PUBLIC_DISABLE_RECAPTCHA=false
# Cloudflare Turnstile
NEXT_PUBLIC_TURNSTILE_SITE_KEY=
TURNSTILE_SECRET_KEY=

# Email (For the future)
EMAIL_ENABLED=false
Expand Down
123 changes: 123 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
# Project Guide

This file is the source of working guidance for AI coding agents in this repository.

## Commands

- Use `pnpm`, not `npm` or `npx`.
- Common checks:
- `pnpm lint`
- `pnpm types`
- `pnpm test`
- `pnpm check`
- Prisma:
- `pnpm db:generate` generates Prisma Client and TypedSQL.
- `pnpm prisma validate` validates the schema.
- Database-backed Prisma commands must use the project scripts that wrap `scripts/db-cmd.sh` when available.
- Do not run `pnpm dev`, `pnpm build`, `pnpm start`, `pnpm run deploy`, migrations, seeds, or data scripts unless the user explicitly asks.
- Never deploy, commit, or push unless the user explicitly asks.
- Use the current git user as commit author; never add Codex/AI authorship or AI-themed branch names.

## Domain Rules

- A Listing is a handheld compatibility report: game plus handheld device plus emulator.
- A PC Listing is a PC compatibility report: game plus PC hardware plus emulator.
- User-facing UI should say "Compatibility Report", "Handheld Report", "PC Report", or "PC Compatibility Report", not "listing".
- Listings and PC Listings must stay behaviorally aligned for voting, comments, moderation, trust effects, notifications, and approval flows.
- Shared cross-listing behavior belongs in utilities instead of duplicated implementations.

## Architecture

- Feature folders should follow the project-structure guidance from Bulletproof React:
https://github.com/alan2207/bulletproof-react/blob/master/docs/project-structure.md
- Within `src/features/*`, prefer scoped subdirectories such as `components`, `hooks`, `utils`, `server`, and `shared` instead of flat feature folders.
- Routers in `src/server/api/routers/` are thin orchestration layers. They handle auth context, schema-validated input, repository/service calls, and response formatting.
- Do not put raw Prisma queries or business logic in routers.
- Define Zod schemas in `src/schemas/*`; do not define inline schemas in router `.input(...)` calls.
- All database access belongs in repository classes under `src/server/repositories/` extending `BaseRepository`.
- Repositories should use project error helpers and consistent database operation handling.
- Multi-step business logic, external API orchestration, and complex calculations belong in services under `src/server/services/`.
- Use `AppError` and `ResourceError` helpers instead of raw `Error`, raw strings, or one-off `TRPCError` usage.
- Use specialized procedures such as `protectedProcedure`, `adminProcedure`, and `permissionProcedure(...)` instead of ad hoc permission checks.

## Database And Prisma

- Treat database changes as high risk.
- Do not run migrations, seeds, `db:push`, or data scripts without explicit user approval.
- Prisma migration commands may only target the local database configured by `.env.local`, never a hosted or Supabase database, unless the user explicitly instructs otherwise.
- Do not create or edit migration SQL manually. Use Prisma migration commands.
- Do not edit an existing migration after it has been created. Create a new migration when a schema change is required.
- Prefer `migrate deploy` for applying migrations. Never use `db:push` outside disposable local development.
- Prisma Client is generated to `prisma/generated/client`; app imports use `@orm`, `@orm/client`, and `@orm/sql`.
- Use `pnpm prisma ...` or project `pnpm db:*` scripts, not `npx prisma`.

## TypeScript And Code Quality

- Do not use `any` or `z.any()`. Use concrete types, discriminated unions, or `unknown` with narrowing.
- Do not use `@ts-ignore`, `@ts-expect-error`, or `eslint-disable` comments.
- Do not use casts to hide type problems. Fix the underlying type issue.
- Handle null and undefined explicitly.
- Use generated Prisma types where appropriate.
- Do not add unused functions, exports, or speculative helpers.
- Remove dead code when refactoring.
- Prefer function declarations for top-level functions/components.
- Component props interfaces should be named `Props`.
- Do not destructure component props in function parameters; use `props.foo`.
- Keep `useEffect` dependencies correct.
- Comments should be rare, factual, and explain only non-obvious external constraints or business invariants.

## Enums

- Import enum values from `@orm`; do not use string literals for enum values in application code.
- This applies to UI state, filters, schemas, comparisons, routers, services, and repositories.
- Prefer `z.nativeEnum(SomeEnum)` over hard-coded `z.enum([...])` when a Prisma enum exists.
- Tests may use string literals only when mocking requires it.

## UI

- Before adding custom UI markup, check existing components in `src/components/ui/`.
- Prefer extending shared UI components over duplicating badge, button, modal, table, loading, card, or form markup.
- Use `Button`, `Badge`, `Card`, table utilities, form components, `LoadingSpinner`, and dialog components from the shared UI library where applicable.
- Never use `window.confirm()`. Use `useConfirmDialog` from `@/components/ui`.
- Keep admin pages consistent: table controls, search/filtering, pagination, statistics, and bulk actions should follow existing admin patterns.

## Filters

- Controllers own filter behavior: interactions, analytics, collapsed badges, active summaries, and calls into presentational content.
- Content components should only render fields and call handlers passed by controllers.
- URL/state hooks own URL sync and local UI state; they must not emit per-filter analytics.
- Filter analytics should be emitted once from controllers, using `filterAnalytics` and `selectedLabels`.
- Call `onChange` before emitting analytics.
- Use shared filter UI pieces: `FilterSidebarShell`, `CollapsedBadges`, `ActiveFiltersSummary`, and `MobileFilterSheet`.
- Use shared option mappers from `src/utils/options.ts`.

## Async Multi-Selects

- Use `src/components/ui/form/async-multi-select/AsyncMultiSelect.tsx` as the base.
- Entity wrappers such as CPU, GPU, Device, and SoC selects should stay thin: call TRPC, map to `Option[]`, manage pagination state, and pass data to the base component.
- Selected chips must persist by deriving them from `options` plus `selectedByIds`.

## Security

- For write operations, never trust a user ID supplied by input when ownership matters. Use `ctx.session.user.id`.
- Pass `requestingUserRole` when admin override behavior is supported.
- Reads may accept user IDs for filtering, but writes must validate ownership or permission.
- Use transactions for multi-step writes that must stay consistent.
- Validate input at API boundaries with Zod schemas.

## Verification

- Run the smallest relevant checks first, then broader checks when risk warrants it.
- Before claiming a fix is complete, verify the actual failing behavior when possible.
- For API changes, exercise the endpoint or generated contract.
- For UI changes, run the relevant UI/test path.
- For Prisma changes, run schema validation and generation. Only run migration commands with explicit approval.
- If a check cannot be run, report the exact blocker.

<!-- BEGIN:nextjs-agent-rules -->

# Next.js: ALWAYS read docs before coding

Before any Next.js work, find and read the relevant doc in `node_modules/next/dist/docs/`. Your training data is outdated — the docs are the source of truth.

<!-- END:nextjs-agent-rules -->
2 changes: 1 addition & 1 deletion docs/DEVELOPMENT_SETUP.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ CLERK_SECRET_KEY=...
NEXT_PUBLIC_APP_URL=http://localhost:3000
```

External provider keys such as `RAWG_API_KEY`, `THE_GAMES_DB_API_KEY`, and reCAPTCHA keys are only needed for the features that call those services.
External provider keys such as `RAWG_API_KEY`, `THE_GAMES_DB_API_KEY`, and Cloudflare Turnstile keys are only needed for the features that call those services.

## Troubleshooting

Expand Down
20 changes: 10 additions & 10 deletions docs/MOBILE_API.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# EmuReady Mobile API (tRPC)

*Auto-generated on: 2026-02-17T20:21:36.450Z*
*Auto-generated on: 2026-05-25T13:13:26.417Z*

## Summary
- **Total Endpoints**: 112
Expand Down Expand Up @@ -710,15 +710,15 @@ Protected endpoints require Bearer token authentication using Clerk JWT.
#### 27. **get**
- **Method**: GET
- **Path**: `/preferences.get`
- **Description**: Get user preferences
- **Description**: get - preferences
- **Tags**: preferences

- **Authentication**: Bearer token required

#### 28. **update**
- **Method**: POST
- **Path**: `/preferences.update`
- **Description**: Update user preferences
- **Description**: update - preferences
- **Tags**: preferences
- **Request Body**: JSON object required
- **Content-Type**: application/json
Expand All @@ -727,7 +727,7 @@ Protected endpoints require Bearer token authentication using Clerk JWT.
#### 29. **addDevice**
- **Method**: POST
- **Path**: `/preferences.addDevice`
- **Description**: Add device preference
- **Description**: addDevice - preferences
- **Tags**: preferences
- **Request Body**: JSON object required
- **Content-Type**: application/json
Expand All @@ -736,7 +736,7 @@ Protected endpoints require Bearer token authentication using Clerk JWT.
#### 30. **removeDevice**
- **Method**: POST
- **Path**: `/preferences.removeDevice`
- **Description**: Remove device preference
- **Description**: removeDevice - preferences
- **Tags**: preferences
- **Request Body**: JSON object required
- **Content-Type**: application/json
Expand All @@ -745,7 +745,7 @@ Protected endpoints require Bearer token authentication using Clerk JWT.
#### 31. **bulkUpdateDevices**
- **Method**: POST
- **Path**: `/preferences.bulkUpdateDevices`
- **Description**: Bulk update device preferences
- **Description**: bulkUpdateDevices - preferences
- **Tags**: preferences
- **Request Body**: JSON object required
- **Content-Type**: application/json
Expand All @@ -754,7 +754,7 @@ Protected endpoints require Bearer token authentication using Clerk JWT.
#### 32. **bulkUpdateSocs**
- **Method**: POST
- **Path**: `/preferences.bulkUpdateSocs`
- **Description**: Bulk update SOC preferences
- **Description**: bulkUpdateSocs - preferences
- **Tags**: preferences
- **Request Body**: JSON object required
- **Content-Type**: application/json
Expand All @@ -763,23 +763,23 @@ Protected endpoints require Bearer token authentication using Clerk JWT.
#### 33. **currentProfile**
- **Method**: GET
- **Path**: `/preferences.currentProfile`
- **Description**: Get current user's profile
- **Description**: currentProfile - preferences
- **Tags**: preferences

- **Authentication**: Bearer token required

#### 34. **profile**
- **Method**: GET
- **Path**: `/preferences.profile`
- **Description**: Get user profile by ID
- **Description**: profile - preferences
- **Tags**: preferences

- **Authentication**: Bearer token required

#### 35. **updateProfile**
- **Method**: POST
- **Path**: `/preferences.updateProfile`
- **Description**: Update profile
- **Description**: updateProfile - preferences
- **Tags**: preferences
- **Request Body**: JSON object required
- **Content-Type**: application/json
Expand Down
16 changes: 16 additions & 0 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
@@ -1,9 +1,24 @@
import { existsSync, readdirSync } from 'node:fs'

import typescriptEslint from '@typescript-eslint/eslint-plugin'
import typescriptParser from '@typescript-eslint/parser'
import eslintConfigPrettier from 'eslint-config-prettier'
import nextCoreWebVitals from 'eslint-config-next/core-web-vitals'
import nextTypeScript from 'eslint-config-next/typescript'

const featureNames = existsSync('./src/features')
? readdirSync('./src/features', { withFileTypes: true })
.filter((entry) => entry.isDirectory())
.map((entry) => entry.name)
: []

const featureBoundaryZones = featureNames.map((featureName) => ({
target: `./src/features/${featureName}`,
from: './src/features',
except: [`./${featureName}`],
message: 'Features must not import from other features. Compose features at the route layer.',
}))

const eslintConfig = [
{
ignores: [
Expand Down Expand Up @@ -137,6 +152,7 @@ const eslintConfig = [
tsx: 'never',
},
],
'import/no-restricted-paths': ['error', { zones: featureBoundaryZones }],
},
}, // UI component import cycle override
{
Expand Down
15 changes: 0 additions & 15 deletions next.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,6 @@ import type { Configuration as WebpackConfiguration } from 'webpack'

const isVercelBuild = process.env.VERCEL === '1'

const recaptchaScriptSources = [
'https://www.google.com/recaptcha/',
'https://www.gstatic.com/recaptcha/',
]

const recaptchaConnectSources = ['https://www.google.com/recaptcha/']

const recaptchaFrameSources = [
'https://www.google.com/recaptcha/',
'https://recaptcha.google.com/recaptcha/',
]

const contentSecurityPolicyDirectives = [
{
name: 'default-src',
Expand All @@ -40,7 +28,6 @@ const contentSecurityPolicyDirectives = [
'https://storage.ko-fi.com',
'https://ko-fi.com',
'https://unpkg.com',
...recaptchaScriptSources,
],
},
{
Expand Down Expand Up @@ -117,7 +104,6 @@ const contentSecurityPolicyDirectives = [
'https://*.r2.cloudflarestorage.com',
'https://cdn.emuready.com',
'https://retrocatalog.com',
...recaptchaConnectSources,
],
},
{
Expand All @@ -132,7 +118,6 @@ const contentSecurityPolicyDirectives = [
'https://vercel.live',
'https://*.vercel.live',
'https://ko-fi.com',
...recaptchaFrameSources,
],
},
{
Expand Down
1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,6 @@
"react": "19.2.6",
"react-dom": "19.2.6",
"react-error-boundary": "^6.0.0",
"react-google-recaptcha-v3": "^1.11.0",
"react-hook-form": "^7.56.4",
"react-syntax-highlighter": "^15.6.6",
"remeda": "^2.22.1",
Expand Down
Loading
Loading