# Lote UI — Login, jogar, modais, ranking e resultado (2026-05-19)

Documento de referência do segundo lote visual/UX desta data: **login com branding**, **tela de jogar**, **modal de erro**, **resultado com pódio/ranking** e **ranking na home** refinado. Complementa [portal-questionario-ui-game.md](./portal-questionario-ui-game.md) e [deploy-producao-questionario-experiencia.md](./deploy-producao-questionario-experiencia.md).

**Última atualização:** 2026-05-19.

---

## 1. Resumo do produto

| Área | O que mudou |
|------|-------------|
| **Login** (`/login`) | Logo e favicon do cadastro; fundo com **gradiente game** (cores do questionário) quando há um único tenant; botão Entrar com cor primária do portal; logo maior no mobile/desktop. |
| **Jogar** | Pergunta e alternativas em **dois blocos**; barra de progresso + “Pergunta X de Y”; ícone de relógio ao lado do cronómetro; botão **Confirmar resposta** no padrão 3D (`qg-btn-iniciar`). |
| **Acerto** | Modal existente (“Parabéns!”) + partículas; avanço só ao clicar no botão. |
| **Erro** | **Novo** modal “Não foi desta vez” (estilo vermelho), no mesmo fluxo do acerto; substitui shake + redirect automático. |
| **Resultado** (`/portal/questionario/experiencia/resultado`) | **Remove** lista pergunta a pergunta; mostra **resumo** (acertos, pontos, tempo) + **pódio e ranking top 10** da bateria + botão 3D “Voltar à listagem”. |
| **Home portal** | Ranking com visual **game** (pódio, espaçamento, fundo escuro no palco); CSS em `battery-ranking.css`. |

---

## 2. Login — branding

### Comportamento

| Cenário | Tema aplicado |
|---------|----------------|
| `/login/{slug}` | Aparência do portal (`portal_appearance_settings`) da empresa do slug. |
| `/login` (sem slug) | Se existir **exatamente um** registo de aparência → mesma empresa (single-tenant). |
| Sem empresa identificada | Cores padrão do sistema. |

### Fundo e logo

- **Fundo:** gradiente radial com cores do **questionário game** (`questionnaire_game_settings` ou defaults `--qg-bg-*`), não só `page_background_color` do portal (que costuma ser `#f1f5f9` claro).
- **Triângulos wireframe:** mesmo componente da splash (`<x-questionnaire.splash-wire-corner />` nos cantos tl/tr/br), CSS em `guest-auth-wires.css`, quando há branding de empresa no login.
- **Logo:** prioridade logo do questionário game; senão logo do portal; senão fallback FUEGO.
- **Tamanho da logo:** até `max-h-40` (desktop) / `max-h-32` (mobile); no mobile com branding game, animação (`guest-auth__logo-wrap--animated`).
- **Slogan:** frase do splash (`QuestionnaireGameTheme::splashTaglineHtml()`), classe `guest-auth__tagline`, fonte Cinzel — ver [lote-portal-avatar-login-dashboard-2026-05-22.md](./lote-portal-avatar-login-dashboard-2026-05-22.md).

### Backend

| Artefato | Função |
|----------|--------|
| `PortalAppearanceSettingService::themeForGuestLogin(?string $slug)` | Resolve `PortalAppearanceTheme` para telas guest. |
| `PortalAppearanceSettingRepository::findUniquePortalAppearanceOrNull()` | Um único registo de aparência (instalação típica). |
| `SharePortalBranding` | Partilha `portalTheme`, `questionnaireGameTheme`, `guestLoginHasCompanyBranding` em views web. |
| `AuthenticatedSessionController::create()` | Passa `portalTheme` via `themeForGuestLogin` (não envia `null`). |

### Ficheiros

- `resources/views/layouts/guest.blade.php`
- `app/Services/PortalAppearanceSettingService.php`
- `app/Http/Controllers/Auth/AuthenticatedSessionController.php`
- `app/Http/Middleware/SharePortalBranding.php`
- `app/Repositories/Contracts/PortalAppearanceSettingRepositoryInterface.php`
- `app/Repositories/Eloquent/PortalAppearanceSettingRepository.php`
- `resources/views/auth/login.blade.php` — botão `.guest-auth-primary`
- `tests/Feature/Auth/AuthenticationTest.php` — `test_login_uses_portal_appearance_when_single_tenant`

---

## 3. Tela de jogar (`experience-play`)

### Layout

- **HUD:** título da bateria, Pausar/Sair, cronómetro com **ícone SVG de relógio** (`qg-timer-wrap`).
- **Progresso:** texto “Pergunta **N** de **M**” + **barra** (`qg-play__progress-bar` / `qg-play__progress-fill`), largura = `N/M × 100%`.
- **Pergunta:** card `.qg-qcard--prompt` (stem + tags).
- **Alternativas:** card `.qg-qcard--choices` separado.
- **Confirmar:** `qg-btn-iniciar` dentro de `qg-splash__btn-wrap` (mesmo padrão da splash).

### Modais de feedback

| Modal | Ficheiro | Quando |
|-------|----------|--------|
| Acerto | `_experience-success-modal.blade.php` | `is_correct === true` (AJAX) |
| Erro | `_experience-error-modal.blade.php` | `is_correct === false` |

Ambos bloqueiam o palco (`.qg-play__stage--answered`), exigem clique em **Avançar** / **Continuar** / **Ver resultado** para seguir.

### JavaScript (`experience-play` @push scripts)

- `showSuccessModal(points)` / `showErrorModal(completed)`
- `lockStageForFeedback()` — blur no palco
- Removido: `setTimeout(navigateNext, 900)` no erro

### CSS relevante

- `questionnaire-game.css` — progresso, timer, modais `.qg-miss` / `.qg-error`, mobile `@media (max-width: 767px)`
- Import: `game-start-button.css`, `battery-ranking.css` (só necessário se outra view game usar ranking no mesmo bundle)

---

## 4. Tela de resultado (`experience-result`)

### Porquê mudou

Com baterias de **30+ perguntas**, a lista “pergunta a pergunta” torna-se ilegível. O participante precisa de **resumo + posição no ranking**, alinhado à home do portal.

### Conteúdo actual

1. **Hero** (`.qg-result__hero`): “Missão cumprida!”, nome da bateria, estatísticas:
   - Acertos `X/Y` (%)
   - Pontos conquistados
   - Tempo total (se &gt; 0)
2. **Ranking** — `<x-portal.battery-ranking variant="game" :show-cta="false" />` com título “Pódio da bateria”.
3. **Ação** — link `qg-btn-iniciar` → `portal.questionnaire.index` (“Voltar à listagem”).

### Backend

```php
$rankingPayload = $this->batteryRankingService->topTenForBattery($user, $battery);
// view: rankingEntries => $rankingPayload['entries']
```

| Método | Classe |
|--------|--------|
| `topTenForBattery(User, QuestionBattery)` | `PortalBatteryRankingService` |
| `topTenForUserActiveBattery(User)` | Reutiliza `topTenForBattery` após resolver bateria activa |

Query: `QuestionnaireParticipantSessionRepository::topActiveRankingForBattery` (top 10 por bateria + **grupo** do participante; ver [correcao-ranking-portal-escopo-2026-05-27.md](./correcao-ranking-portal-escopo-2026-05-27.md)).

### Teste

`QuestionnaireParticipantSessionTest::test_completing_all_questions_redirects_to_result` — asserte “Missão cumprida”, “Pódio da bateria”, **não** vê “Resposta correta”.

---

## 5. Ranking partilhado (`battery-ranking`)

### Componente

`resources/views/components/portal/battery-ranking.blade.php`

| Prop | Default | Uso |
|------|---------|-----|
| `battery` | — | Bateria |
| `entries` | `[]` | Linhas do ranking (rank, name, points, is_current_user, …) |
| `variant` | `portal` | `portal` \| `game` |
| `showCta` | `true` | Botão “Ir ao questionário” no header |
| `headerEyebrow` | Top 10 participantes | Texto superior |
| `headerTitle` | Ranking da bateria | Título |

Classes CSS: `.qrank` (portal) ou `.qrank.qrank--game` (ecrã cheio).

### CSS extraído

Ficheiro **`resources/css/battery-ranking.css`** — pódio, lista, header, variantes.

Importado por:

- `resources/css/portal-questionnaire-hub.css` (home portal)
- `resources/css/questionnaire-game.css` (resultado experiência)

Variante **game** (`.qrank--game`): usa variáveis `--qg-*`, lista com fundo escuro semântico.

### Onde aparece

| Ecrã | Rota | variant | showCta |
|------|------|---------|---------|
| Home portal | `/portal` | portal | true |
| Resultado experiência | `/portal/questionario/experiencia/resultado` | game | false |

`portal/home.blade.php` inclui `@vite('resources/css/portal-questionnaire-hub.css')`.

---

## 6. Inventário de ficheiros

### Criados

| Ficheiro | Descrição |
|----------|-----------|
| `resources/views/portal/questionnaire/_experience-error-modal.blade.php` | Modal de resposta incorrecta |
| `resources/css/battery-ranking.css` | Estilos do pódio/lista (portal + game) |

### Alterados (principais)

| Ficheiro | Descrição |
|----------|-----------|
| `resources/views/layouts/guest.blade.php` | Login com logo, gradiente game, botão temático |
| `resources/views/layouts/questionnaire-game.blade.php` | Font Awesome (ícone relógio na lista ranking) |
| `resources/views/portal/questionnaire/experience-play.blade.php` | Progresso, timer, botão 3D, modais, JS erro |
| `resources/views/portal/questionnaire/experience-result.blade.php` | Resumo + ranking + voltar |
| `resources/views/components/portal/battery-ranking.blade.php` | Refactor `qrank`, props variant/cta |
| `resources/views/portal/home.blade.php` | Vite hub CSS |
| `resources/css/questionnaire-game.css` | Progresso, timer, modais erro, resultado hero, imports |
| `resources/css/portal-questionnaire-hub.css` | Import `battery-ranking.css` (ranking removido inline) |
| `app/Services/PortalAppearanceSettingService.php` | `themeForGuestLogin`, `themeFromModel` |
| `app/Services/PortalBatteryRankingService.php` | `topTenForBattery` |
| `app/Http/Controllers/Portal/PortalQuestionnaireController.php` | Ranking no resultado |
| `app/Http/Controllers/Auth/AuthenticatedSessionController.php` | Tema login |
| `app/Http/Middleware/SharePortalBranding.php` | QG theme + flag guest |
| `app/Repositories/.../PortalAppearanceSettingRepository.php` | `findUniquePortalAppearanceOrNull` |
| `tests/Feature/Auth/AuthenticationTest.php` | Login branding |
| `tests/Feature/Portal/QuestionnaireParticipantSessionTest.php` | Resultado + erro JSON |

---

## 7. Verificação antes do commit

```bash
php artisan test
php artisan test --filter=AuthenticationTest
php artisan test --filter=QuestionnaireParticipantSessionTest
php artisan test --filter=PortalHomeRankingTest
npm ci && npm run build
```

### QA manual

1. **Login** — `/login`: logo grande, fundo azul escuro (com 1 empresa configurada); favicon e título do projeto.
2. **Jogar** — barra de progresso, relógio no HUD, confirmar 3D.
3. **Acertar** — modal verde + partículas; Avançar.
4. **Errar** — modal vermelho; Continuar (não salta sozinho).
5. **Resultado** — resumo + pódio + lista; sem lista de 30 perguntas; Voltar à listagem.
6. **Home** — ranking com palco escuro e espaçamento.

---

## 8. Mensagem de commit sugerida (este lote)

```
feat(portal): login com branding, modal de erro e resultado com ranking

- Login usa logo/cores do cadastro (gradiente game em single-tenant)
- Jogar: barra de progresso, ícone do cronómetro, botão confirmar 3D
- Modal de erro ao falhar; palco bloqueado até Continuar/Avançar
- Resultado: resumo (acertos, pontos, tempo) + pódio/top 10 em vez de lista por pergunta
- CSS battery-ranking.css partilhado entre portal home e experiência game
- Ranking home com visual game (espaçamento e palco)
```

---

## 9. Manutenção

- Ajustar modal de erro: `_experience-error-modal.blade.php` + secção `.qg-error` em `questionnaire-game.css`.
- Ajustar resumo do resultado: `.qg-result__*` em `questionnaire-game.css`.
- Ajustar pódio: `battery-ranking.css` (afecta home e resultado).
- Login só com cor do portal (sem gradiente game): alterar lógica em `guest.blade.php` (`$guestUsesGameBackdrop`) ou `page_background_color` na aba Portal.
