Operator feedback after the first refinement (PR #109): "It's hard to see which one you're hovering" + asked for a fun mouse-tracking radial gradient AND a subtle gradient on the active state. Three targeted refinements in one PR. 1. Active state: subtle ember linear-gradient added. - Kept the 1px ember border + ember text from PR #109's first refinement (operator approved the toned-down read). - Added a 135deg linear-gradient at 14% → 6% alpha (light) / 18% → 8% alpha (dark) so the active item has visible warmth without returning to the heavy saturated pill from sub-convoy #2. Background shorthand replaces the prior transparent value. 2. Hover state: base tint bolder. - rgba(216, 67, 21, 0.08) → rgba(216, 67, 21, 0.18) on light (per operator: "hard to see which one you're hovering") - rgba(255, 138, 80, 0.10) → rgba(255, 138, 80, 0.22) on dark - Also expanded the selector list to include .nav-item-hover consistently with .nav-item and .nav-item-bottom (the three hoverable nav surface classes). 3. Hover state: mouse-tracking radial spotlight overlay. - New ::before pseudo-element on .nav-item / .nav-item-bottom / .nav-item-hover. Uses a radial-gradient positioned via two CSS custom properties --mouse-x and --mouse-y (default 50%). - Light theme: rgba(255, 140, 30, 0.35) center → rgba(216, 67, 21, 0.18) at 35% → transparent at 70%. 120px radius. - Dark theme: rgba(255, 160, 70, 0.40) center → rgba(255, 110, 0, 0.22) → transparent. Brighter for visibility against navy. - Opacity transitions in/out via --motion-duration-quick. - pointer-events: none so it never blocks clicks or focus. - Content z-index: 1 so icons + labels paint above the gradient. 4. Layout.js: single delegated mousemove listener at document level. - Cheaper than per-item React onMouseMove handlers (the NavigationContent function has 10+ different nav-item render paths). closest() returns null for non-nav events so the listener cost is negligible. - Writes --mouse-x / --mouse-y as percentages onto the closest matching nav element. CSS consumes those vars to position the radial-gradient via inheritance through the ::before. - prefers-reduced-motion: respected by the existing global 0.01ms transition sweep — the spotlight opacity fade is collapsed but the static gradient position update is a positional change (not an animation), so it stays per WCAG SC 2.3.3 without special-casing. Tests: - npm run test:run: 113/113 - npm run lint: clean (1 pre-existing unused-disable warning) - npm run build: green Co-authored-by: Cursor <cursoragent@cursor.com> |
||
|---|---|---|
| .convoys | ||
| .cursor | ||
| .github | ||
| components | ||
| docs | ||
| lib | ||
| migrations | ||
| pages | ||
| public | ||
| scripts | ||
| styles | ||
| test | ||
| tests | ||
| .agent-context-manifest.yml | ||
| .gitignore | ||
| .npmrc | ||
| AGENTS.md | ||
| eslint.config.mjs | ||
| next.config.js | ||
| package-lock.json | ||
| package.json | ||
| playwright.config.js | ||
| postcss.config.js | ||
| README.md | ||
| tailwind.config.js | ||
| TESTING_GUIDE.md | ||
| vercel.json | ||
| vitest.config.js | ||
Deck Hearth
A modern trading card game collection manager built with Next.js and Neon Database.
🚀 Features
- Card Management: Track your MTG, Pokémon, and Lorcana cards
- Collection Organization: Create and manage card collections
- Deck Building: Build and share decks
- Authentication: Secure user accounts with JWT
- Admin Panel: Manage cards and users
- Real-time Pricing: Track card values
🛠️ Tech Stack
- Frontend: Next.js 16 (Pages router), React 18, JavaScript (TypeScript is a devDep only — see
AGENTS.mdGotcha #9) - Backend: Next.js API Routes
- Database: Neon PostgreSQL (serverless)
- Authentication: JWT with bcrypt (24-hour expiry;
lib/auth-secret.jsis the single source of truth forJWT_SECRET) - Rate limiting:
@upstash/ratelimiton/api/auth/login+/api/auth/register(5 attempts / 15 min per IP) - Testing: Vitest (unit); Playwright queued
- Styling: Tailwind CSS
- Deployment: Vercel
📦 Installation
-
Clone the repository
git clone <repository-url> cd tcg-vault -
Install dependencies
npm install -
Set up environment variables
cp .env.example .env.localUpdate
.env.localwith your Neon database URL and a real JWT secret:POSTGRES_URL="postgresql://your-username:your-password@your-host/your-database" JWT_SECRET="<generate with: openssl rand -hex 32>" # Required for `npm run setup-db` — used once to hash the initial admin password. # Set in .env.local for local dev, or as a CI secret if you run setup from CI. ADMIN_INITIAL_PASSWORD="<generate with: openssl rand -base64 24>" # Optional — exercise the rate limiter locally. Without them, `lib/rate-limit.js` # warn-and-no-ops in dev. In production these are auto-provisioned by the # Vercel Upstash Marketplace integration. KV_REST_API_URL="https://<your-upstash-host>.upstash.io" KV_REST_API_TOKEN="<your-upstash-rest-token>"JWT_SECRETis required —lib/auth-secret.jsthrows at import time if it's unset.ADMIN_INITIAL_PASSWORDis required fornpm run setup-db— the script exits with code 1 if it's unset. -
Set up the database
npm run setup-dbThis applies every pending migration under
migrations/(vianode-pg-migrate) and then seeds the initial admin user. To run just the migration step without seeding, usenpm run migrate up. -
Start development server
npm run dev
🧱 Schema changes (post-migration-tool convoy)
Schema is now managed by node-pg-migrate. New migrations live under
migrations/ at the repo root (the legacy scripts/migrations/
placeholder is preserved for the one pre-existing dated script and is
not used going forward).
# 1. Generate a new migration file (JS template, timestamp-prefixed)
npm run migrate create add-foo-column -- -j js
# 2. Edit the generated file under migrations/<timestamp>_add-foo-column.js
# Put your DDL in up(); write a real down() if rollback is safe.
# 3. Apply it locally (POSTGRES_URL from .env.local)
npm run migrate up
# 4. Commit the migration file + any docs/SCHEMA_MAP.md updates together.
Onboarding a new env is now exactly npm install → seed .env.local
→ npm run setup-db (which chains npm run migrate up and then
seeds the admin user).
Do not edit historical
scripts/add-*.js/scripts/fix-*.js/scripts/seed-*.js— those are already-run, append-only jobs. They are preserved for the audit trail; new column / constraint work goes through a migration file instead.
🗄️ Database Schema
The application uses the following tables:
users- User accounts and authenticationcards- Card information and metadatauser_cards- User's card collectionscollections- Named card collectionscollection_cards- Cards in collectionsdecks- Deck definitionsdeck_cards- Cards in decks
🔧 API Endpoints
Authentication
POST /api/auth/register- User registrationPOST /api/auth/login- User login
Admin
GET /api/admin- Admin panel data
Health Check
GET /api/health- Application health
Note: Earlier revisions of this README also listed
GET /api/test-db(and three other unauthenticated dev endpoints:/api/simple,/api/test-auth,/api/setup-database). All four were deleted infix-auth-bypassBrief 3 (commitfc0dd73) and CI now blocks their reintroduction. Don't recreate them.
🚀 Deployment
This app is configured for deployment on Vercel:
- Connect your repository to Vercel
- Set environment variables in Vercel dashboard
- Deploy automatically on push to main branch
📁 Project Structure
tcg-vault/
├── pages/ # Next.js pages and API routes
│ ├── api/ # API endpoints
│ │ ├── auth/ # Authentication routes
│ │ └── admin/ # Admin routes
│ ├── _app.js # App wrapper
│ └── index.js # Home page
├── lib/ # Utility libraries
│ └── database.js # Database adapter
├── scripts/ # Database setup scripts
├── public/ # Static assets
└── .env.local # Environment variables
🔐 First-time admin setup
npm run setup-db first applies every pending migration (via npm run migrate up), then creates a single admin user. The password is read
from the ADMIN_INITIAL_PASSWORD environment variable; the script
exits with code 1 (and does not open a database connection) if the
variable is unset or empty.
- Local dev: set
ADMIN_INITIAL_PASSWORDin.env.localbefore runningnpm run setup-db. Useopenssl rand -base64 24(or any other strong source) to generate the value. - CI / Vercel: set
ADMIN_INITIAL_PASSWORDas a project secret if setup ever runs from CI. The env var is only read by the seed script; runtime auth uses the per-user password stored in the database. - Admin email: the seed creates
admin@deckhearth.com. Change the password immediately after first login via the app's profile settings.
Operators of envs that pre-date this change:
npm run setup-dbis idempotent (ON CONFLICT (email) DO NOTHING) — re-running it withADMIN_INITIAL_PASSWORDset will not rotate an existing admin row's password. If your environment was set up before this change and still has the prior weak default, rotate the password manually via the app after logging in, or wait for the queuedrotate-default-adminfollow-up convoy.
Operators of envs that pre-date the
pick-a-nameconvoy (2026-05-24): the admin row was renamed fromadmin@tcgvault.comtoadmin@deckhearth.com. Runnode scripts/migrations/2026-05-24-rename-admin-email.jsonce after deploy to UPDATE any existing@tcgvault.comuser rows (the admin row, plus alice/bob ifnpm run create-test-userswas ever run). Re-running the migration after the first run is idempotent and prints "Nothing to migrate." Verify post-migration withpsql $POSTGRES_URL -c "SELECT email FROM users WHERE email LIKE '%@tcgvault.com'"— expect zero rows.
🤝 Contributing
- Fork the repository
- Create a feature branch
- Make your changes
- Submit a pull request
📄 License
MIT License - see LICENSE file for details