A production-ready lineup builder for Premier League Big Six teams with drag & drop interface
- Overview
- System Architecture
- Data Design
- UI/UX Structure
- API Endpoints
- Features
- Roadmap
- Tech Stack
- Design Decisions
Premier League Lineup Builder is a tactical lineup management system that allows users to:
- Select from Big Six Premier League teams
- Build custom lineups using drag & drop
- Manage starting XI and bench players
- Save and share lineups
- Switch between different formations
β
UI-First - No complex ratings or AI scoring
β
Clean Data - JSON-based player roster
β
Production-Ready - Scalable and maintainable
β
User-Friendly - Intuitive drag & drop interface
User
βββ Lineup
βββ Team (Big Six - Read-only)
β βββ players (JSON Array)
β
βββ Pitch Slots (11 players)
βββ Bench Slots (7 substitutes)
Login β Select Team β Create/Select Lineup β Build Formation β Save
model User {
id String @id @default(cuid())
email String @unique
password String
name String
lineups Lineup[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model Team {
id String @id @default(cuid())
name String
logo String
players Json // Array of player objects
createdAt DateTime @default(now())
lineups Lineup[]
}
model Lineup {
id String @id @default(cuid())
name String
formation String
userId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
TeamId String
Team Team @relation(fields: [TeamId], references: [id])
slots LineupSlot[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model LineupSlot {
id String @id @default(cuid())
lineupId String
lineup Lineup @relation(fields: [lineupId], references: [id], onDelete: Cascade)
position String // GK, LCB, RCM, ST, etc.
x Float // Position on pitch (0-1)
y Float // Position on pitch (0-1)
playerId String? // Reference to player in JSON
playerName String?
playerImage String?
@@unique([lineupId, position])
}{
"id": "salah",
"name": "Mohamed Salah",
"number": 11,
"primaryPosition": "RW",
"secondaryPositions": ["ST"],
"imageKey": "players/liverpool/salah.png"
}- Manchester City
- Arsenal
- Liverpool
- Manchester United
- Chelsea
- Tottenham Hotspur
/login
β
/register
β
/select-team
β
/lineups (List of user's lineups)
β
/lineups/:id (Lineup Builder)
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Player List (Sidebar) β
β β’ Search by name β
β β’ Filter by position β
β β’ Draggable player cards β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Football Pitch β
β β
β GK β
β β
β LB LCB RCB RB β
β β
β LCM CM RCM β
β β
β LW ST RW β
β β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Bench (7 substitutes) β
β Sub1 | Sub2 | Sub3 | Sub4 | Sub5 | Sub6 | 7β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
| Component | Description |
|---|---|
AuthProvider |
Global authentication context |
ProtectedRoute |
Route guard for authenticated users |
TeamSelectPage |
Choose from Big Six teams |
LineupListPage |
View all saved lineups |
LineupBuilder |
Main formation builder |
FormationSelector |
Switch between formations |
Pitch |
Football field with slot positions |
PitchSlot |
Drop zone for players |
PlayerSidebar |
Searchable player list |
PlayerCard |
Draggable player element |
BenchPanel |
Substitute management |
| Method | Endpoint | Description |
|---|---|---|
| POST | /api/auth/register |
Create new user |
| POST | /api/auth/login |
Login user |
| POST | /api/auth/logout |
Logout user |
| GET | /api/auth/me |
Get current user |
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/premier-teams |
List all Big Six teams |
| GET | /api/premier-teams/:id |
Get team with players |
| GET | /api/formations |
List available formations |
| Method | Endpoint | Description |
|---|---|---|
| POST | /api/lineups |
Create new lineup |
| GET | /api/lineups |
Get user's lineups |
| GET | /api/lineups/:id |
Get specific lineup |
| PUT | /api/lineups/:id |
Update lineup |
| DELETE | /api/lineups/:id |
Delete lineup |
| Method | Endpoint | Description |
|---|---|---|
| PUT | /api/lineups/:id/slots |
Update all slots (full state) |
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/images/:key |
Serve player/team images |
- User authentication (JWT + bcrypt)
- Big Six team selection
- Lineup CRUD operations
- Drag & drop pitch interface
- Formation presets (4-3-3, 4-2-3-1, 3-5-2)
- Save lineup functionality
- Bench/substitute system
- Player swap animation
- Search & filter players
- Real-time formation switching
- Validation feedback
- Share lineup (public link)
- Duplicate lineup
- Add more leagues
- Mobile responsive design
- Light AI suggestions (optional)
- Export as image
- Framework: Next.js 14+ (App Router)
- Language: TypeScript
- Styling: Tailwind CSS
- Drag & Drop: react-dnd / dnd-kit
- State: React Context / Zustand
- Forms: React Hook Form + Zod
- Runtime: Node.js
- Database: PostgreSQL (Neon)
- ORM: Prisma 5.10.2
- Auth: JWT + HttpOnly Cookies
- Password: bcrypt
- Storage: Backend file storage (local/S3/Supabase)
- Deployment: Vercel / Railway
- Database: Neon PostgreSQL
Decision: Store players in JSON field instead of separate table
Reasons:
- Players are read-only master data
- Fixed roster per team (no user modifications)
- Reduces database joins
- Better performance for drag & drop UI
- Simpler seed data management
Trade-off: Less normalized but more practical for this use case
Decision: Create dedicated table for pitch/bench slots
Reasons:
- Unified abstraction for pitch and bench
- Easy swap/animation logic
- Clean validation rules
- Scalable for future features
- Clear slot positioning (x, y coordinates)
Alternative Considered: Inline JSON slots in Lineup table
Why Rejected: Harder to query, validate, and update individual slots
Decision: Store images in backend storage with metadata table
Reasons:
- Decouple storage provider (local β S3 β CDN)
- Enable caching and CDN integration
- Better security and access control
- Production-grade approach
- Easy migration between storage solutions
Alternative Considered: Store paths directly in JSON
Why Rejected: Hardcoded paths, no flexibility, harder to manage at scale
Decision: Implement JWT + HttpOnly Cookie authentication
Reasons:
- Full control over auth flow
- Simpler for MVP scope
- No external dependencies
- Educational value (demonstrates auth implementation)
- Easy to migrate to NextAuth/Clerk later
Trade-off: More code to maintain but better learning experience
- Set up Next.js + Prisma
- Implement authentication
- Seed Big Six teams data
- Build lineup CRUD
- Create drag & drop pitch
- Basic save functionality
Deliverable: Working prototype for portfolio
- Implement bench system
- Add swap animations
- Search and filter players
- Formation switching
- Validation messages
- Responsive design (desktop)
Deliverable: Production-quality product
- Public lineup sharing
- Lineup duplication
- Add more leagues/teams
- Mobile optimization
- Optional AI suggestions
- Export as image/PDF
Deliverable: SaaS-ready platform
- Node.js 18+
- PostgreSQL database
- npm/yarn/pnpm
# Clone repository
git clone <repo-url>
cd lineup-builder
# Install dependencies
npm install
# Set up environment variables
cp .env.example .env
# Edit .env with your DATABASE_URL
# Run Prisma migrations
npx prisma db push
npx prisma generate
# Seed database
npm run seed
# Start development server
npm run devDATABASE_URL="postgresql://user:password@host:5432/database"
JWT_SECRET="your-secret-key"
NEXT_PUBLIC_API_URL="http://localhost:3000"- β Each player can only be in one location (pitch or bench)
- β Exactly 1 goalkeeper on pitch
- β Position slots must match player primary position
- β Player ID must exist in team's JSON roster
- β User can only edit their own lineups
- β Maximum 7 substitutes on bench
- β Sidebar β Pitch (adds player)
- β Pitch β Bench (removes from pitch)
- β Bench β Pitch (adds to pitch, removes from bench)
- β Pitch β Pitch (swap positions)
- β Invalid drop β snap back animation
"Players are master data that never change per session. They're read-only roster information, similar to how a sports API would return team data. Using JSON reduces unnecessary joins and makes the UI faster. For this use case, it's the right trade-off between normalization and performance."
"The architecture is designed for extension: separate image storage for CDN integration, formation presets as data for easy expansion, slot abstraction that works for any formation, and clean API boundaries. To scale, we'd add caching, implement CDN for images, and potentially move to microservices for user-generated content vs. master data."
"The slot-based architecture makes this straightforward. We'd add WebSocket connections, implement optimistic updates, and use conflict resolution for simultaneous edits. The state is already centralized in the database, so adding real-time sync is a natural extension."
MIT License - Feel free to use for portfolio projects
This is a portfolio/educational project. Feel free to fork and customize for your own use!
Built with β€οΈ for learning and demonstration purposes