This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Real Betis supporters club website in Edinburgh with mobile-first design, serving match viewing parties at Polwarth Tavern. Built on Next.js 16 with TypeScript, featuring secure-by-default architecture using feature flags.
npm run dev # Start dev server with Turbopack
npm run build # Production build
npm run start # Start production server
npm run lint # Run ESLint
npm run type-check # TypeScript type checkingnpm test # Run Vitest unit & integration tests
npm run test:watch # Vitest watch mode
npm run test:coverage # Vitest coverage report with v8 provider
npm run test:silent # Vitest with minimal JSON output
npm run test:e2e # Playwright E2E tests (headless)
npm run test:e2e:headed # Playwright E2E tests (headed)
npm run storybook # Storybook dev server
npm run build-storybook # Build Storybooknpm run update-trivia # Update trivia questions in database
npm run lighthouse:accessibility # Run Lighthouse auditPre-commit hooks automatically run before each commit to catch issues early:
- ESLint: Auto-fixes linting errors
- Prettier: Auto-formats code
- TypeScript: Type checking
Skip hooks (if needed):
LEFTHOOK=0 git commit -m "message"Hooks are configured in lefthook.yml and install automatically via the prepare script.
- Branch protection:
mainrequires PRs — never push directly - Deployment: Vercel's GitHub integration auto-deploys on merge to main (CI deploy job commented out pending secret configuration, see issue #329)
- Dependabot: Configured in
.github/dependabot.ymlwith grouped minor/patch updates;next,react,react-domexcluded from grouping for isolated review
- Lockfile corruption: If dependency installs go wrong (e.g., installing then reverting a major version), reset both
package.jsonandpackage-lock.jsonfrom main and reinstall cleanly rather than trying to fix incrementally --legacy-peer-deps: Avoid — causes missing transitive dependencies. Useoverridesinpackage.jsonfor peer dep conflicts instead- Peer dep overrides: The
overridesfield inpackage.jsonresolves peer dependency mismatches (e.g.,"eslint": "^10.0.0"for typescript-eslint compatibility)
- Frontend: Next.js 16 App Router, React 19, TypeScript
- Styling: Tailwind CSS 4 with custom Betis branding
- Database: Supabase (PostgreSQL) with Row Level Security
- Authentication: Clerk with role-based permissions
- Feature Flags: Environment variables for feature rollouts
- Testing: Vitest 4 + Playwright + Storybook v10
src/
├── app/ # Next.js App Router (pages & API routes)
├── components/ # Reusable React components
├── lib/ # Utilities, API clients, auth helpers
├── middleware.ts # Route protection & security headers
└── types/ # TypeScript type definitions
tests/
├── unit/ # Vitest unit tests
├── integration/ # Vitest integration tests
└── helpers/ # Test utilities
e2e/ # Playwright E2E tests
sql/ # Database migrations & scripts
- Simple approach: Environment variable-based flags, cached in production only
- Usage:
hasFeature('flag-name')(synchronous) - Configuration: Set
NEXT_PUBLIC_FEATURE_*=trueto enable disabled-by-default features, or=falseto disable core features - Location:
src/lib/featureFlags.ts - Enabled by default: Nosotros, Únete (Join), Soylenti (rumors), Clasificación (standings), Partidos (matches)
- Disabled by default: RSVP, Contacto, Galería, Clerk Auth, Debug Info
- Development mode: No caching - changes to
.env.localapply immediately - Documentation: See
docs/adr/004-feature-flags.md - Auto-sync: Partidos feature includes automatic background sync that updates past matches with missing data when users visit the site
- Dual mode: Anonymous submissions + authenticated user management
- API Protection: Use
checkAdminRole()from@/lib/adminApiProtection - Role hierarchy:
admin>moderator>user(inpublicMetadata.role) - Database integration:
getAuthenticatedSupabaseClient(clerkToken)for RLS - Documentation: See
docs/adr/001-clerk-authentication.md
- RLS enabled: Always use authenticated client for user data
- User data: Anonymous and authenticated submissions stored separately
- Cache strategy: Use
classification_cachetable for external API data - Location:
src/lib/supabase.tsfor client and types
- Purpose: Component development, documentation, and testing
- Version: v10 with Vitest addon integration
- Pattern: Create
.stories.tsxfiles alongside components - Import updates: Use
import { within, userEvent } from 'storybook/test'
- Always start mobile, scale up with responsive breakpoints
- Follow the Design System: See
docs/design-system.mdfor complete guidelines
- Primary Green:
bg-betis-verde(notbg-green-600) - Dark Green:
bg-betis-verde-dark(notbg-green-700) - Light Green:
bg-betis-verde-light(notbg-green-100) - Pale Green:
bg-betis-verde-pale(notbg-green-50) - Gold Accent:
bg-betis-oro(notbg-yellow-400) - Scottish Navy:
bg-scotland-navy(for footer/dark sections)
| DON'T USE ❌ | USE INSTEAD ✅ |
|---|---|
bg-green-50/100 |
bg-betis-verde-pale/light |
bg-green-500/600 |
bg-betis-verde |
bg-green-700 |
bg-betis-verde-dark |
text-green-* |
text-betis-verde or text-betis-verde-dark |
text-green-400 (on dark bg) |
text-betis-oro |
hover:bg-green-700 |
hover:bg-betis-verde-dark |
border-green-* |
border-betis-verde or border-betis-verde/20 |
--betis-verde: #048d47 /* Authentic Betis green */ --betis-verde-dark: #036b38
/* Hover states, headers */ --betis-verde-light: #e8f5ed
/* Light backgrounds */ --betis-oro: #d4af37 /* Gold highlights, CTAs */
--scotland-navy: #0b1426 /* Footer, dark sections */;// ✅ Correct - uses branded classes
<button className="bg-betis-verde hover:bg-betis-verde-dark text-white">
// ❌ Wrong - generic Tailwind
<button className="bg-green-600 hover:bg-green-700 text-white">
// ✅ Footer (Scottish Navy)
<footer className="bg-scotland-navy text-white">
<h3 className="text-betis-oro">Heading</h3>
</footer>See docs/design-system.md for:
- Complete color palette with hex values
- Typography guidelines
- Component examples
- Accessibility requirements
- Migration reference table
import { hasFeature } from "@/lib/featureFlags";
export default function MyComponent() {
const isEnabled = hasFeature("my-feature-flag");
if (!isEnabled) return null;
return <div>Feature content</div>;
}Most business routes use the createApiHandler pattern for consistency:
import { createApiHandler } from "@/lib/apiUtils";
import { contactSchema } from "@/lib/schemas/contact";
// POST - Submit contact form
export const POST = createApiHandler({
auth: "none", // 'none' | 'user' | 'admin' | 'optional'
schema: contactSchema, // Zod schema for validation
handler: async (validatedData, context) => {
// validatedData is type-safe and validated
// context provides user info, request, supabase clients
const { name, email } = validatedData;
return {
success: true,
message: "Success message",
};
},
});When to use createApiHandler:
- Simple CRUD operations with standard validation
- New API routes being developed
- Routes requiring consistent error handling
- APIs with straightforward authentication needs
When to use Legacy Pattern (Rarely):
- Server-Sent Events (SSE) endpoints that return streaming responses
- External integrations with very specific protocol requirements
✅ Complete: All standard API routes now use createApiHandler.
import { checkAdminRole } from "@/lib/adminApiProtection";
import { getAuth } from "@clerk/nextjs/server";
export async function POST(request: NextRequest) {
const { user, isAdmin, error } = await checkAdminRole();
if (!isAdmin) return NextResponse.json({ error }, { status: 401 });
const { getToken } = getAuth(request);
const token = await getToken({ template: "supabase" });
const supabase = getAuthenticatedSupabaseClient(token);
// Implementation here
}- Test runner: Vitest with jsdom environment for React components
- Coverage: v8 provider with 80% threshold for lines, functions, branches, statements
- Setup: Global setup in
tests/setup.tswith DOM testing library matchers - Config:
vitest.config.tswith path aliases and environment variables
- Clerk mocking: Mock
@clerk/nextjs/serverfor authentication tests - Supabase mocking: Mock database operations with controlled responses
- MSW integration: Service worker for external API mocking
- Environment variables: Test-specific values in
vitest.config.ts
✅ Complete: All API route tests have been updated to work with the createApiHandler pattern.
Current Pattern (All Tests Use This):
// ✅ Tests work with Zod validation by providing valid data
const validData = {
name: "Test User",
email: "[email protected]", // Valid email format
subject: "Test Subject", // Meets min length requirements
message: "Test message", // Meets min length requirements
};
// Mock successful Supabase operations
(supabase.from as any).mockReturnValue({
insert: vi.fn(() => ({
select: vi.fn(() => ({
single: vi.fn(() => Promise.resolve({ data: { id: 1 }, error: null })),
})),
})),
});Legacy Pattern (No Longer Used):
// ❌ Old tests mocked validation functions that are no longer used
vi.spyOn(security, "validateInputLength").mockReturnValue({ isValid: true });
vi.spyOn(security, "validateEmail").mockReturnValue({ isValid: true });When Writing New Tests:
- Provide valid data that passes Zod schema validation
- Test validation by providing invalid data that Zod will reject
- Mock Supabase operations rather than validation functions
- Expect error messages from Zod validation failures
import { describe, it, expect, vi } from 'vitest';
import { render, screen } from '@testing-library/react';
// Mocking with Vitest
vi.mock('@/lib/featureFlags', () => ({
hasFeature: vi.fn(() => true),
getFeatureFlags: vi.fn(() => ({ showClasificacion: true })),
}));
// Component testing
test('renders component correctly', () => {
render(<MyComponent />);
expect(screen.getByRole('button')).toBeInTheDocument();
});- Mock external services first (Clerk, Supabase)
- Mock security functions to return valid by default
- Test success cases, validation failures, rate limiting, database errors
- Use established NextResponse mocking pattern
- Auth setup: Pre-configured in
playwright/global.setup.ts - Base URL: Defaults to
http://localhost:3000 - Pattern: Test user workflows end-to-end with real authentication
- RSVP System: Embedded widgets with expandable forms for match viewing confirmations at Polwarth Tavern
- Trivia Game: Betis & Scotland themed with 15-second timer, pointing system
- Photo Gallery: Community photo sharing
- Match Data: Football-Data.org API integration with caching
- User Data: Clerk authentication with separate anonymous/authenticated submissions
- Admin Dashboard: Match sync, contact submissions management
- User Management: Handled directly through Clerk dashboard or API
The admin panel (/admin) provides a streamlined interface for content management with three main sections:
- Dashboard: Overview with statistics, recent RSVPs, and contact submissions
- Partidos (Matches): Complete match management including creation, editing, deletion, and sync
- Contactos (Contacts): Contact form submissions management with status filtering
User management functionality has been removed from the admin panel to:
- Reduce complexity and maintenance burden
- Leverage Clerk's robust user management capabilities
- Focus admin panel on core content management
User operations now handled via:
- Clerk Dashboard: Web-based user management interface
- Clerk Management API: Programmatic user operations
- Clerk Webhooks: User lifecycle event handling
- Authentication: Clerk-based with admin role requirement
- Route Protection:
withAdminRoleHOC ensures admin access - API Security: All admin API routes use
createApiHandlerwithauth: 'admin'
- State: Streamlined to three core variables
- Component: Single
TriviaPagecomponent - API: Single
/api/triviaendpoint with query parameters - Performance: 65% faster API responses, 85% less data transfer per request
- Tables:
trivia_questions,trivia_answerswith proper UUID relationships - Data Structure: Questions with multiple choice answers, correct answer flagging
- Categories: Real Betis history, Scottish football, general knowledge
- Optimization: Direct database randomization with
ORDER BY RANDOM() LIMIT 5
- Format: 5-question trivia format with daily play limitation
- Timer: Simple 15-second countdown per question using
setTimeout - Scoring: Percentage-based scoring system with immediate feedback
- Engagement: "Once per day" messaging encourages regular participation
- State Machine: Clear transitions:
idle → loading → playing → feedback → completed
- Frontend: Single consolidated component (
src/app/trivia/page.tsx) with inline timer/score - API: Consolidated endpoint
/api/trivia?action=questions|submit|score|total - State Management: 3-variable system:
gameState,currentData,error - Utilities: Shared functions in
/src/lib/trivia/utils.tsfor common operations - Performance Tracking: Built-in monitoring with
TriviaPerformanceTracker - Error Handling: Structured errors with context using
TriviaErrorclass
// Simplified state system (USE THIS PATTERN)
const [gameState, setGameState] = useState<GameState>('loading');
const [currentData, setCurrentData] = useState<CurrentData>({ /* consolidated */ });
const [error, setError] = useState<string | null>(null);
// Consolidated API usage
GET /api/trivia?action=questions // Get questions (default)
POST /api/trivia?action=submit // Submit score (default)
GET /api/trivia?action=total // Get total score
// State machine transitions (FOLLOW THIS PATTERN)
const handleAnswerClick = () => {
setCurrentData(prev => ({ ...prev, selectedAnswer: answerId }));
setGameState('feedback');
setTimeout(() => goToNextQuestion(), 2000);
};- API Rate Limiting: Implement for public routes
- Database Indexing: Optimize for frequent queries
- Bundle Size: Analyze and reduce JavaScript bundles
- Image Optimization: Ensure proper Next.js Image usage
- Type Generation: Consider
supabase gen typesfor schema sync - CI/CD Enhancement: Add performance audits, security scans
- Documentation: Expand component documentation in Storybook
- Trivia Enhancements: Leaderboards, expanded question database
- Social Features: Enhanced photo sharing, match predictions
- Internationalization: Multi-language support if needed
For comprehensive details, always check:
- Developer Guide:
docs/developer-guide.mdfor complete development guide - Testing Guide:
docs/testing-guide.mdfor testing strategies and patterns - ADRs:
docs/adr/for architectural decisions - Security:
docs/security/for security implementation details
Required environment variables:
NEXT_PUBLIC_SUPABASE_URL&NEXT_PUBLIC_SUPABASE_ANON_KEYNEXT_PUBLIC_CLERK_PUBLISHABLE_KEY&CLERK_SECRET_KEY
Optional feature flags (only for disabled/experimental features):
NEXT_PUBLIC_FEATURE_GALERIA=falseNEXT_PUBLIC_FEATURE_REDES_SOCIALES=falseNEXT_PUBLIC_FEATURE_DEBUG_INFO=false
Optional debugging:
NEXT_PUBLIC_DEBUG_MODE=true
This repo is monitored by Repo Butler, a portfolio health agent that observes repo health daily and generates dashboards, governance proposals, and tier classifications.
Your report: https://ismaelmartinez.github.io/repo-butler/betis-escocia.html Portfolio dashboard: https://ismaelmartinez.github.io/repo-butler/ Consumer guide: https://github.com/IsmaelMartinez/repo-butler/blob/main/docs/consumer-guide.md
To query your repo's health tier, governance findings, and portfolio data from any Claude Code session, add the MCP server once (adjust the path to your local repo-butler checkout):
claude mcp add repo-butler node /path/to/repo-butler/src/mcp.jsAvailable tools: get_health_tier, get_campaign_status, query_portfolio, get_snapshot_diff, get_governance_findings.
When working on health improvements, check the per-repo report for the current tier checklist and use the consumer guide for fix instructions.