diff --git a/docs/superpowers/specs/2026-03-28-auth-react-frontend-design.md b/docs/superpowers/specs/2026-03-28-auth-react-frontend-design.md new file mode 100644 index 0000000..c06f0dc --- /dev/null +++ b/docs/superpowers/specs/2026-03-28-auth-react-frontend-design.md @@ -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 +- `` 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