docs: add design spec for auth + React frontend
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
201
docs/superpowers/specs/2026-03-28-auth-react-frontend-design.md
Normal file
201
docs/superpowers/specs/2026-03-28-auth-react-frontend-design.md
Normal file
@@ -0,0 +1,201 @@
|
||||
# Auth + React Frontend — Design Spec
|
||||
|
||||
## Context
|
||||
|
||||
Actorle is a Symfony 8.0 word game (Wordle-like for actors) running on FrankenPHP with PostgreSQL. The app currently has no authentication and uses Asset Mapper with Stimulus for a minimal frontend. This spec covers adding user authentication and migrating to a React-based interactive frontend via SymfonyUX.
|
||||
|
||||
## Approach
|
||||
|
||||
**Hybrid Twig + React** (SymfonyUX React option 2):
|
||||
- Symfony stays in control of routing, sessions, and page rendering via Twig
|
||||
- Interactive parts (the game grid) are React components mounted in Twig via `{{ react_component() }}`
|
||||
- Auth pages (login, register) remain pure Twig — no benefit from React here
|
||||
|
||||
## 1. Frontend Migration: Asset Mapper to Vite
|
||||
|
||||
### Remove Asset Mapper
|
||||
- Remove `symfony/asset-mapper` from composer
|
||||
- Delete `importmap.php`
|
||||
- Remove `asset_mapper.yaml` config
|
||||
|
||||
### Install Vite
|
||||
- Install `pentatrion/vite-bundle` (Symfony Vite integration)
|
||||
- Create `vite.config.js` at project root with `@vitejs/plugin-react`
|
||||
- Update `base.html.twig` to use Vite's `{{ vite_entry_link_tags('app') }}` and `{{ vite_entry_script_tags('app') }}` instead of `{{ importmap() }}`
|
||||
|
||||
### Install React
|
||||
- Install `symfony/ux-react`
|
||||
- npm dependencies: `react`, `react-dom`, `@vitejs/plugin-react`
|
||||
- The UX React bundle auto-registers a Stimulus controller that mounts React components
|
||||
|
||||
### Assets Structure
|
||||
```
|
||||
assets/
|
||||
├── app.js (entry point: imports styles + Stimulus bootstrap)
|
||||
├── bootstrap.js (Stimulus app initialization)
|
||||
├── styles/
|
||||
│ └── app.css
|
||||
├── controllers/ (Stimulus controllers)
|
||||
│ └── csrf_protection_controller.js
|
||||
└── react/
|
||||
└── controllers/ (React components mountable via {{ react_component() }})
|
||||
├── GameGrid.jsx
|
||||
├── GameRow.jsx
|
||||
├── LetterInput.jsx
|
||||
└── ActorPopover.jsx
|
||||
```
|
||||
|
||||
## 2. Docker Setup
|
||||
|
||||
### New service: `docker/node/Dockerfile`
|
||||
- Base image: `node:22-alpine`
|
||||
- Working directory: `/app`
|
||||
- Runs `npm install` and `npx vite` dev server on port 5173
|
||||
|
||||
### docker-compose.override.yaml (dev)
|
||||
Add `node` service:
|
||||
```yaml
|
||||
node:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: docker/node/Dockerfile
|
||||
volumes:
|
||||
- .:/app
|
||||
- node_modules:/app/node_modules
|
||||
ports:
|
||||
- "5173:5173"
|
||||
```
|
||||
|
||||
### Production build
|
||||
- The `docker/node/Dockerfile` has a `build` stage that runs `npm run build`
|
||||
- The `docker/app/Dockerfile` prod stage copies built assets from the node build stage via `COPY --from=`
|
||||
- No Node.js runtime in production — only the compiled static assets
|
||||
|
||||
## 3. Authentication
|
||||
|
||||
### User Entity
|
||||
- Fields: `id` (int, auto), `email` (string, unique), `password` (hashed), `roles` (json)
|
||||
- Implements `UserInterface` and `PasswordAuthenticatedUserInterface`
|
||||
- Doctrine repository: `UserRepository`
|
||||
- Database migration for the `user` table
|
||||
|
||||
### Security Configuration (`security.yaml`)
|
||||
```yaml
|
||||
security:
|
||||
password_hashers:
|
||||
Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: 'auto'
|
||||
|
||||
providers:
|
||||
app_user_provider:
|
||||
entity:
|
||||
class: App\Entity\User
|
||||
property: email
|
||||
|
||||
firewalls:
|
||||
main:
|
||||
lazy: true
|
||||
provider: app_user_provider
|
||||
form_login:
|
||||
login_path: app_login
|
||||
check_path: app_login
|
||||
default_target_path: /
|
||||
logout:
|
||||
path: app_logout
|
||||
|
||||
access_control:
|
||||
- { path: ^/login, roles: PUBLIC_ACCESS }
|
||||
- { path: ^/register, roles: PUBLIC_ACCESS }
|
||||
- { path: ^/, roles: ROLE_USER }
|
||||
```
|
||||
|
||||
### Controllers & Routes
|
||||
|
||||
**SecurityController:**
|
||||
- `GET /login` — renders login form (`templates/security/login.html.twig`)
|
||||
- `POST /login` — handled by Symfony's `form_login` authenticator
|
||||
- `GET /logout` — handled by Symfony's logout handler
|
||||
|
||||
**RegistrationController:**
|
||||
- `GET /register` — renders registration form (`templates/security/register.html.twig`)
|
||||
- `POST /register` — validates form, hashes password, persists User, redirects to `/login`
|
||||
|
||||
### Templates
|
||||
- `templates/security/login.html.twig` — email + password fields, CSRF token, error display, link to register
|
||||
- `templates/security/register.html.twig` — email + password + confirm password fields, CSRF token, validation errors, link to login
|
||||
- Both extend `base.html.twig`
|
||||
- Pure Twig, no React
|
||||
|
||||
## 4. Game Grid React Components
|
||||
|
||||
### Data Flow
|
||||
1. `HomepageController::index()` prepares grid data (actors, letters, target) as it does today
|
||||
2. Data is passed as JSON props to the React component in Twig:
|
||||
```twig
|
||||
{{ react_component('GameGrid', { grid: gridData, targetActorId: targetId }) }}
|
||||
```
|
||||
3. React hydrates client-side and manages all interactivity
|
||||
|
||||
### Components
|
||||
|
||||
**`GameGrid.jsx`** — Root component
|
||||
- Receives `grid` (array of rows) and `targetActorId` as props
|
||||
- Renders a list of `GameRow` components
|
||||
- Manages global game state (which letters have been guessed)
|
||||
|
||||
**`GameRow.jsx`** — One row = one actor
|
||||
- Receives row data (actor name, highlighted letter index)
|
||||
- Renders a sequence of `LetterInput` components
|
||||
- Contains a button that triggers `ActorPopover`
|
||||
|
||||
**`LetterInput.jsx`** — Single character input
|
||||
- `<input maxLength={1}>` styled to look like a game tile
|
||||
- On `keyup`: if a character was typed, move focus to the next `LetterInput`
|
||||
- On `Backspace`: clear and move focus to previous input
|
||||
- Highlighted letter has distinct styling (red background, as current CSS)
|
||||
|
||||
**`ActorPopover.jsx`** — Info popover
|
||||
- Uses `@floating-ui/react` for positioning
|
||||
- Triggered by a button click on the row
|
||||
- Displays actor name, movie info, or hints
|
||||
- Dismissible by clicking outside or pressing Escape
|
||||
|
||||
### Popover Library
|
||||
- `@floating-ui/react` — modern, lightweight, React-native successor to Popper.js
|
||||
|
||||
## 5. What Does NOT Change
|
||||
- `HomepageController` — same game logic, adapted only to pass data as props to React instead of Twig variables (listed in Modified Files below)
|
||||
- Entities: `Actor`, `Movie`, `MovieRole` — untouched
|
||||
- PostgreSQL database and existing migrations — untouched (new migration only adds `user` table)
|
||||
- FrankenPHP server — untouched
|
||||
- Makefile — extended with node/npm targets, existing targets unchanged
|
||||
|
||||
## 6. File Changes Summary
|
||||
|
||||
### New Files
|
||||
- `docker/node/Dockerfile`
|
||||
- `vite.config.js`
|
||||
- `package.json` (replaces importmap-based setup)
|
||||
- `src/Entity/User.php`
|
||||
- `src/Repository/UserRepository.php`
|
||||
- `src/Controller/SecurityController.php`
|
||||
- `src/Controller/RegistrationController.php`
|
||||
- `src/Form/RegistrationType.php`
|
||||
- `templates/security/login.html.twig`
|
||||
- `templates/security/register.html.twig`
|
||||
- `assets/react/controllers/GameGrid.jsx`
|
||||
- `assets/react/controllers/GameRow.jsx`
|
||||
- `assets/react/controllers/LetterInput.jsx`
|
||||
- `assets/react/controllers/ActorPopover.jsx`
|
||||
- Migration file for `user` table
|
||||
|
||||
### Modified Files
|
||||
- `composer.json` — remove asset-mapper, add vite-bundle + ux-react
|
||||
- `config/packages/security.yaml` — full auth config
|
||||
- `templates/base.html.twig` — Vite tags instead of importmap
|
||||
- `templates/homepage/index.html.twig` — replace HTML grid with `{{ react_component() }}`
|
||||
- `docker-compose.override.yaml` — add node service
|
||||
- `docker-compose.yaml` — add node build step for prod
|
||||
- `docker/app/Dockerfile` — prod stage copies built JS assets
|
||||
- `assets/app.js` — entry point adjustments for Vite
|
||||
- `src/Controller/HomepageController.php` — pass grid data as JSON props to React component
|
||||
- `Makefile` — add npm/node targets
|
||||
Reference in New Issue
Block a user