# TV Câmara — Views e UX

> **Escopo:** módulo `tv-camara` na `v6`. Blades do portal (Bootstrap 4) e do
> CMS (AdminLTE 1.22), JS de listagem e o mecanismo de embed de vídeo.

---

## 1. Portal público

### 1.1 Mapa de arquivos

O portal resolve a view dinamicamente pelo tema ativo
(`Configuration` com `key = 'layout'`, lido em
`v6/app/Http/Controllers/Admin/Controller.php:39-49`):

```php
view('portal.layouts.' . $this->getTemplateActived() . '.tv-camara.index')
view('portal.layouts.' . $this->getTemplateActived() . '.tv-camara.show')
```

| Tema | Listagem | Detalhe | Bloco da home |
|---|---|---|---|
| `modelo1` | `v6/resources/views/portal/layouts/modelo1/tv-camara/index.blade.php` | `v6/resources/views/portal/layouts/modelo1/tv-camara/show.blade.php` | `v6/resources/views/portal/layouts/modelo1/tv-camara/index-home.blade.php` |
| `modelo2` | `v6/resources/views/portal/layouts/modelo2/tv-camara/index.blade.php` | `v6/resources/views/portal/layouts/modelo2/tv-camara/show.blade.php` | `v6/resources/views/portal/layouts/modelo2/tv-camara/index-home.blade.php` |
| `modelo3` | `v6/resources/views/portal/layouts/modelo3/tv-camara/index.blade.php` | `v6/resources/views/portal/layouts/modelo3/tv-camara/show.blade.php` | `v6/resources/views/portal/layouts/modelo3/tv-camara/index-home.blade.php` |
| `modelo4` | `v6/resources/views/portal/layouts/modelo4/tv-camara/index.blade.php` | `v6/resources/views/portal/layouts/modelo4/tv-camara/show.blade.php` | `v6/resources/views/portal/layouts/modelo4/tv-camara/index-home.blade.php` |
| `modelo5` | `v6/resources/views/portal/layouts/modelo5/tv-camara/index.blade.php` | `v6/resources/views/portal/layouts/modelo5/tv-camara/show.blade.php` | `v6/resources/views/portal/layouts/modelo5/tv-camara/index-home.blade.php` |
| `modelo6` | `v6/resources/views/portal/layouts/modelo6/tv-camara/index.blade.php` | `v6/resources/views/portal/layouts/modelo6/tv-camara/show.blade.php` | `v6/resources/views/portal/layouts/modelo6/tv-camara/index-home.blade.php` |

As seis variantes são **cópias** do mesmo markup com o `@extends` e os
`Breadcrumbs` trocados (verificado por `diff`); só o `modelo1` traz o bloco
comentado de WebP na imagem de destaque. A pasta `default` **não** possui
views de TV Câmara: se o layout ativo for `default`, o controller aborta com 404
(`view_inexistente`) ao checar `portal.layouts.default.home.index`.

### 1.2 Listagem (`index.blade.php`)

Estrutura (idêntica nos 6 temas):

- `@extends('portal.layouts.modelo1.layout.layout')` (varia por tema).
- Breadcrumb: `Breadcrumbs::view('portal.layouts.{tema}.layout.breadcrumb','tv-camara')`.
- Injeta `App\Prefecture\Configuration` e lê a chave `cor_1` (a cor é lida mas
  não usada no markup).
- Para cada item da página (`$contents`, 12 por página):
  - `<a href="{{route('portal.tv-camara.show', $content->slug)}}" target="_blank">`.
  - Thumbnail: `background: url('{{url($content->archives[0]->path)}}')`; quando
    não há arquivo, usa `url("/storage/img/exemplos/sem-imagem.png")`.
  - Data: `{{date('d.m.Y', strtotime($content->publication_date))}}` em badge.
  - Título: `{{str_limit($content->title, 50)}}` com `title` completo no atributo.
- Paginação: `{{ $contents->links() }}` (paginator do Laravel).
- Estado vazio: "Nenhuma registro encontrado." (gramática do original).

Observação de N+1: o controller **não** usa `with('archives')`; a view acessa
`$content->archives[0]`, gerando uma query por item (até 12 por página).

### 1.3 Detalhe (`show.blade.php`)

Ordem de renderização:

1. Título em `<p class="h4 text-uppercase txt-color-2 font-or">`.
2. Linha de crédito:
   `@if(!empty($content->source)) Do(a) ... @endif @if(!empty($content->author)) por ... @endif`
   — **`source` e `author` não existem na tabela `tv_camaras`**; portanto essa
   linha nunca mostra autor/fonte. O que aparece é apenas
   "Publicado em dd/mm/aaaa às HH:MM".
3. `addthis_inline_share_toolbox` (botões de compartilhamento AddThis).
4. Imagem de destaque: `$content->archives->where('highlight', 1)->first()->path`,
   renderizada com `url($data_webp)` e `data-webp='{{url($data_webp)}}.webp'`.
5. Texto: `{!! $content->text !!}` — **HTML não sanitizado**.
6. Player: `@if(isset($content->link) && $content->link) @include("portal.statics.video") @endif`.
7. Coluna lateral com o include do sidebar do tema.

### 1.4 Bloco da home (`index-home.blade.php`)

Condições de exibição:

```blade
@if(isset($modulos['tv-camara']) && $modulos['tv-camara'])
    @php $has_tv = isset($tvCamaras) && count($tvCamaras) != 0 @endphp
    @if ($has_tv)
```

- `$modulos` é injetado por `v6/app/Providers/ModulosServiceProvider.php:22-50`
  (`Cache::rememberForever('modulos')` com `Modulos::all()->pluck('status','slug')`).
  Como o seed cria o módulo com `status = 0`, **o bloco não aparece** até o
  operador habilitá-lo em `/cms/modulos` (que executa `Cache::pull('modulos')`).
- `$tvCamaras` vem de `v6/app/Http/Controllers/Portal/PortalController.php:161-163`:
  `TvCamara::with('archives')->where('status','true')->orderBy('publication_date','DESC')->limit(3)->get()`.
  Ou seja, **no máximo 3 vídeos**, ordenados por data de publicação.
  Esta query **não** filtra `publication_date <= now()`.
- Cada card mostra a miniatura via
  `getMinFile($tv->present()->getArchiveContentHighlightOrThumbnail(1, 'min'))`
  (`v6/app/Helpers/functions.php:977` e
  `v6/app/Http/Presenters/ContentPresenter.php:89`), o título truncado em 50
  caracteres e a data em `d.m.Y`.
- Link "Veja mais..." para `route('portal.tv-camara.index')`.

Onde o bloco é incluído:

| Arquivo | Linha |
|---|---|
| `v6/resources/views/portal/layouts/modelo1/home/elementos.blade.php` | 85 |
| `v6/resources/views/portal/layouts/modelo3/home/elementos.blade.php` | 74 |
| `v6/resources/views/portal/layouts/modelo5/home/news/category-news/news.blade.php` | 26 |
| `v6/resources/views/portal/layouts/modelo6/home/index.blade.php` | 47 |

Os temas `modelo2` e `modelo4` possuem o partial `index-home.blade.php`, mas
**não o incluem** em suas homes.

## 2. O embed de vídeo

### 2.1 Partial do player

`v6/resources/views/portal/statics/video.blade.php` (arquivo completo):

```blade
<div class=" card p-3 my-4">
    <div class="embed-responsive embed-responsive-16by9">
        <iframe class="embed-responsive-item iframe-video w-100" src="{{ $linkVideo }}" allowfullscreen></iframe>
    </div>
</div>
```

A variável `$linkVideo` é entregue pelo controller
(`v6/app/Http/Controllers/Portal/TvCamaraController.php:37`):

```php
$linkVideo = isset($content->link) ? embedVideo($content->link) : null;
```

### 2.2 Regras do helper `embedVideo()`

Fonte: `v6/app/Helpers/functions.php:607-665`.

| Etapa | Regra |
|---|---|
| Normalização do esquema | Se a URL não começa com `http://` ou `https://`, prefixa `https://`. |
| YouTube | Se a URL contém `youtube.com` ou `youtu.be`, extrai o ID de 11 caracteres pelo regex `%(?:youtube(?:-nocookie)?\.com/(?:[^/]+/.+/|(?:v|e(?:mbed)?)/|.*[?&]v=)|youtu\.be/)([^"&?/ ]{11})%i` e devolve `https://www.youtube.com/embed/{id}`. Se o regex não casar, devolve a URL original. |
| Vimeo | Se a URL contém `vimeo`, extrai o ID numérico (6 a 12 dígitos) e devolve `https://player.vimeo.com/video/{id}`; sem match, devolve a URL original. |
| Facebook | Se a URL contém `facebook.com`, devolve `https://www.facebook.com/plugins/video.php?href={urlencode}&show_text=false`. |
| Instagram | Se a URL contém `instagram.com`, remove query/fragmento e acrescenta `/embed/` (mantendo `/embed` ou `/amp` se já presentes). |
| Fallback | Qualquer outra URL é devolvida como veio (após normalização do esquema). |

### 2.3 O que o cidadão vê em cada caso

| Link cadastrado | URL do iframe | Resultado |
|---|---|---|
| `https://www.youtube.com/watch?v=YbJOTdZBX1g` | `https://www.youtube.com/embed/YbJOTdZBX1g` | Reproduz. |
| `https://youtu.be/YbJOTdZBX1g` | `https://www.youtube.com/embed/YbJOTdZBX1g` | Reproduz. |
| `https://www.youtube.com/embed/YbJOTdZBX1g` | `https://www.youtube.com/embed/YbJOTdZBX1g` | Reproduz. |
| `https://vimeo.com/123456789` | `https://player.vimeo.com/video/123456789` | Reproduz. |
| `https://www.facebook.com/watch/?v=123` | `https://www.facebook.com/plugins/video.php?href=...&show_text=false` | Reproduz via plugin. |
| `https://www.instagram.com/reel/ABC123/?x=1` | `https://www.instagram.com/reel/ABC123/embed/` | Reproduz. |
| `YbJOTdZBX1g` (ID cru) | `https://YbJOTdZBX1g` | **Não reproduz**: o ID puro não casa no regex do YouTube e não contém `youtube.com`, caindo no fallback. |
| `https://exemplo.com/video.mp4` | `https://exemplo.com/video.mp4` | Não reproduz: o iframe não é um player de vídeo nativo para mp4. |

**Contradição documentada**: o placeholder do campo "Link do Vídeo" sugere
`https://www.youtube.com/watch?v=YbJOTdZBX1g ou YbJOTdZBX1g`
(`v6/resources/views/prefecture/contents/tv-camara/form.blade.php:65`), ou seja,
o ID cru é aceito pelo formulário — mas **não** funciona no embed. Já a
miniatura da home **sabe** tratar o ID cru: o presenter tem um segundo regex
`/^[a-zA-Z0-9_-]{11}$/` em `getYoutubeThumbnail()`
(`v6/app/Http/Presenters/ContentPresenter.php:117-129`). Logo, um ID cru
mostra miniatura na home, mas o player fica em branco na página de detalhes.

### 2.4 Miniatura (imagem de destaque) e fallback automático

`ContentPresenter::getArchiveContentHighlightOrThumbnail()` escolhe, nesta ordem:

1. `archives` com `highlight = true`; se o arquivo existir em disco, devolve
   `'/' . $collection->path`.
2. Sem arquivo: se o `link` for de YouTube, devolve
   `https://img.youtube.com/vi/{id}/mqdefault.jpg` (ou `maxresdefault` sem
   `min`); se for de Vimeo, troca `vimeo.com/` por `thumbnail.com/` e
   acrescenta `.jpg`.
3. Sem nenhuma das anteriores: `storage/img/exemplos/sem-imagem.png`.

## 3. CMS (AdminLTE)

### 3.1 Arquivos

| Arquivo | Função |
|---|---|
| `v6/resources/views/prefecture/contents/tv-camara/index.blade.php` | Tela "Lista de TV Câmara": cabeçalho com botões, tabela `#tabela_tv-camara` (colunas ID, Título, Data, Status, Ações) e scripts. |
| `v6/resources/views/prefecture/contents/tv-camara/create.blade.php` | Formulário de cadastro (`Form::open` com `files => true`, rota `tv-camara.store`). |
| `v6/resources/views/prefecture/contents/tv-camara/edit.blade.php` | Formulário de edição (`Form::model`, rota `tv-camara.update`). |
| `v6/resources/views/prefecture/contents/tv-camara/form.blade.php` | Corpo compartilhado do formulário. |

### 3.2 Campos do formulário

| Campo | Componente | Requisitos na UI | Observação |
|---|---|---|---|
| Título | `components.inputs.input` | obrigatório, `maxlen 120` | — |
| Data de publicação | `components.inputs.date` (`input-date-hour`, máscara `br-data-tempo`, formato `d/m/Y H:i`) | obrigatório | O controller converte `d/m/Y H:i` para `Y-m-d H:i` com `str_replace('/','-', ...)`. |
| Imagem Principal | `components.inputs.image` | opcional | Input de arquivo `name="archive_highlight"`, `accept="image/*"`, aviso de 5 MB. |
| Texto | `components.inputs.textarea` (Summernote) | marcado como obrigatório | Sem validação no Form Request. |
| Link do Vídeo | `components.inputs.input` | obrigatório, `maxlen 2999` | Placeholder sugere URL do YouTube ou ID cru. |
| Exibir na página inicial | `components.inputs.checkbox` | padrão `true` | O rótulo é impreciso: o `status` controla a exibição na **listagem** `/tv-camara`, e a home mostra até 3 itens. |

### 3.3 Listagem (DataTable)

| Item | Valor |
|---|---|
| ID da tabela | `tabela_tv-camara` |
| Fonte | `/cms/datatable/tv-camara` (view SQL `tv_camara`) |
| Colunas JS | `titulo`, `data_publicacao`, `status_name` + coluna de ações |
| Coluna de ações | `createActiveButton(data.status, data.id, 'item', data.active, data.inactivate, 'tv-camara/set-status')` e `createBTN(data.id, 'tv-camara/{id}/edit', data.edit, data.delete, 'tv-camara/delete/{id}')` |
| Exclusão em lote | `prefecture.components.deleteByLoteSubmitButton` com a rota `ajax.tv-camara.deleteByLote` + `prefecture.components.deleteByLoteMasterCheckbox` |
| Exportação | Botões padrão do `runDatatables` (`v6/public/assets/js/custom/utilities/table.js:29-45` e `:123`): Excel, PDF e CSV, com `columns: ':visible:not(.column-actions)'` |

Arquivo JS: `v6/public/assets/js/custom/contents/tv-camara/datatable.js`.

```js
runDatatables('tv-camara', collums, true, 'ASC', null, true);
```

O último argumento (`true`) habilita a coluna de exclusão em lote.

### 3.4 Download/visualização da imagem de destaque

- **Visualização no CMS**: o componente `v6/resources/views/components/inputs/image.blade.php`
  mostra a miniatura atual (`url($destaque->path)`), com os botões "Selecionar
  imagem", "Editar" (cropper) e "Remover Imagem".
- **Remoção**: `deleteHighlightArchive(archive_id)` faz POST em
  `ajax.delete.files` (`v6/app/Http/Controllers/Prefecture/ContentController.php:577-582`),
  que faz **soft delete** do registro em `archives`. O arquivo físico não é
  removido do storage.
- **Download**: o widget `jquery.uploadfile` montado no `edit.blade.php` chama
  `ajax.download.files` (`ContentController@ajaxDownloadFiles`), mas o widget
  está vinculado a `#multipleupload`, elemento que **não existe** no formulário da
  TV Câmara — o código é código morto herdado do módulo de notícias.
- **Armazenamento**: os arquivos ficam em `storage/tv-camara/{id}/destaque/{nome}`
  e são gravados com `storeAs("tv-camara/{id}/destaque", $name)`. O `path`
  gravado em `archives` já inclui o prefixo `storage/`, o que faz o `url()`
  resolver para `/storage/tv-camara/...`.

## 4. JavaScript e Ajax

| Recurso | Caminho | Função |
|---|---|---|
| Listagem | `v6/public/assets/js/custom/contents/tv-camara/datatable.js` | Colunas e ações do DataTable. |
| Utilitários de tabela | `v6/public/assets/js/custom/utilities/table.js` | `runDatatables`, `createBTN`, `createActiveButton`, exportação. |
| Exclusão em lote | `v6/public/assets/js/custom/utilities/deleteAll.js` | `submitDeleteAll(route)` → POST com `toDelete[]`. |
| Editor de imagem | `v6/public/assets/js/custom/utilities/helpers/select-image.js`, `v6/public/assets/js/custom/utilities/helpers/editor-de-imagem.js` | Seleção/crop da imagem de destaque. |
| Summernote | `v6/public/assets/js/custom/utilities/richtext/summernote.js` | Edição do texto. |

Não há polling, SSE nem WebSocket no módulo.

## 5. Acessibilidade e SEO (estado atual)

| Aspecto | Situação |
|---|---|
| `alt` da imagem de destaque | Preenchido com o título (`alt="{{$content->title}}"`). |
| Alt da miniatura da home | `alt="Imagem {{ $tv->title }}"`. |
| Player | `iframe` sem `title` nem `aria-label`. |
| Metatags por vídeo | Não há `og:video`, `og:image` nem JSON-LD. |
| Descrição (meta description) | Não há campo nem tag. |
| Tradução | Todo o texto está fixo em português no blade (sem `trans()`), exceto os rótulos de botão do AdminLTE. |
