Commit 72556b0da726f41b4d0bcc15c19e4e4179c28848
1 parent
09cef093
Exists in
main
URLs amigáveis e redesign visual completo
- .htaccess: esconde a extensão .php (RewriteRule mapeando /rota pro arquivo .php correspondente, redirect 301 de .php -> sem extensão), bloqueia acesso a dotfiles e nega acesso via URL a src/config/storage/sql - Todos os links, forms e redirects internos atualizados pras novas rotas - CSS reescrito do zero: tema claro/escuro automático (prefers-color-scheme), tipografia Inter, cards com sombra e cantos arredondados, botões com gradiente, badges e alerts mais refinados - Tabelas ficam responsivas: em telas estreitas (<720px) cada linha vira um cartão empilhado em vez de rolar horizontalmente - Login com subtítulo e leve animação de entrada Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Showing
13 changed files
with
391 additions
and
81 deletions
Show diff stats
README.md
| @@ -48,10 +48,17 @@ e resetar senha, com controle de acesso gerenciado pela própria aplicação. | @@ -48,10 +48,17 @@ e resetar senha, com controle de acesso gerenciado pela própria aplicação. | ||
| 48 | - `sql/schema.sql` — schema do banco (admins, settings, audit_log, login_attempts). | 48 | - `sql/schema.sql` — schema do banco (admins, settings, audit_log, login_attempts). |
| 49 | - `storage/` — log de auditoria em arquivo (complementar ao banco). | 49 | - `storage/` — log de auditoria em arquivo (complementar ao banco). |
| 50 | 50 | ||
| 51 | +## URLs amigáveis | ||
| 52 | + | ||
| 53 | +O `public_html/.htaccess` esconde a extensão `.php` (requer `mod_rewrite` e | ||
| 54 | +`AllowOverride FileInfo`/`Limit` no vhost, já presentes na config desse site): | ||
| 55 | +`/dashboard`, `/admins`, `/auditoria`, `/settings`, `/logout` etc. em vez de | ||
| 56 | +`dashboard.php`, `admins.php`... Acessar o `.php` direto redireciona pra URL limpa. | ||
| 57 | + | ||
| 51 | ## Papéis | 58 | ## Papéis |
| 52 | 59 | ||
| 53 | -- **admin**: tudo que operador pode, mais gerenciar `admins.php` e `settings.php`. | ||
| 54 | -- **operador**: só habilita/desabilita aluno e reseta senha no `dashboard.php`. | 60 | +- **admin**: tudo que operador pode, mais gerenciar administradores e configurações. |
| 61 | +- **operador**: só habilita/desabilita aluno e reseta senha no dashboard. | ||
| 55 | 62 | ||
| 56 | A autenticação de quem usa o sistema é sempre feita com login e senha do próprio AD | 63 | A autenticação de quem usa o sistema é sempre feita com login e senha do próprio AD |
| 57 | (bind LDAP) — o banco **não armazena nenhuma senha**. A tabela `admins` só guarda | 64 | (bind LDAP) — o banco **não armazena nenhuma senha**. A tabela `admins` só guarda |
| @@ -0,0 +1,27 @@ | @@ -0,0 +1,27 @@ | ||
| 1 | +RewriteEngine On | ||
| 2 | +RewriteBase / | ||
| 3 | + | ||
| 4 | +# Esconde a extensão .php: se alguém acessar /dashboard.php direto, redireciona | ||
| 5 | +# pra /dashboard (sem expor o nome do arquivo em links/histórico). | ||
| 6 | +RewriteCond %{THE_REQUEST} \s/+([^.\s]+)\.php[\s?] [NC] | ||
| 7 | +RewriteRule ^ /%1 [R=301,L] | ||
| 8 | + | ||
| 9 | +# Bloqueia acesso a qualquer coisa fora de public_html através de URL, | ||
| 10 | +# mesmo sem rewrite (defesa extra, já que src/config/storage ficam um | ||
| 11 | +# nível acima e não deveriam ser alcançáveis mesmo sem isso). | ||
| 12 | +RewriteRule ^(src|config|storage|sql)(/|$) - [F,L] | ||
| 13 | + | ||
| 14 | +# Roteia /algumacoisa para algumacoisa.php quando o arquivo existir e | ||
| 15 | +# nenhum arquivo/diretório real com esse nome exista. | ||
| 16 | +RewriteCond %{REQUEST_FILENAME} !-f | ||
| 17 | +RewriteCond %{REQUEST_FILENAME} !-d | ||
| 18 | +RewriteCond %{DOCUMENT_ROOT}/$1.php -f | ||
| 19 | +RewriteRule ^([a-zA-Z0-9_-]+)$ $1.php [L,QSA] | ||
| 20 | + | ||
| 21 | +# Nunca serve .htaccess/.env ou outro arquivo de config por engano. | ||
| 22 | +# (Options -Indexes não entra aqui de propósito: o AllowOverride do vhost | ||
| 23 | +# não libera o contexto "Options", só FileInfo/AuthConfig/Limit -- colocar | ||
| 24 | +# a diretiva aqui derrubaria o site com "Options not allowed here".) | ||
| 25 | +<FilesMatch "^\."> | ||
| 26 | + Require all denied | ||
| 27 | +</FilesMatch> |
public_html/action_status.php
| @@ -6,7 +6,7 @@ require __DIR__ . '/../src/bootstrap.php'; | @@ -6,7 +6,7 @@ require __DIR__ . '/../src/bootstrap.php'; | ||
| 6 | Auth::requireLogin(); | 6 | Auth::requireLogin(); |
| 7 | 7 | ||
| 8 | if ($_SERVER['REQUEST_METHOD'] !== 'POST') { | 8 | if ($_SERVER['REQUEST_METHOD'] !== 'POST') { |
| 9 | - header('Location: dashboard.php'); | 9 | + header('Location: dashboard'); |
| 10 | exit; | 10 | exit; |
| 11 | } | 11 | } |
| 12 | 12 | ||
| @@ -14,7 +14,7 @@ $user = trim((string) ($_POST['user'] ?? '')); | @@ -14,7 +14,7 @@ $user = trim((string) ($_POST['user'] ?? '')); | ||
| 14 | $enable = ($_POST['enable'] ?? '') === '1'; | 14 | $enable = ($_POST['enable'] ?? '') === '1'; |
| 15 | 15 | ||
| 16 | if ($user === '') { | 16 | if ($user === '') { |
| 17 | - header('Location: dashboard.php?erro=1&msg=' . urlencode('Usuário inválido.')); | 17 | + header('Location: dashboard?erro=1&msg=' . urlencode('Usuário inválido.')); |
| 18 | exit; | 18 | exit; |
| 19 | } | 19 | } |
| 20 | 20 | ||
| @@ -39,9 +39,9 @@ try { | @@ -39,9 +39,9 @@ try { | ||
| 39 | 39 | ||
| 40 | AuditLog::record($quemFez, $enable ? 'habilitou_aluno' : 'desabilitou_aluno', $user); | 40 | AuditLog::record($quemFez, $enable ? 'habilitou_aluno' : 'desabilitou_aluno', $user); |
| 41 | 41 | ||
| 42 | - header('Location: dashboard.php?ok=1'); | 42 | + header('Location: dashboard?ok=1'); |
| 43 | exit; | 43 | exit; |
| 44 | } catch (Throwable $e) { | 44 | } catch (Throwable $e) { |
| 45 | - header('Location: dashboard.php?erro=1&msg=' . urlencode($e->getMessage())); | 45 | + header('Location: dashboard?erro=1&msg=' . urlencode($e->getMessage())); |
| 46 | exit; | 46 | exit; |
| 47 | } | 47 | } |
public_html/admins.php
| @@ -86,7 +86,7 @@ View::header('Administradores'); | @@ -86,7 +86,7 @@ View::header('Administradores'); | ||
| 86 | <?php if ($erro): ?><div class="alert error"><?= View::escape($erro) ?></div><?php endif; ?> | 86 | <?php if ($erro): ?><div class="alert error"><?= View::escape($erro) ?></div><?php endif; ?> |
| 87 | <?php if ($ok): ?><div class="alert success"><?= View::escape($ok) ?></div><?php endif; ?> | 87 | <?php if ($ok): ?><div class="alert success"><?= View::escape($ok) ?></div><?php endif; ?> |
| 88 | 88 | ||
| 89 | - <form method="post" action="admins.php"> | 89 | + <form method="post" action="admins"> |
| 90 | <input type="hidden" name="acao" value="adicionar"> | 90 | <input type="hidden" name="acao" value="adicionar"> |
| 91 | <label for="login">Login do AD (sem o domínio <?= View::escape(Config::get('ldap.domain_suffix', '')) ?>)</label> | 91 | <label for="login">Login do AD (sem o domínio <?= View::escape(Config::get('ldap.domain_suffix', '')) ?>)</label> |
| 92 | <input type="text" id="login" name="login" required> | 92 | <input type="text" id="login" name="login" required> |
| @@ -108,11 +108,11 @@ View::header('Administradores'); | @@ -108,11 +108,11 @@ View::header('Administradores'); | ||
| 108 | </tr> | 108 | </tr> |
| 109 | <?php foreach ($admins as $a): ?> | 109 | <?php foreach ($admins as $a): ?> |
| 110 | <tr> | 110 | <tr> |
| 111 | - <td><?= View::escape($a['login']) ?></td> | ||
| 112 | - <td><?= View::escape($a['nome']) ?></td> | ||
| 113 | - <td> | 111 | + <td data-label="Login"><?= View::escape($a['login']) ?></td> |
| 112 | + <td data-label="Nome"><?= View::escape($a['nome']) ?></td> | ||
| 113 | + <td data-label="Papel"> | ||
| 114 | <?php if ($a['ativo']): ?> | 114 | <?php if ($a['ativo']): ?> |
| 115 | - <form method="post" action="admins.php" style="display:flex;gap:6px;align-items:center;margin:0"> | 115 | + <form method="post" action="admins" style="display:flex;gap:6px;align-items:center;margin:0;justify-content:flex-end"> |
| 116 | <input type="hidden" name="acao" value="atualizar_papel"> | 116 | <input type="hidden" name="acao" value="atualizar_papel"> |
| 117 | <input type="hidden" name="id" value="<?= (int) $a['id'] ?>"> | 117 | <input type="hidden" name="id" value="<?= (int) $a['id'] ?>"> |
| 118 | <select name="papel" style="margin:0"> | 118 | <select name="papel" style="margin:0"> |
| @@ -125,15 +125,15 @@ View::header('Administradores'); | @@ -125,15 +125,15 @@ View::header('Administradores'); | ||
| 125 | <?= View::escape($a['papel']) ?> | 125 | <?= View::escape($a['papel']) ?> |
| 126 | <?php endif; ?> | 126 | <?php endif; ?> |
| 127 | </td> | 127 | </td> |
| 128 | - <td> | 128 | + <td data-label="Status"> |
| 129 | <?php if ($a['ativo']): ?> | 129 | <?php if ($a['ativo']): ?> |
| 130 | <span class="badge ok">Ativo</span> | 130 | <span class="badge ok">Ativo</span> |
| 131 | <?php else: ?> | 131 | <?php else: ?> |
| 132 | <span class="badge off">Inativo</span> | 132 | <span class="badge off">Inativo</span> |
| 133 | <?php endif; ?> | 133 | <?php endif; ?> |
| 134 | </td> | 134 | </td> |
| 135 | - <td> | ||
| 136 | - <form method="post" action="admins.php" style="display:inline"> | 135 | + <td data-label="Ações"> |
| 136 | + <form method="post" action="admins" style="display:inline"> | ||
| 137 | <input type="hidden" name="id" value="<?= (int) $a['id'] ?>"> | 137 | <input type="hidden" name="id" value="<?= (int) $a['id'] ?>"> |
| 138 | <?php if ($a['ativo']): ?> | 138 | <?php if ($a['ativo']): ?> |
| 139 | <input type="hidden" name="acao" value="desativar"> | 139 | <input type="hidden" name="acao" value="desativar"> |
public_html/assets/style.css
| 1 | :root { | 1 | :root { |
| 2 | color-scheme: light; | 2 | color-scheme: light; |
| 3 | - --bg: #f4f5f7; | 3 | + --bg: #f1f3f8; |
| 4 | + --bg-pattern: radial-gradient(circle at 10% -10%, #dbe4ff 0%, transparent 45%), | ||
| 5 | + radial-gradient(circle at 110% 10%, #e3f3ff 0%, transparent 40%); | ||
| 4 | --card: #ffffff; | 6 | --card: #ffffff; |
| 5 | - --text: #1f2430; | ||
| 6 | - --muted: #6b7280; | ||
| 7 | - --border: #e2e4e9; | ||
| 8 | - --accent: #2563eb; | 7 | + --card-border: #e7e9f0; |
| 8 | + --text: #1a1d29; | ||
| 9 | + --text-muted: #6b7280; | ||
| 10 | + --border: #e7e9f0; | ||
| 11 | + --accent: #4f46e5; | ||
| 12 | + --accent-2: #4338ca; | ||
| 13 | + --accent-soft: #eef0ff; | ||
| 9 | --accent-contrast: #ffffff; | 14 | --accent-contrast: #ffffff; |
| 10 | --danger: #dc2626; | 15 | --danger: #dc2626; |
| 16 | + --danger-soft: #fee2e2; | ||
| 11 | --success: #16a34a; | 17 | --success: #16a34a; |
| 18 | + --success-soft: #dcfce7; | ||
| 19 | + --shadow-sm: 0 1px 2px rgba(16, 24, 40, 0.06); | ||
| 20 | + --shadow-md: 0 8px 24px -8px rgba(16, 24, 40, 0.18), 0 2px 6px rgba(16, 24, 40, 0.06); | ||
| 21 | + --radius: 14px; | ||
| 22 | + --radius-sm: 8px; | ||
| 23 | +} | ||
| 24 | + | ||
| 25 | +@media (prefers-color-scheme: dark) { | ||
| 26 | + :root:not([data-theme="light"]) { | ||
| 27 | + color-scheme: dark; | ||
| 28 | + --bg: #0f1117; | ||
| 29 | + --bg-pattern: radial-gradient(circle at 10% -10%, #1e2140 0%, transparent 45%), | ||
| 30 | + radial-gradient(circle at 110% 10%, #10233a 0%, transparent 40%); | ||
| 31 | + --card: #171923; | ||
| 32 | + --card-border: #272b3a; | ||
| 33 | + --text: #eef0f5; | ||
| 34 | + --text-muted: #9aa0b2; | ||
| 35 | + --border: #272b3a; | ||
| 36 | + --accent: #6d66f0; | ||
| 37 | + --accent-2: #857ef5; | ||
| 38 | + --accent-soft: #211f3d; | ||
| 39 | + --accent-contrast: #ffffff; | ||
| 40 | + --danger: #f87171; | ||
| 41 | + --danger-soft: #3a1a1c; | ||
| 42 | + --success: #4ade80; | ||
| 43 | + --success-soft: #11301f; | ||
| 44 | + --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.3); | ||
| 45 | + --shadow-md: 0 12px 32px -12px rgba(0, 0, 0, 0.55), 0 2px 8px rgba(0, 0, 0, 0.35); | ||
| 46 | + } | ||
| 47 | +} | ||
| 48 | + | ||
| 49 | +:root[data-theme="dark"] { | ||
| 50 | + color-scheme: dark; | ||
| 51 | + --bg: #0f1117; | ||
| 52 | + --bg-pattern: radial-gradient(circle at 10% -10%, #1e2140 0%, transparent 45%), | ||
| 53 | + radial-gradient(circle at 110% 10%, #10233a 0%, transparent 40%); | ||
| 54 | + --card: #171923; | ||
| 55 | + --card-border: #272b3a; | ||
| 56 | + --text: #eef0f5; | ||
| 57 | + --text-muted: #9aa0b2; | ||
| 58 | + --border: #272b3a; | ||
| 59 | + --accent: #6d66f0; | ||
| 60 | + --accent-2: #857ef5; | ||
| 61 | + --accent-soft: #211f3d; | ||
| 62 | + --accent-contrast: #ffffff; | ||
| 63 | + --danger: #f87171; | ||
| 64 | + --danger-soft: #3a1a1c; | ||
| 65 | + --success: #4ade80; | ||
| 66 | + --success-soft: #11301f; | ||
| 67 | + --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.3); | ||
| 68 | + --shadow-md: 0 12px 32px -12px rgba(0, 0, 0, 0.55), 0 2px 8px rgba(0, 0, 0, 0.35); | ||
| 12 | } | 69 | } |
| 13 | 70 | ||
| 14 | * { box-sizing: border-box; } | 71 | * { box-sizing: border-box; } |
| 15 | 72 | ||
| 73 | +html { -webkit-text-size-adjust: 100%; } | ||
| 74 | + | ||
| 16 | body { | 75 | body { |
| 17 | margin: 0; | 76 | margin: 0; |
| 18 | - font-family: -apple-system, Segoe UI, Roboto, Arial, sans-serif; | ||
| 19 | - background: var(--bg); | 77 | + min-height: 100vh; |
| 78 | + font-family: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; | ||
| 79 | + background-color: var(--bg); | ||
| 80 | + background-image: var(--bg-pattern); | ||
| 81 | + background-attachment: fixed; | ||
| 20 | color: var(--text); | 82 | color: var(--text); |
| 83 | + -webkit-font-smoothing: antialiased; | ||
| 21 | } | 84 | } |
| 22 | 85 | ||
| 86 | +/* Topbar */ | ||
| 87 | + | ||
| 23 | .topbar { | 88 | .topbar { |
| 24 | display: flex; | 89 | display: flex; |
| 25 | justify-content: space-between; | 90 | justify-content: space-between; |
| 26 | align-items: center; | 91 | align-items: center; |
| 92 | + gap: 16px; | ||
| 27 | padding: 14px 24px; | 93 | padding: 14px 24px; |
| 28 | - background: var(--card); | 94 | + background: color-mix(in srgb, var(--card) 88%, transparent); |
| 95 | + backdrop-filter: blur(10px); | ||
| 29 | border-bottom: 1px solid var(--border); | 96 | border-bottom: 1px solid var(--border); |
| 97 | + position: sticky; | ||
| 98 | + top: 0; | ||
| 99 | + z-index: 20; | ||
| 100 | + flex-wrap: wrap; | ||
| 101 | +} | ||
| 102 | + | ||
| 103 | +.topbar strong { | ||
| 104 | + font-weight: 700; | ||
| 105 | + letter-spacing: -0.01em; | ||
| 106 | + font-size: 15px; | ||
| 107 | + white-space: nowrap; | ||
| 108 | +} | ||
| 109 | + | ||
| 110 | +.topbar nav { | ||
| 111 | + display: flex; | ||
| 112 | + align-items: center; | ||
| 113 | + gap: 4px; | ||
| 114 | + flex-wrap: wrap; | ||
| 115 | +} | ||
| 116 | + | ||
| 117 | +.topbar nav span { | ||
| 118 | + color: var(--text-muted); | ||
| 119 | + font-size: 13px; | ||
| 120 | + margin-right: 10px; | ||
| 121 | + padding-right: 10px; | ||
| 122 | + border-right: 1px solid var(--border); | ||
| 30 | } | 123 | } |
| 31 | 124 | ||
| 32 | .topbar nav a { | 125 | .topbar nav a { |
| 33 | - margin-left: 16px; | ||
| 34 | - color: var(--text); | 126 | + color: var(--text-muted); |
| 35 | text-decoration: none; | 127 | text-decoration: none; |
| 36 | font-size: 14px; | 128 | font-size: 14px; |
| 129 | + font-weight: 500; | ||
| 130 | + padding: 7px 12px; | ||
| 131 | + border-radius: 999px; | ||
| 132 | + transition: background-color 0.15s ease, color 0.15s ease; | ||
| 133 | + white-space: nowrap; | ||
| 37 | } | 134 | } |
| 38 | 135 | ||
| 39 | -.topbar nav a:hover { color: var(--accent); } | 136 | +.topbar nav a:hover { |
| 137 | + color: var(--accent); | ||
| 138 | + background: var(--accent-soft); | ||
| 139 | +} | ||
| 140 | + | ||
| 141 | +/* Layout */ | ||
| 40 | 142 | ||
| 41 | .container { | 143 | .container { |
| 42 | - max-width: 960px; | ||
| 43 | - margin: 32px auto; | ||
| 44 | - padding: 0 16px; | 144 | + max-width: 1000px; |
| 145 | + margin: 0 auto; | ||
| 146 | + padding: 28px 16px 48px; | ||
| 45 | } | 147 | } |
| 46 | 148 | ||
| 47 | .card { | 149 | .card { |
| 48 | background: var(--card); | 150 | background: var(--card); |
| 49 | - border: 1px solid var(--border); | ||
| 50 | - border-radius: 10px; | ||
| 51 | - padding: 24px; | 151 | + border: 1px solid var(--card-border); |
| 152 | + border-radius: var(--radius); | ||
| 153 | + padding: 28px; | ||
| 154 | + box-shadow: var(--shadow-sm); | ||
| 52 | } | 155 | } |
| 53 | 156 | ||
| 54 | .login-wrap { | 157 | .login-wrap { |
| @@ -56,46 +159,210 @@ body { | @@ -56,46 +159,210 @@ body { | ||
| 56 | min-height: 100vh; | 159 | min-height: 100vh; |
| 57 | align-items: center; | 160 | align-items: center; |
| 58 | justify-content: center; | 161 | justify-content: center; |
| 162 | + padding: 16px; | ||
| 163 | +} | ||
| 164 | + | ||
| 165 | +.login-wrap .card { | ||
| 166 | + width: 100%; | ||
| 167 | + max-width: 380px; | ||
| 168 | + box-shadow: var(--shadow-md); | ||
| 169 | + animation: rise 0.35s ease; | ||
| 59 | } | 170 | } |
| 60 | 171 | ||
| 61 | -.login-wrap .card { width: 100%; max-width: 360px; } | 172 | +.login-wrap .card h1 { |
| 173 | + text-align: center; | ||
| 174 | + margin-bottom: 4px; | ||
| 175 | +} | ||
| 62 | 176 | ||
| 63 | -h1 { font-size: 20px; margin: 0 0 16px; } | 177 | +.login-sub { |
| 178 | + text-align: center; | ||
| 179 | + color: var(--text-muted); | ||
| 180 | + font-size: 13px; | ||
| 181 | + margin: 0 0 22px; | ||
| 182 | +} | ||
| 64 | 183 | ||
| 65 | -label { display: block; font-size: 13px; color: var(--muted); margin-bottom: 4px; } | 184 | +@keyframes rise { |
| 185 | + from { opacity: 0; transform: translateY(8px); } | ||
| 186 | + to { opacity: 1; transform: translateY(0); } | ||
| 187 | +} | ||
| 66 | 188 | ||
| 67 | -input[type=text], input[type=password], select { | 189 | +h1 { |
| 190 | + font-size: 21px; | ||
| 191 | + font-weight: 700; | ||
| 192 | + letter-spacing: -0.01em; | ||
| 193 | + margin: 0 0 18px; | ||
| 194 | +} | ||
| 195 | + | ||
| 196 | +label { | ||
| 197 | + display: block; | ||
| 198 | + font-size: 13px; | ||
| 199 | + font-weight: 500; | ||
| 200 | + color: var(--text-muted); | ||
| 201 | + margin-bottom: 6px; | ||
| 202 | +} | ||
| 203 | + | ||
| 204 | +input[type=text], | ||
| 205 | +input[type=password], | ||
| 206 | +select { | ||
| 68 | width: 100%; | 207 | width: 100%; |
| 69 | - padding: 8px 10px; | ||
| 70 | - margin-bottom: 14px; | 208 | + padding: 10px 12px; |
| 209 | + margin-bottom: 16px; | ||
| 71 | border: 1px solid var(--border); | 210 | border: 1px solid var(--border); |
| 72 | - border-radius: 6px; | 211 | + border-radius: var(--radius-sm); |
| 73 | font-size: 14px; | 212 | font-size: 14px; |
| 213 | + background: var(--bg); | ||
| 214 | + color: var(--text); | ||
| 215 | + transition: border-color 0.15s ease, box-shadow 0.15s ease; | ||
| 216 | +} | ||
| 217 | + | ||
| 218 | +input[type=text]:focus, | ||
| 219 | +input[type=password]:focus, | ||
| 220 | +select:focus { | ||
| 221 | + outline: none; | ||
| 222 | + border-color: var(--accent); | ||
| 223 | + box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 20%, transparent); | ||
| 74 | } | 224 | } |
| 75 | 225 | ||
| 76 | button, .btn { | 226 | button, .btn { |
| 77 | - display: inline-block; | 227 | + display: inline-flex; |
| 228 | + align-items: center; | ||
| 229 | + justify-content: center; | ||
| 230 | + gap: 6px; | ||
| 78 | background: var(--accent); | 231 | background: var(--accent); |
| 232 | + background-image: linear-gradient(135deg, var(--accent), var(--accent-2)); | ||
| 79 | color: var(--accent-contrast); | 233 | color: var(--accent-contrast); |
| 80 | border: none; | 234 | border: none; |
| 81 | - padding: 8px 14px; | ||
| 82 | - border-radius: 6px; | 235 | + padding: 9px 16px; |
| 236 | + border-radius: var(--radius-sm); | ||
| 83 | font-size: 14px; | 237 | font-size: 14px; |
| 238 | + font-weight: 600; | ||
| 84 | cursor: pointer; | 239 | cursor: pointer; |
| 85 | text-decoration: none; | 240 | text-decoration: none; |
| 241 | + box-shadow: var(--shadow-sm); | ||
| 242 | + transition: transform 0.1s ease, box-shadow 0.15s ease, opacity 0.15s ease; | ||
| 243 | +} | ||
| 244 | + | ||
| 245 | +button:hover, .btn:hover { opacity: 0.92; } | ||
| 246 | +button:active, .btn:active { transform: translateY(1px); } | ||
| 247 | + | ||
| 248 | +button.danger, .btn.danger { | ||
| 249 | + background-image: none; | ||
| 250 | + background: var(--danger); | ||
| 251 | +} | ||
| 252 | + | ||
| 253 | +button.secondary, .btn.secondary { | ||
| 254 | + background-image: none; | ||
| 255 | + background: transparent; | ||
| 256 | + color: var(--text-muted); | ||
| 257 | + border: 1px solid var(--border); | ||
| 258 | + box-shadow: none; | ||
| 259 | +} | ||
| 260 | + | ||
| 261 | +button.secondary:hover, .btn.secondary:hover { | ||
| 262 | + color: var(--text); | ||
| 263 | + border-color: var(--text-muted); | ||
| 264 | + opacity: 1; | ||
| 265 | +} | ||
| 266 | + | ||
| 267 | +/* Tabela / responsiva */ | ||
| 268 | + | ||
| 269 | +table { | ||
| 270 | + width: 100%; | ||
| 271 | + border-collapse: collapse; | ||
| 272 | + margin-top: 16px; | ||
| 273 | +} | ||
| 274 | + | ||
| 275 | +th, td { | ||
| 276 | + text-align: left; | ||
| 277 | + padding: 12px 10px; | ||
| 278 | + border-bottom: 1px solid var(--border); | ||
| 279 | + font-size: 14px; | ||
| 280 | + vertical-align: middle; | ||
| 281 | +} | ||
| 282 | + | ||
| 283 | +th { | ||
| 284 | + color: var(--text-muted); | ||
| 285 | + font-weight: 600; | ||
| 286 | + font-size: 12px; | ||
| 287 | + text-transform: uppercase; | ||
| 288 | + letter-spacing: 0.03em; | ||
| 289 | +} | ||
| 290 | + | ||
| 291 | +tr:last-child td { border-bottom: none; } | ||
| 292 | + | ||
| 293 | +.badge { | ||
| 294 | + display: inline-block; | ||
| 295 | + padding: 3px 10px; | ||
| 296 | + border-radius: 999px; | ||
| 297 | + font-size: 12px; | ||
| 298 | + font-weight: 600; | ||
| 86 | } | 299 | } |
| 87 | 300 | ||
| 88 | -button.danger, .btn.danger { background: var(--danger); } | ||
| 89 | -button.secondary, .btn.secondary { background: var(--muted); } | 301 | +.badge.ok { background: var(--success-soft); color: var(--success); } |
| 302 | +.badge.off { background: var(--danger-soft); color: var(--danger); } | ||
| 90 | 303 | ||
| 91 | -table { width: 100%; border-collapse: collapse; margin-top: 12px; } | ||
| 92 | -th, td { text-align: left; padding: 8px 10px; border-bottom: 1px solid var(--border); font-size: 14px; } | ||
| 93 | -th { color: var(--muted); font-weight: 600; } | 304 | +.alert { |
| 305 | + padding: 11px 16px; | ||
| 306 | + border-radius: var(--radius-sm); | ||
| 307 | + margin-bottom: 18px; | ||
| 308 | + font-size: 14px; | ||
| 309 | + border: 1px solid transparent; | ||
| 310 | +} | ||
| 94 | 311 | ||
| 95 | -.badge { padding: 2px 8px; border-radius: 999px; font-size: 12px; } | ||
| 96 | -.badge.ok { background: #dcfce7; color: var(--success); } | ||
| 97 | -.badge.off { background: #fee2e2; color: var(--danger); } | 312 | +.alert.error { background: var(--danger-soft); color: var(--danger); border-color: color-mix(in srgb, var(--danger) 30%, transparent); } |
| 313 | +.alert.success { background: var(--success-soft); color: var(--success); border-color: color-mix(in srgb, var(--success) 30%, transparent); } | ||
| 98 | 314 | ||
| 99 | -.alert { padding: 10px 14px; border-radius: 6px; margin-bottom: 16px; font-size: 14px; } | ||
| 100 | -.alert.error { background: #fee2e2; color: var(--danger); } | ||
| 101 | -.alert.success { background: #dcfce7; color: var(--success); } | 315 | +.hint { |
| 316 | + color: var(--text-muted); | ||
| 317 | + font-size: 13px; | ||
| 318 | +} | ||
| 319 | + | ||
| 320 | +code { | ||
| 321 | + background: var(--accent-soft); | ||
| 322 | + color: var(--accent); | ||
| 323 | + padding: 2px 6px; | ||
| 324 | + border-radius: 4px; | ||
| 325 | + font-size: 12.5px; | ||
| 326 | +} | ||
| 327 | + | ||
| 328 | +/* Responsivo: tabela vira "cards" empilhados no celular */ | ||
| 329 | + | ||
| 330 | +@media (max-width: 720px) { | ||
| 331 | + .container { padding: 20px 12px 40px; } | ||
| 332 | + .card { padding: 18px; border-radius: 12px; } | ||
| 333 | + | ||
| 334 | + .topbar { padding: 12px 16px; } | ||
| 335 | + .topbar nav span { border-right: none; padding-right: 0; margin-right: 0; width: 100%; order: -1; } | ||
| 336 | + | ||
| 337 | + table, thead, tbody, th, td, tr { display: block; } | ||
| 338 | + thead { display: none; } | ||
| 339 | + | ||
| 340 | + tr { | ||
| 341 | + border: 1px solid var(--border); | ||
| 342 | + border-radius: var(--radius-sm); | ||
| 343 | + margin-bottom: 12px; | ||
| 344 | + padding: 10px 12px; | ||
| 345 | + } | ||
| 346 | + | ||
| 347 | + td { | ||
| 348 | + border-bottom: none; | ||
| 349 | + padding: 6px 0; | ||
| 350 | + display: flex; | ||
| 351 | + justify-content: space-between; | ||
| 352 | + align-items: center; | ||
| 353 | + gap: 12px; | ||
| 354 | + text-align: right; | ||
| 355 | + } | ||
| 356 | + | ||
| 357 | + td::before { | ||
| 358 | + content: attr(data-label); | ||
| 359 | + font-size: 12px; | ||
| 360 | + font-weight: 600; | ||
| 361 | + color: var(--text-muted); | ||
| 362 | + text-transform: uppercase; | ||
| 363 | + letter-spacing: 0.03em; | ||
| 364 | + text-align: left; | ||
| 365 | + } | ||
| 366 | + | ||
| 367 | + td form { justify-content: flex-end; } | ||
| 368 | +} |
public_html/auditoria.php
| @@ -40,7 +40,7 @@ View::header('Auditoria'); | @@ -40,7 +40,7 @@ View::header('Auditoria'); | ||
| 40 | ?> | 40 | ?> |
| 41 | <div class="card"> | 41 | <div class="card"> |
| 42 | <h1>Auditoria</h1> | 42 | <h1>Auditoria</h1> |
| 43 | - <p style="color:#6b7280;font-size:13px"> | 43 | + <p class="hint"> |
| 44 | Histórico de logins (inclusive tentativas com senha incorreta), alterações em | 44 | Histórico de logins (inclusive tentativas com senha incorreta), alterações em |
| 45 | alunos, administradores e configurações. Mostrando <?= count($logs) ?> de <?= $total ?> registros. | 45 | alunos, administradores e configurações. Mostrando <?= count($logs) ?> de <?= $total ?> registros. |
| 46 | </p> | 46 | </p> |
| @@ -56,12 +56,12 @@ View::header('Auditoria'); | @@ -56,12 +56,12 @@ View::header('Auditoria'); | ||
| 56 | </tr> | 56 | </tr> |
| 57 | <?php foreach ($logs as $log): ?> | 57 | <?php foreach ($logs as $log): ?> |
| 58 | <tr> | 58 | <tr> |
| 59 | - <td><?= View::escape($log['criado_em']) ?></td> | ||
| 60 | - <td><?= View::escape($log['usuario']) ?></td> | ||
| 61 | - <td><?= View::escape($acaoLabels[$log['acao']] ?? $log['acao']) ?></td> | ||
| 62 | - <td><?= View::escape($log['alvo'] ?? '') ?></td> | ||
| 63 | - <td><?= View::escape($log['detalhe'] ?? '') ?></td> | ||
| 64 | - <td><?= View::escape($log['ip'] ?? '') ?></td> | 59 | + <td data-label="Data/hora"><?= View::escape($log['criado_em']) ?></td> |
| 60 | + <td data-label="Usuário"><?= View::escape($log['usuario']) ?></td> | ||
| 61 | + <td data-label="Ação"><?= View::escape($acaoLabels[$log['acao']] ?? $log['acao']) ?></td> | ||
| 62 | + <td data-label="Alvo"><?= View::escape($log['alvo'] ?? '') ?></td> | ||
| 63 | + <td data-label="Detalhe"><?= View::escape($log['detalhe'] ?? '') ?></td> | ||
| 64 | + <td data-label="IP"><?= View::escape($log['ip'] ?? '') ?></td> | ||
| 65 | </tr> | 65 | </tr> |
| 66 | <?php endforeach; ?> | 66 | <?php endforeach; ?> |
| 67 | <?php if (!$logs): ?> | 67 | <?php if (!$logs): ?> |
| @@ -75,7 +75,7 @@ View::header('Auditoria'); | @@ -75,7 +75,7 @@ View::header('Auditoria'); | ||
| 75 | <?php if ($p === $page): ?> | 75 | <?php if ($p === $page): ?> |
| 76 | <strong><?= $p ?></strong> | 76 | <strong><?= $p ?></strong> |
| 77 | <?php else: ?> | 77 | <?php else: ?> |
| 78 | - <a href="auditoria.php?p=<?= $p ?>"><?= $p ?></a> | 78 | + <a href="auditoria?p=<?= $p ?>"><?= $p ?></a> |
| 79 | <?php endif; ?> | 79 | <?php endif; ?> |
| 80 | <?php if ($p < $totalPages): ?> | <?php endif; ?> | 80 | <?php if ($p < $totalPages): ?> | <?php endif; ?> |
| 81 | <?php endfor; ?> | 81 | <?php endfor; ?> |
public_html/dashboard.php
| @@ -61,24 +61,24 @@ View::header('Alunos'); | @@ -61,24 +61,24 @@ View::header('Alunos'); | ||
| 61 | </tr> | 61 | </tr> |
| 62 | <?php foreach ($alunos as $aluno): ?> | 62 | <?php foreach ($alunos as $aluno): ?> |
| 63 | <tr> | 63 | <tr> |
| 64 | - <td><?= View::escape($aluno['nome']) ?></td> | ||
| 65 | - <td> | 64 | + <td data-label="Aluno"><?= View::escape($aluno['nome']) ?></td> |
| 65 | + <td data-label="Status"> | ||
| 66 | <?php if ($aluno['habilitado']): ?> | 66 | <?php if ($aluno['habilitado']): ?> |
| 67 | <span class="badge ok">Habilitado</span> | 67 | <span class="badge ok">Habilitado</span> |
| 68 | <?php else: ?> | 68 | <?php else: ?> |
| 69 | <span class="badge off">Desabilitado</span> | 69 | <span class="badge off">Desabilitado</span> |
| 70 | <?php endif; ?> | 70 | <?php endif; ?> |
| 71 | </td> | 71 | </td> |
| 72 | - <td><?= View::escape($aluno['descricao']) ?></td> | ||
| 73 | - <td> | ||
| 74 | - <form method="post" action="action_status.php" style="display:inline"> | 72 | + <td data-label="Última alteração por"><?= View::escape($aluno['descricao']) ?></td> |
| 73 | + <td data-label="Ações"> | ||
| 74 | + <form method="post" action="action_status" style="display:inline"> | ||
| 75 | <input type="hidden" name="user" value="<?= View::escape($aluno['cn']) ?>"> | 75 | <input type="hidden" name="user" value="<?= View::escape($aluno['cn']) ?>"> |
| 76 | <input type="hidden" name="enable" value="<?= $aluno['habilitado'] ? '0' : '1' ?>"> | 76 | <input type="hidden" name="enable" value="<?= $aluno['habilitado'] ? '0' : '1' ?>"> |
| 77 | <button type="submit" class="<?= $aluno['habilitado'] ? 'danger' : '' ?>"> | 77 | <button type="submit" class="<?= $aluno['habilitado'] ? 'danger' : '' ?>"> |
| 78 | <?= $aluno['habilitado'] ? 'Desabilitar' : 'Habilitar' ?> | 78 | <?= $aluno['habilitado'] ? 'Desabilitar' : 'Habilitar' ?> |
| 79 | </button> | 79 | </button> |
| 80 | </form> | 80 | </form> |
| 81 | - <a class="btn secondary" href="newpasswd.php?user=<?= urlencode($aluno['cn']) ?>">Nova senha</a> | 81 | + <a class="btn secondary" href="newpasswd?user=<?= urlencode($aluno['cn']) ?>">Nova senha</a> |
| 82 | </td> | 82 | </td> |
| 83 | </tr> | 83 | </tr> |
| 84 | <?php endforeach; ?> | 84 | <?php endforeach; ?> |
public_html/index.php
| @@ -5,7 +5,7 @@ declare(strict_types=1); | @@ -5,7 +5,7 @@ declare(strict_types=1); | ||
| 5 | require __DIR__ . '/../src/bootstrap.php'; | 5 | require __DIR__ . '/../src/bootstrap.php'; |
| 6 | 6 | ||
| 7 | if (!empty($_SESSION['login'])) { | 7 | if (!empty($_SESSION['login'])) { |
| 8 | - header('Location: dashboard.php'); | 8 | + header('Location: dashboard'); |
| 9 | exit; | 9 | exit; |
| 10 | } | 10 | } |
| 11 | 11 | ||
| @@ -20,7 +20,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { | @@ -20,7 +20,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { | ||
| 20 | } else { | 20 | } else { |
| 21 | $result = Auth::attempt($login, $senha); | 21 | $result = Auth::attempt($login, $senha); |
| 22 | if ($result['ok']) { | 22 | if ($result['ok']) { |
| 23 | - header('Location: dashboard.php'); | 23 | + header('Location: dashboard'); |
| 24 | exit; | 24 | exit; |
| 25 | } | 25 | } |
| 26 | $erro = $result['error']; | 26 | $erro = $result['error']; |
| @@ -33,17 +33,22 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { | @@ -33,17 +33,22 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { | ||
| 33 | <html lang="pt-br"> | 33 | <html lang="pt-br"> |
| 34 | <head> | 34 | <head> |
| 35 | <meta charset="utf-8"> | 35 | <meta charset="utf-8"> |
| 36 | + <meta name="viewport" content="width=device-width, initial-scale=1"> | ||
| 36 | <title>Gestão de Alunos Lab. Informática</title> | 37 | <title>Gestão de Alunos Lab. Informática</title> |
| 38 | + <link rel="preconnect" href="https://fonts.googleapis.com"> | ||
| 39 | + <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> | ||
| 40 | + <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet"> | ||
| 37 | <link rel="stylesheet" href="assets/style.css"> | 41 | <link rel="stylesheet" href="assets/style.css"> |
| 38 | </head> | 42 | </head> |
| 39 | <body> | 43 | <body> |
| 40 | <div class="login-wrap"> | 44 | <div class="login-wrap"> |
| 41 | <div class="card"> | 45 | <div class="card"> |
| 42 | - <h1>Gestão de Alunos Lab. Informática</h1> | 46 | + <h1>Gestão de Alunos</h1> |
| 47 | + <p class="login-sub">Lab. Informática</p> | ||
| 43 | <?php if ($erro): ?> | 48 | <?php if ($erro): ?> |
| 44 | <div class="alert error"><?= htmlspecialchars($erro, ENT_QUOTES) ?></div> | 49 | <div class="alert error"><?= htmlspecialchars($erro, ENT_QUOTES) ?></div> |
| 45 | <?php endif; ?> | 50 | <?php endif; ?> |
| 46 | - <form method="post" action="index.php"> | 51 | + <form method="post" action=""> |
| 47 | <label for="login">Login</label> | 52 | <label for="login">Login</label> |
| 48 | <input type="text" id="login" name="login" autocomplete="username" required autofocus> | 53 | <input type="text" id="login" name="login" autocomplete="username" required autofocus> |
| 49 | 54 |
public_html/logout.php
public_html/newpasswd.php
| @@ -52,7 +52,7 @@ View::header('Nova senha'); | @@ -52,7 +52,7 @@ View::header('Nova senha'); | ||
| 52 | <?php if ($ok): ?><div class="alert success"><?= View::escape($ok) ?></div><?php endif; ?> | 52 | <?php if ($ok): ?><div class="alert success"><?= View::escape($ok) ?></div><?php endif; ?> |
| 53 | 53 | ||
| 54 | <?php if (!$ok): ?> | 54 | <?php if (!$ok): ?> |
| 55 | - <form method="post" action="newpasswd.php"> | 55 | + <form method="post" action="newpasswd"> |
| 56 | <input type="hidden" name="user" value="<?= View::escape($user) ?>"> | 56 | <input type="hidden" name="user" value="<?= View::escape($user) ?>"> |
| 57 | <label for="passwd1">Nova senha</label> | 57 | <label for="passwd1">Nova senha</label> |
| 58 | <input type="password" id="passwd1" name="passwd1" required minlength="8"> | 58 | <input type="password" id="passwd1" name="passwd1" required minlength="8"> |
| @@ -62,7 +62,7 @@ View::header('Nova senha'); | @@ -62,7 +62,7 @@ View::header('Nova senha'); | ||
| 62 | </form> | 62 | </form> |
| 63 | <?php endif; ?> | 63 | <?php endif; ?> |
| 64 | 64 | ||
| 65 | - <p><a href="dashboard.php">← Voltar</a></p> | 65 | + <p><a href="dashboard">← Voltar</a></p> |
| 66 | </div> | 66 | </div> |
| 67 | <?php | 67 | <?php |
| 68 | View::footer(); | 68 | View::footer(); |
public_html/settings.php
| @@ -29,14 +29,14 @@ View::header('Configurações'); | @@ -29,14 +29,14 @@ View::header('Configurações'); | ||
| 29 | <?php if ($erro): ?><div class="alert error"><?= View::escape($erro) ?></div><?php endif; ?> | 29 | <?php if ($erro): ?><div class="alert error"><?= View::escape($erro) ?></div><?php endif; ?> |
| 30 | <?php if ($ok): ?><div class="alert success"><?= View::escape($ok) ?></div><?php endif; ?> | 30 | <?php if ($ok): ?><div class="alert success"><?= View::escape($ok) ?></div><?php endif; ?> |
| 31 | 31 | ||
| 32 | - <form method="post" action="settings.php"> | 32 | + <form method="post" action="settings"> |
| 33 | <label for="group_aluno_filter">Grupo (CN) de alunos monitorado no dashboard</label> | 33 | <label for="group_aluno_filter">Grupo (CN) de alunos monitorado no dashboard</label> |
| 34 | <input type="text" id="group_aluno_filter" name="group_aluno_filter" | 34 | <input type="text" id="group_aluno_filter" name="group_aluno_filter" |
| 35 | value="<?= View::escape($settings['group_aluno_filter'] ?? '') ?>" required> | 35 | value="<?= View::escape($settings['group_aluno_filter'] ?? '') ?>" required> |
| 36 | <button type="submit">Salvar</button> | 36 | <button type="submit">Salvar</button> |
| 37 | </form> | 37 | </form> |
| 38 | 38 | ||
| 39 | - <p style="color:#6b7280;font-size:13px"> | 39 | + <p class="hint"> |
| 40 | As demais configurações de conexão (servidor LDAP, conta de serviço, credenciais de banco) | 40 | As demais configurações de conexão (servidor LDAP, conta de serviço, credenciais de banco) |
| 41 | ficam em <code>config/local.php</code>, fora do versionamento, por segurança. | 41 | ficam em <code>config/local.php</code>, fora do versionamento, por segurança. |
| 42 | </p> | 42 | </p> |
src/Auth.php
| @@ -81,7 +81,7 @@ final class Auth | @@ -81,7 +81,7 @@ final class Auth | ||
| 81 | { | 81 | { |
| 82 | self::start(); | 82 | self::start(); |
| 83 | if (empty($_SESSION['login'])) { | 83 | if (empty($_SESSION['login'])) { |
| 84 | - header('Location: index.php?erro=sessao_expirada'); | 84 | + header('Location: /?erro=sessao_expirada'); |
| 85 | exit; | 85 | exit; |
| 86 | } | 86 | } |
| 87 | } | 87 | } |
src/View.php
| @@ -8,7 +8,11 @@ final class View | @@ -8,7 +8,11 @@ final class View | ||
| 8 | <html lang="pt-br"> | 8 | <html lang="pt-br"> |
| 9 | <head> | 9 | <head> |
| 10 | <meta charset="utf-8"> | 10 | <meta charset="utf-8"> |
| 11 | + <meta name="viewport" content="width=device-width, initial-scale=1"> | ||
| 11 | <title><?= htmlspecialchars($title, ENT_QUOTES) ?> - Gestão de Alunos Lab. Informática</title> | 12 | <title><?= htmlspecialchars($title, ENT_QUOTES) ?> - Gestão de Alunos Lab. Informática</title> |
| 13 | + <link rel="preconnect" href="https://fonts.googleapis.com"> | ||
| 14 | + <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> | ||
| 15 | + <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet"> | ||
| 12 | <link rel="stylesheet" href="assets/style.css"> | 16 | <link rel="stylesheet" href="assets/style.css"> |
| 13 | </head> | 17 | </head> |
| 14 | <body> | 18 | <body> |
| @@ -16,13 +20,13 @@ final class View | @@ -16,13 +20,13 @@ final class View | ||
| 16 | <strong>Gestão de Alunos Lab. Informática</strong> | 20 | <strong>Gestão de Alunos Lab. Informática</strong> |
| 17 | <nav> | 21 | <nav> |
| 18 | <span><?= htmlspecialchars(Auth::currentName() ?? '', ENT_QUOTES) ?></span> | 22 | <span><?= htmlspecialchars(Auth::currentName() ?? '', ENT_QUOTES) ?></span> |
| 19 | - <a href="dashboard.php">Alunos</a> | 23 | + <a href="dashboard">Alunos</a> |
| 20 | <?php if (Auth::isAdmin()): ?> | 24 | <?php if (Auth::isAdmin()): ?> |
| 21 | - <a href="admins.php">Administradores</a> | ||
| 22 | - <a href="auditoria.php">Auditoria</a> | ||
| 23 | - <a href="settings.php">Configurações</a> | 25 | + <a href="admins">Administradores</a> |
| 26 | + <a href="auditoria">Auditoria</a> | ||
| 27 | + <a href="settings">Configurações</a> | ||
| 24 | <?php endif; ?> | 28 | <?php endif; ?> |
| 25 | - <a href="logout.php">Sair</a> | 29 | + <a href="logout">Sair</a> |
| 26 | </nav> | 30 | </nav> |
| 27 | </div> | 31 | </div> |
| 28 | <div class="container"> | 32 | <div class="container"> |