1<!DOCTYPE html> 2<html lang="pt-BR"> 3<head> 4<meta charset="UTF-8" /> 5<meta name="viewport" content="width=device-width, initial-scale=1.0" /> 6<title>Admin â Blog Grupo Trinta</title>
7<script src="https://cdn.jsdelivr.net/npm/@supabase/supabase-js@2"></script>
vendor: 1 bytes, line 7
7
8<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/quill.js"></script>
8 9<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/quill.snow.css" rel="stylesheet" /> 10<style> 11 :root { 12 --dark: #031a25; 13 --gold: #cbb36d; 14 --off: #faf9f6; 15 --mid: #333333; 16 --red: #c0392b; 17 --green:#1a7a4a; 18 --radius: 8px; 19 --shadow: 0 2px 12px rgba(0,0,0,.12); 20 } 21 *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } 22 body { font-family: 'Segoe UI', system-ui, sans-serif; background: #f0ede8; color: var(--mid); min-height: 100vh; } 23 24 /* ââ LOGIN ââ */ 25 #login-screen { 26 display: flex; align-items: center; justify-content: center; 27 min-height: 100vh; background: var(--dark); 28 } 29 .login-card { 30 background: white; border-radius: 12px; padding: 2.5rem 2rem; 31 width: 100%; max-width: 380px; box-shadow: 0 8px 40px rgba(0,0,0,.3); 32 } 33 .login-card .logo { 34 display: flex; align-items: center; gap: .6rem; 35 margin-bottom: 2rem; justify-content: center; 36 } 37 .login-card .logo img { width: 36px; height: 36px; } 38 .login-card .logo span { font-size: 1.1rem; font-weight: 700; color: var(--dark); letter-spacing: .04em; } 39 .login-card h2 { font-size: 1.25rem; font-weight: 700; color: var(--dark); margin-bottom: .25rem; text-align:center; } 40 .login-card p { font-size: .85rem; color: #888; margin-bottom: 1.5rem; text-align:center; } 41 .field { margin-bottom: 1rem; } 42 .field label { display: block; font-size: .8rem; font-weight: 600; color: var(--mid); margin-bottom: .35rem; } 43 .field input { 44 width: 100%; padding: .65rem .85rem; border: 1.5px solid #ddd; 45 border-radius: var(--radius); font-size: .9rem; outline: none; transition: border-color .2s; 46 } 47 .field input:focus { border-color: var(--gold); } 48 .btn-primary { 49 width: 100%; padding: .75rem; background: var(--gold); color: var(--dark); 50 font-weight: 700; font-size: .95rem; border: none; border-radius: var(--radius); 51 cursor: pointer; transition: opacity .2s; 52 } 53 .btn-primary:hover { opacity: .88; } 54 .btn-primary:disabled { opacity: .5; cursor: not-allowed; } 55 #login-error { color: var(--red); font-size: .82rem; margin-top: .75rem; text-align: center; min-height: 1.2em; } 56 57 /* ââ ADMIN SHELL ââ */ 58 #admin-shell { display: none; min-height: 100vh; } 59 .topbar { 60 background: var(--dark); color: var(--off); height: 56px; 61 display: flex; align-items: center; justify-content: space-between; 62 padding: 0 1.5rem; position: sticky; top: 0; z-index: 100; box-shadow: var(--shadow); 63 } 64 .topbar-left { display: flex; align-items: center; gap: .75rem; } 65 .topbar-left img { width: 28px; height: 28px; } 66 .topbar-left span { font-weight: 700; font-size: .95rem; letter-spacing: .04em; } 67 .topbar-right { display: flex; align-items: center; gap: 1rem; } 68 .topbar-right span { font-size: .82rem; opacity: .75; } 69 .btn-sm { 70 padding: .38rem .9rem; border-radius: 6px; font-size: .82rem; 71 font-weight: 600; cursor: pointer; border: none; transition: opacity .2s; 72 } 73 .btn-sm:hover { opacity: .8; } 74 .btn-outline { background: transparent; color: var(--off); border: 1.5px solid rgba(255,255,255,.3); } 75 .btn-gold { background: var(--gold); color: var(--dark); } 76 .btn-danger { background: var(--red); color: white; } 77 .btn-success { background: var(--green); color: white; } 78 79 .main { display: flex; gap: 0; min-height: calc(100vh - 56px); } 80 81 /* ââ SIDEBAR ââ */ 82 .sidebar { 83 width: 260px; min-width: 260px; background: white; 84 border-right: 1px solid #e5e1da; padding: 1.25rem 0; overflow-y: auto; 85 } 86 .sidebar-section { padding: 0 1rem 1rem; } 87 .sidebar-section h4 { 88 font-size: .7rem; font-weight: 700; text-transform: uppercase; letter-spacing: .1em; 89 color: #aaa; margin-bottom: .6rem; padding: 0 .5rem; 90 } 91 .post-item { 92 padding: .6rem .75rem; border-radius: 6px; cursor: pointer; 93 display: flex; flex-direction: column; gap: .2rem; margin-bottom: .25rem; 94 border: 1.5px solid transparent; transition: background .15s, border-color .15s; 95 } 96 .post-item:hover { background: #f5f2ed; } 97 .post-item.active { background: #fdf8ef; border-color: var(--gold); } 98 .post-item-title { font-size: .88rem; font-weight: 600; color: var(--dark); line-height: 1.3; } 99 .post-item-meta { font-size: .73rem; color: #aaa; } 100 .badge { 101 display: inline-block; padding: .15rem .45rem; border-radius: 4px; 102 font-size: .68rem; font-weight: 700; letter-spacing: .04em; 103 } 104 .badge-pub { background: #d4f0e0; color: #1a7a4a; } 105 .badge-draft { background: #f5e9d0; color: #996633; } 106 .btn-new-post { 107 width: calc(100% - 2rem); margin: 0 1rem 1rem; padding: .6rem; 108 background: var(--dark); color: var(--off); font-weight: 700; font-size: .85rem; 109 border: none; border-radius: 8px; cursor: pointer; display: flex; align-items: center; 110 justify-content: center; gap: .4rem; transition: opacity .2s; 111 } 112 .btn-new-post:hover { opacity: .85; } 113 114 /* ââ EDITOR AREA ââ */ 115 .editor-area { 116 flex: 1; padding: 2rem; overflow-y: auto; display: flex; flex-direction: column; gap: 1.25rem; 117 } 118 .editor-card { 119 background: white; border-radius: var(--radius); padding: 1.5rem; 120 box-shadow: var(--shadow); 121 } 122 .editor-card h3 { font-size: .8rem; text-transform: uppercase; letter-spacing: .08em; color: #aaa; margin-bottom: 1rem; } 123 .editor-row { display: flex; gap: 1rem; } 124 .editor-col { flex: 1; } 125 .editor-field { margin-bottom: 1rem; } 126 .editor-field label { display: block; font-size: .8rem; font-weight: 600; color: var(--mid); margin-bottom: .35rem; } 127 .editor-field input, 128 .editor-field textarea,
129 .editor-field select { 130 width: 100%; padding: .65rem .85rem; border: 1.5px solid #ddd; 131 border-radius: var(--radius); font-size: .9rem; outline: none; 132 transition: border-color .2s; font-family: inherit; resize: vertical; 133 } 134 .editor-field input:focus, 135 .editor-field textarea:focus { border-color: var(--gold); } 136 .slug-hint { font-size: .73rem; color: #999; margin-top: .3rem; } 137 138 /* Quill */ 139 #quill-editor { min-height: 280px; font-size: .95rem; } 140 .ql-container { border-radius: 0 0 var(--radius) var(--radius); border-color: #ddd !important; } 141 .ql-toolbar { border-radius: var(--radius) var(--radius) 0 0 !important; border-color: #ddd !important; background: #faf9f6; } 142 143 /* Cover preview */ 144 .cover-preview { 145 width: 100%; height: 160px; object-fit: cover; 146 border-radius: var(--radius); margin-top: .5rem; display: none; 147 border: 1.5px solid #e5e1da; 148 } 149 150 /* Upload area */ 151 .upload-area { 152 border: 2px dashed #ddd; border-radius: var(--radius); 153 padding: 1.25rem 1rem; text-align: center; cursor: pointer; 154 transition: border-color .2s, background .2s; margin-top: .35rem; 155 background: #faf9f6; 156 } 157 .upload-area:hover, .upload-area.drag-over { border-color: var(--gold); background: #fdf8ef; } 158 .upload-area-icon { font-size: 1.5rem; margin-bottom: .35rem; } 159 .upload-area p { font-size: .8rem; color: #aaa; margin: 0; } 160 .upload-area p strong { color: var(--gold); } 161 .upload-area input[type="file"] { display: none; } 162 .upload-row { display: flex; gap: .5rem; align-items: center; margin-top: .5rem; } 163 .upload-row input { flex: 1; } 164 .btn-upload { 165 padding: .62rem .9rem; background: var(--dark); color: var(--off); 166 border: none; border-radius: var(--radius); font-size: .8rem; font-weight: 600; 167 cursor: pointer; white-space: nowrap; transition: opacity .2s; display: flex; 168 align-items: center; gap: .35rem; 169 } 170 .btn-upload:hover { opacity: .85; } 171 .btn-upload:disabled { opacity: .5; cursor: not-allowed; } 172 .upload-progress { 173 height: 3px; background: #e5e1da; border-radius: 2px; margin-top: .5rem; overflow: hidden; display: none; 174 } 175 .upload-progress-bar { height: 100%; background: var(--gold); width: 0%; transition: width .3s; } 176 177 /* Action bar */ 178 .action-bar { 179 display: flex; gap: .75rem; align-items: center; justify-content: space-between; 180 background: white; border-radius: var(--radius); padding: 1rem 1.5rem; 181 box-shadow: var(--shadow); position: sticky; bottom: 1.5rem; 182 } 183 .action-bar-left { display: flex; gap: .5rem; align-items: center; } 184 #save-status { font-size: .82rem; color: #aaa; } 185 186 /* Empty state */ 187 .empty-state { 188 flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: center; 189 gap: 1rem; color: #aaa; padding: 3rem; 190 } 191 .empty-state svg { opacity: .35; } 192 .empty-state p { font-size: .95rem; } 193 194 /* Toast */ 195 #toast { 196 position: fixed; bottom: 2rem; right: 2rem; padding: .85rem 1.25rem; 197 border-radius: var(--radius); font-size: .88rem; font-weight: 600; 198 box-shadow: 0 4px 20px rgba(0,0,0,.2); z-index: 9999; 199 transform: translateY(20px); opacity: 0; transition: all .3s; 200 pointer-events: none; 201 } 202 #toast.show { transform: translateY(0); opacity: 1; } 203 #toast.success { background: var(--green); color: white; } 204 #toast.error { background: var(--red); color: white; } 205 206 /* Confirm modal */ 207 #modal-backdrop { 208 display: none; position: fixed; inset: 0; background: rgba(0,0,0,.45); 209 z-index: 5000; align-items: center; justify-content: center; 210 } 211 #modal-backdrop.open { display: flex; } 212 .modal-box { 213 background: white; border-radius: 12px; padding: 2rem; max-width: 380px; width: 90%; 214 box-shadow: 0 8px 40px rgba(0,0,0,.3); 215 } 216 .modal-box h3 { font-size: 1rem; font-weight: 700; margin-bottom: .5rem; color: var(--dark); } 217 .modal-box p { font-size: .88rem; color: #777; margin-bottom: 1.5rem; } 218 .modal-btns { display: flex; gap: .75rem; justify-content: flex-end; } 219 220 /* ââ ADMIN TABS ââ */ 221 .admin-tabs { 222 display: flex; border-bottom: 1.5px solid #e5e1da; 223 margin: 0 1rem 1rem; gap: 0; 224 } 225 .admin-tab { 226 flex: 1; padding: .6rem; text-align: center; font-size: .78rem; 227 font-weight: 700; letter-spacing: .06em; text-transform: uppercase; 228 color: #aaa; cursor: pointer; border-bottom: 2.5px solid transparent; 229 transition: color .2s, border-color .2s; 230 } 231 .admin-tab:hover { color: var(--mid); } 232 .admin-tab.active { color: var(--gold); border-color: var(--gold); } 233 234 .admin-panel { display: none; } 235 .admin-panel.active { display: block; } 236 237 /* Evento list item */ 238 .evento-item { 239 padding: .6rem .75rem; border-radius: 6px; cursor: pointer; 240 display: flex; flex-direction: column; gap: .2rem; margin-bottom: .25rem; 241 border: 1.5px solid transparent; transition: background .15s, border-color .15s; 242 } 243 .evento-item:hover { background: #f5f2ed; } 244 .evento-item.active { background: #fdf8ef; border-color: var(--gold); } 245 .evento-item-title { font-size: .88rem; font-weight: 600; color: var(--dark); line-height: 1.3; } 246 .evento-item-meta { font-size: .73rem; color: #aaa; } 247 248 @media (max-width: 700px) { 249 .sidebar { display: none; } 250 .editor-row { flex-direction: column; } 251 } 252</style> 253</head> 254<body> 255 256<!-- LOGIN --> 257<div id="login-screen"> 258 <div class="login-card"> 259 <div class="logo"> 260 <img src="logo.png" alt="Grupo Trinta" onerror="this.style.display='none'" />
261 <span>GRUPO TRINTA</span> 262 </div> 263 <h2>Painel Admin</h2> 264 <p>Acesso restrito à equipe interna</p> 265 <div class="field"> 266 <label>E-mail</label> 267 <input type="email" id="login-email" placeholder="[email protected]" autocomplete="email" /> 268 </div> 269 <div class="field"> 270 <label>Senha</label> 271 <input type="password" id="login-password" placeholder="â¢â¢â¢â¢â¢â¢â¢â¢" autocomplete="current-password" /> 272 </div> 273 <button class="btn-primary" id="login-btn">Entrar</button> 274 <div id="login-error"></div> 275 </div> 276</div> 277 278<!-- ADMIN SHELL --> 279<div id="admin-shell"> 280 <div class="topbar"> 281 <div class="topbar-left"> 282 <img src="logo.png" alt="GT" onerror="this.style.display='none'" /> 283 <span>GRUPO TRINTA â Admin</span> 284 </div> 285 <div class="topbar-right"> 286 <span id="user-email-display"></span> 287 <button class="btn-sm btn-outline" id="logout-btn">Sair</button> 288 </div> 289 </div> 290 291 <div class="main"> 292 <!-- SIDEBAR --> 293 <aside class="sidebar"> 294 <div class="admin-tabs"> 295 <div class="admin-tab active" data-tab="blog">Blog</div> 296 <div class="admin-tab" data-tab="eventos">Eventos</div> 297 </div> 298 299 <!-- BLOG PANEL -->
300 <div class="admin-panel active" id="panel-blog"> 301 <button class="btn-new-post" id="btn-new-post"> 302 <svg width="14" height="14" viewBox="0 0 14 14" fill="none"><path d="M7 1v12M1 7h12" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"/></svg> 303 Novo Post 304 </button> 305 <div class="sidebar-section"> 306 <h4>Posts</h4> 307 <div id="posts-list"></div> 308 </div> 309 </div> 310 311 <!-- EVENTOS PANEL --> 312 <div class="admin-panel" id="panel-eventos"> 313 <button class="btn-new-post" id="btn-new-evento"> 314 <svg width="14" height="14" viewBox="0 0 14 14" fill="none"><path d="M7 1v12M1 7h12" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"/></svg> 315 Novo Evento 316 </button> 317 <div class="sidebar-section"> 318 <h4>Eventos</h4> 319 <div id="eventos-list"></div> 320 </div> 321 </div> 322 </aside> 323 324 <!-- EDITOR --> 325 <div class="editor-area" id="editor-area"> 326 <div class="empty-state" id="empty-state"> 327 <svg width="64" height="64" viewBox="0 0 64 64" fill="none"> 328 <rect x="8" y="12" width="48" height="40" rx="4" stroke="#ccc" stroke-width="2.5"/> 329 <path d="M20 24h24M20 32h16M20 40h20" stroke="#ccc" stroke-width="2" stroke-linecap="round"/> 330 </svg> 331 <p>Selecione um post ou crie um novo</p> 332 </div> 333 334 <!-- EVENTO EDITOR --> 335 <div id="evento-form" style="display:none; flex-direction:column; gap:1.25rem;"> 336 <div class="editor-card"> 337 <h3>Informações do Evento</h3> 338 <div class="editor-row"> 339 <div class="editor-col"> 340 <div class="editor-field"> 341 <label>Nome do Evento *</label> 342 <input type="text" id="ev-nome" placeholder="Ex: Jantar de Negócios â Edição BH" /> 343 </div> 344 <div class="editor-field"> 345 <label>Data *</label> 346 <input type="date" id="ev-data" /> 347 </div> 348 <div class="editor-field"> 349 <label>Local</label> 350 <input type="text" id="ev-local" placeholder="Ex: Restaurante Villa Trinta, BH" /> 351 </div> 352 </div> 353 <div class="editor-col"> 354 <div class="editor-field"> 355 <label>Descrição</label> 356 <textarea id="ev-desc" rows="4" placeholder="Breve descrição do evento"></textarea> 357 </div> 358 <div class="editor-field"> 359 <label>Imagem do Evento</label> 360 <div class="upload-area" id="ev-upload-area" onclick="document.getElementById('ev-file-input').click()"> 361 <input type="file" id="ev-file-input" accept="image/*" /> 362 <div class="upload-area-icon">ð¸</div> 363 <p><strong>Clique para escolher</strong> ou arraste</p> 364 <p>JPG, PNG, WEBP â máx. 5 MB</p> 365 </div> 366 <div class="upload-row"> 367 <input type="url" id="ev-image" placeholder="Ou cole a URL da imagemâ¦" /> 368 </div> 369 <img id="ev-img-preview" class="cover-preview" alt="Preview" /> 370 </div> 371 </div> 372 </div> 373 </div> 374 <div class="action-bar"> 375 <div class="action-bar-left"> 376 <span id="ev-save-status"></span> 377 </div> 378 <div style="display:flex;gap:.75rem;"> 379 <button class="btn-sm btn-danger" id="ev-btn-delete" style="display:none;">Excluir</button> 380 <button class="btn-sm btn-gold" id="ev-btn-update" style="display:none;">Atualizar</button> 381 <button class="btn-sm btn-outline" id="ev-btn-draft" style="color:var(--mid);border-color:#ccc;">Salvar oculto</button> 382 <button class="btn-sm btn-success" id="ev-btn-publish">Publicar</button> 383 </div> 384 </div> 385 </div> 386 387 <div id="editor-form" style="display:none; flex-direction:column; gap:1.25rem;"> 388 <!-- Meta --> 389 <div class="editor-card"> 390 <h3>Informações do Post</h3> 391 <div class="editor-row"> 392 <div class="editor-col"> 393 <div class="editor-field"> 394 <label>TÃtulo *</label> 395 <input type="text" id="f-title" placeholder="TÃtulo do post" /> 396 </div> 397 <div class="editor-field"> 398 <label>Slug (URL)</label> 399 <input type="text" id="f-slug" placeholder="titulo-do-post" /> 400 <div class="slug-hint">Gerado automaticamente â pode editar</div> 401 </div> 402 <div class="editor-field"> 403 <label>Resumo</label> 404 <textarea id="f-summary" rows="3" placeholder="Breve descrição para listagens e SEO"></textarea> 405 </div> 406 </div> 407 <div class="editor-col"> 408 <div class="editor-field"> 409 <label>Autor</label> 410 <input type="text" id="f-author" value="Grupo Trinta" /> 411 </div> 412 <div class="editor-field"> 413 <label>Tags</label> 414 <input type="text" id="f-tags" placeholder="gestao, financas, networking, tributario" /> 415 <div class="slug-hint">Separe por vÃrgula. Usadas nos filtros do blog.</div> 416 </div> 417 <div class="editor-field"> 418 <label>Imagem de capa</label> 419 <div class="upload-area" id="upload-area" onclick="document.getElementById('cover-file-input').click()"> 420 <input type="file" id="cover-file-input" accept="image/*" /> 421 <div class="upload-area-icon">ð¼ï¸</div> 422 <p><strong>Clique para escolher</strong> ou arraste uma imagem aqui</p> 423 <p>JPG, PNG, WEBP â máx. 5 MB</p> 424 </div> 425 <div class="upload-progress" id="upload-progress"> 426 <div class="upload-progress-bar" id="upload-progress-bar"></div> 427 </div> 428 <div class="upload-row"> 429 <input type="url" id="f-cover" placeholder="Ou cole a URL diretamenteâ¦" /> 430 </div> 431 <img id="cover-preview" class="cover-preview" alt="Preview da capa" /> 432 </div> 433 </div> 434 </div> 435 </div> 436 437 <!-- Content --> 438 <div class="editor-card"> 439 <h3>Conteúdo</h3> 440 <div id="quill-editor"></div> 441 </div> 442 443 <!-- Action bar --> 444 <div class="action-bar"> 445 <div class="action-bar-left">
446 <span id="save-status"></span> 447 </div> 448 <div style="display:flex;gap:.75rem;"> 449 <button class="btn-sm btn-danger" id="btn-delete" style="display:none;">Excluir</button> 450 <button class="btn-sm btn-gold" id="btn-update" style="display:none;">Atualizar</button> 451 <button class="btn-sm btn-outline" id="btn-save-draft" style="color:var(--mid);border-color:#ccc;">Salvar rascunho</button> 452 <button class="btn-sm btn-success" id="btn-publish">Publicar</button> 453 </div> 454 </div> 455 </div> 456 </div> 457 </div> 458</div> 459 460<!-- Toast --> 461<div id="toast"></div> 462 463<!-- Confirm modal --> 464<div id="modal-backdrop"> 465 <div class="modal-box"> 466 <h3 id="modal-title">Confirmar ação</h3> 467 <p id="modal-msg">Tem certeza?</p> 468 <div class="modal-btns"> 469 <button class="btn-sm btn-outline" id="modal-cancel" style="color:var(--mid);border-color:#ccc;">Cancelar</button> 470 <button class="btn-sm btn-danger" id="modal-confirm">Confirmar</button> 471 </div> 472 </div> 473</div> 474
475<script> 476const SUPABASE_URL = 'https://nelfqvontppdrwwqontq.supabase.co'; 477const SUPABASE_KEY = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Im5lbGZxdm9udHBwZHJ3d3FvbnRxIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NzkyOTE4NDAsImV4cCI6MjA5NDg2Nzg0MH0.p7ohsTybOCze8NIZAfmpo4WwsP8caJ25gtHarL9hjgE'; 478 479const { createClient } = supabase; 480const sb = createClient(SUPABASE_URL, SUPABASE_KEY); 481 482let quill; 483let currentPostId = null; 484let allPosts = []; 485 486// ââ UTILS ââââââââââââââââââââââââââââââââââââââââââââââ 487function slugify(str) { 488 return str.toLowerCase() 489 .normalize('NFD').replace(/[\u0300-\u036f]/g, '') 490 .replace(/[^a-z0-9\s-]/g, '') 491 .trim().replace(/\s+/g, '-'); 492} 493 494function toast(msg, type = 'success') { 495 const el = document.getElementById('toast'); 496 el.textContent = msg; 497 el.className = `show ${type}`; 498 setTimeout(() => { el.className = ''; }, 3000); 499} 500 501function confirm(title, msg) { 502 return new Promise(resolve => { 503 document.getElementById('modal-title').textContent = title; 504 document.getElementById('modal-msg').textContent = msg; 505 const backdrop = document.getElementById('modal-backdrop'); 506 backdrop.classList.add('open'); 507 const yes = document.getElementById('modal-confirm'); 508 const no = document.getElementById('modal-cancel'); 509 const cleanup = () => { backdrop.classList.remove('open'); yes.onclick = null; no.onclick = null; }; 510 yes.onclick = () => { cleanup(); resolve(true); }; 511 no.onclick = () => { cleanup(); resolve(false); }; 512 }); 513} 514 515function setSaveStatus(msg) { 516 document.getElementById('save-status').textContent = msg; 517} 518 519// ââ AUTH âââââââââââââââââââââââââââââââââââââââââââââââ 520document.getElementById('login-btn').addEventListener('click', async () => { 521 const email = document.getElementById('login-email').value.trim(); 522 const password = document.getElementById('login-password').value; 523 const btn = document.getElementById('login-btn'); 524 const errEl = document.getElementById('login-error'); 525 errEl.textContent = ''; 526 btn.disabled = true; 527 btn.textContent = 'Entrandoâ¦'; 528 529 const { error } = await sb.auth.signInWithPassword({ email, password }); 530 if (error) { 531 errEl.textContent = 'E-mail ou senha inválidos.'; 532 btn.disabled = false; 533 btn.textContent = 'Entrar'; 534 } 535}); 536 537document.getElementById('login-password').addEventListener('keydown', e => { 538 if (e.key === 'Enter') document.getElementById('login-btn').click(); 539}); 540 541document.getElementById('logout-btn').addEventListener('click', async () => { 542 await sb.auth.signOut(); 543}); 544 545sb.auth.onAuthStateChange((event, session) => { 546 if (session) { 547 document.getElementById('login-screen').style.display = 'none'; 548 document.getElementById('admin-shell').style.display = 'block'; 549 document.getElementById('user-email-display').textContent = session.user.email; 550 initAdmin(); 551 } else { 552 document.getElementById('login-screen').style.display = 'flex'; 553 document.getElementById('admin-shell').style.display = 'none'; 554 } 555}); 556 557// ââ INIT âââââââââââââââââââââââââââââââââââââââââââââââ 558function initAdmin() { 559 if (!quill) { 560 quill = new Quill('#quill-editor', { 561 theme: 'snow', 562 placeholder: 'Escreva o conteúdo do post aquiâ¦', 563 modules: { 564 toolbar: { 565 container: [ 566 [{ header: [2, 3, false] }], 567 ['bold', 'italic', 'underline'], 568 ['link', 'blockquote', 'code-block'], 569 [{ list: 'ordered' }, { list: 'bullet' }], 570 ['image'], 571 ['clean'] 572 ], 573 handlers: { 574 image: function() { 575 const input = document.createElement('input'); 576 input.type = 'file'; 577 input.accept = 'image/*'; 578 input.click(); 579 input.onchange = async () => { 580 const file = input.files[0]; 581 if (!file) return; 582 if (file.size > 5 * 1024 * 1024) { toast('Imagem muito grande (máx. 5 MB).', 'error'); return; } 583 toast('Enviando imagemâ¦'); 584 const ext = file.name.split('.').pop(); 585 const name = `content-${Date.now()}.${ext}`; 586 const { data, error } = await sb.storage 587 .from('blog-images') 588 .upload(name, file, { upsert: false, contentType: file.type }); 589 if (error) { toast('Erro no upload da imagem.', 'error'); return; } 590 const { data: urlData } = sb.storage.from('blog-images').getPublicUrl(data.path); 591 const range = quill.getSelection(true); 592 quill.insertEmbed(range.index, 'image', urlData.publicUrl); 593 quill.setSelection(range.index + 1); 594 toast('Imagem inserida!'); 595 }; 596 } 597 } 598 } 599 } 600 }); 601 602 // Bloquear paste de imagens base64 â redireciona para upload 603 quill.root.addEventListener('paste', async (e) => { 604 const items = e.clipboardData && e.clipboardData.items; 605 if (!items) return; 606 for (const item of items) { 607 if (item.type.startsWith('image/')) { 608 e.preventDefault(); 609 const file = item.getAsFile(); 610 if (!file) continue; 611 toast('Enviando imagem coladaâ¦'); 612 const ext = 'png'; 613 const name = `content-${Date.now()}.${ext}`; 614 const { data, error } = await sb.storage 615 .from('blog-images') 616 .upload(name, file, { upsert: false, contentType: file.type }); 617 if (error) { toast('Erro no upload.', 'error'); return; } 618 const { data: urlData } = sb.storage.from('blog-images').getPublicUrl(data.path); 619 const range = quill.getSelection(true); 620 quill.insertEmbed(range ? range.index : 0, 'image', urlData.publicUrl); 621 toast('Imagem inserida!'); 622 } 623 } 624 }); 625 } 626 loadPosts(); 627} 628 629// ââ POSTS LIST âââââââââââââââââââââââââââââââââââââââââ 630async function loadPosts() { 631 const { data, error } = await sb 632 .from('posts') 633 .select('id,title,slug,published,created_at') 634 .order('created_at', { ascending: false }); 635 636 if (error) { toast('Erro ao carregar posts.', 'error'); return; } 637 allPosts = data || []; 638 renderPostsList(); 639} 640 641function renderPostsList() { 642 const container = document.getElementById('posts-list'); 643 if (!allPosts.length) { 644 container.innerHTML = '<div style="padding:.5rem;font-size:.8rem;color:#aaa;">Nenhum post ainda.</div>'; 645 return; 646 } 647 container.innerHTML = allPosts.map(p => ` 648 <div class="post-item ${p.id === currentPostId ? 'active' : ''}" data-id="${p.id}">
649 <span class="post-item-title">${p.title || '(sem tÃtulo)'}</span> 650 <span class="post-item-meta"> 651 <span class="badge ${p.published ? 'badge-pub' : 'badge-draft'}">${p.published ? 'Publicado' : 'Rascunho'}</span> 652 ${new Date(p.created_at).toLocaleDateString('pt-BR')} 653 </span> 654 </div> 655 `).join(''); 656 657 container.querySelectorAll('.post-item').forEach(el => { 658 el.addEventListener('click', () => openPost(el.dataset.id)); 659 }); 660} 661 662// ââ OPEN / NEW POST ââââââââââââââââââââââââââââââââââââ 663async function openPost(id) { 664 const { data, error } = await sb.from('posts').select('*').eq('id', id).single(); 665 if (error) { toast('Erro ao carregar post.', 'error'); return; } 666 currentPostId = id; 667 fillForm(data); 668 showEditor(); 669 renderPostsList(); 670} 671 672function fillForm(post) { 673 document.getElementById('f-title').value = post.title || ''; 674 document.getElementById('f-slug').value = post.slug || ''; 675 document.getElementById('f-summary').value = post.summary || ''; 676 document.getElementById('f-author').value = post.author || 'Grupo Trinta'; 677 document.getElementById('f-tags').value = (post.tags || []).join(', '); 678 document.getElementById('f-cover').value = post.cover_url || ''; 679 updateCoverPreview(post.cover_url || ''); 680 quill.root.innerHTML = post.content || ''; 681 document.getElementById('btn-delete').style.display = 'inline-flex'; 682 document.getElementById('btn-update').style.display = 'inline-flex'; 683 document.getElementById('btn-publish').textContent = post.published ? 'Despublicar' : 'Publicar'; 684 setSaveStatus(post.published ? 'Publicado' : 'Rascunho'); 685} 686 687function newPostForm() { 688 currentPostId = null; 689 document.getElementById('f-title').value = ''; 690 document.getElementById('f-slug').value = ''; 691 document.getElementById('f-summary').value = ''; 692 document.getElementById('f-author').value = 'Grupo Trinta'; 693 document.getElementById('f-tags').value = ''; 694 document.getElementById('f-cover').value = ''; 695 updateCoverPreview(''); 696 quill.setText(''); 697 document.getElementById('btn-delete').style.display = 'none'; 698 document.getElementById('btn-update').style.display = 'none'; 699 document.getElementById('btn-publish').textContent = 'Publicar'; 700 setSaveStatus('Novo post'); 701 showEditor(); 702 renderPostsList(); 703} 704 705function showEditor() { 706 document.getElementById('empty-state').style.display = 'none'; 707 document.getElementById('editor-form').style.display = 'flex'; 708} 709 710// ââ COVER PREVIEW ââââââââââââââââââââââââââââââââââââââ 711function updateCoverPreview(url) { 712 const img = document.getElementById('cover-preview'); 713 if (url && url.startsWith('http')) { 714 img.src = url; img.style.display = 'block'; 715 } else { 716 img.style.display = 'none'; 717 } 718} 719document.getElementById('f-cover').addEventListener('input', e => updateCoverPreview(e.target.value)); 720 721// ââ SLUG AUTO-GEN ââââââââââââââââââââââââââââââââââââââ 722document.getElementById('f-title').addEventListener('input', e => { 723 if (!currentPostId) { 724 document.getElementById('f-slug').value = slugify(e.target.value); 725 } 726}); 727 728// ââ SAVE âââââââââââââââââââââââââââââââââââââââââââââââ 729async function savePost(publish = null) { 730 const title = document.getElementById('f-title').value.trim(); 731 const slug = document.getElementById('f-slug').value.trim() || slugify(title); 732 const summary = document.getElementById('f-summary').value.trim(); 733 const author = document.getElementById('f-author').value.trim() || 'Grupo Trinta'; 734 const tags = document.getElementById('f-tags').value.split(',').map(t => t.trim().toLowerCase()).filter(Boolean); 735 const cover_url = document.getElementById('f-cover').value.trim(); 736 const content = quill.root.innerHTML; 737 738 if (!title) { toast('O tÃtulo é obrigatório.', 'error'); return; } 739 if (!slug) { toast('O slug é obrigatório.', 'error'); return; } 740 741 setSaveStatus('Salvandoâ¦'); 742 743 const payload = { title, slug, summary, author, tags, cover_url, content }; 744 if (publish !== null) payload.published = publish; 745 746 let error; 747 if (currentPostId) { 748 ({ error } = await sb.from('posts').update(payload).eq('id', currentPostId)); 749 } else { 750 if (publish === null) payload.published = false;
751 const { data, error: e } = await sb.from('posts').insert(payload).select().single(); 752 error = e; 753 if (!error && data) { 754 currentPostId = data.id; 755 document.getElementById('btn-delete').style.display = 'inline-flex'; 756 } 757 } 758 759 if (error) { 760 toast(error.message || 'Erro ao salvar.', 'error'); 761 setSaveStatus('Erro ao salvar'); 762 return; 763 } 764 765 const published = publish !== null 766 ? publish 767 : (allPosts.find(p => p.id === currentPostId)?.published ?? false); 768 769 toast(publish === true ? 'Post publicado!' : publish === false ? 'Rascunho salvo.' : 'Salvo!'); 770 setSaveStatus(published ? 'Publicado' : 'Rascunho'); 771 document.getElementById('btn-publish').textContent = published ? 'Despublicar' : 'Publicar'; 772 await loadPosts(); 773} 774 775// ââ DELETE âââââââââââââââââââââââââââââââââââââââââââââ 776async function deletePost() { 777 const ok = await confirm('Excluir post', 'Esta ação não pode ser desfeita. Deseja continuar?'); 778 if (!ok) return; 779 const { error } = await sb.from('posts').delete().eq('id', currentPostId); 780 if (error) { toast('Erro ao excluir.', 'error'); return; } 781 toast('Post excluÃdo.'); 782 currentPostId = null; 783 document.getElementById('empty-state').style.display = 'flex'; 784 document.getElementById('editor-form').style.display = 'none'; 785 await loadPosts(); 786} 787 788// ââ IMAGE UPLOAD âââââââââââââââââââââââââââââââââââââââ 789const STORAGE_BUCKET = 'blog-images'; 790 791async function uploadCoverImage(file) { 792 if (!file) return; 793 if (file.size > 5 * 1024 * 1024) { toast('Imagem muito grande (máx. 5 MB).', 'error'); return; } 794 795 const area = document.getElementById('upload-area'); 796 const prog = document.getElementById('upload-progress'); 797 const bar = document.getElementById('upload-progress-bar'); 798 const fileInput = document.getElementById('cover-file-input'); 799 800 area.style.pointerEvents = 'none'; 801 area.style.opacity = '.6'; 802 prog.style.display = 'block'; 803 bar.style.width = '20%'; 804 805 const ext = file.name.split('.').pop(); 806 const name = `cover-${Date.now()}.${ext}`; 807 808 bar.style.width = '50%'; 809 810 const { data, error } = await sb.storage 811 .from(STORAGE_BUCKET) 812 .upload(name, file, { upsert: false, contentType: file.type }); 813 814 bar.style.width = '80%'; 815 816 if (error) { 817 toast('Erro no upload: ' + (error.message || 'verifique o bucket no Supabase.'), 'error'); 818 area.style.pointerEvents = ''; 819 area.style.opacity = ''; 820 prog.style.display = 'none'; 821 bar.style.width = '0%'; 822 fileInput.value = ''; 823 return; 824 } 825 826 const { data: urlData } = sb.storage.from(STORAGE_BUCKET).getPublicUrl(data.path); 827 const publicUrl = urlData.publicUrl; 828 829 document.getElementById('f-cover').value = publicUrl; 830 updateCoverPreview(publicUrl); 831 bar.style.width = '100%'; 832 setTimeout(() => { 833 prog.style.display = 'none'; 834 bar.style.width = '0%'; 835 area.style.pointerEvents = ''; 836 area.style.opacity = ''; 837 fileInput.value = ''; 838 }, 600); 839 toast('Imagem enviada!'); 840} 841 842document.getElementById('cover-file-input').addEventListener('change', e => { 843 if (e.target.files[0]) uploadCoverImage(e.target.files[0]); 844}); 845 846const uploadArea = document.getElementById('upload-area'); 847uploadArea.addEventListener('dragover', e => { e.preventDefault(); uploadArea.classList.add('drag-over'); }); 848uploadArea.addEventListener('dragleave', () => uploadArea.classList.remove('drag-over')); 849uploadArea.addEventListener('drop', e => { 850 e.preventDefault(); 851 uploadArea.classList.remove('drag-over'); 852 const file = e.dataTransfer.files[0]; 853 if (file && file.type.startsWith('image/')) uploadCoverImage(file); 854}); 855 856// ââ PUBLISH TOGGLE âââââââââââââââââââââââââââââââââââââ 857document.getElementById('btn-publish').addEventListener('click', async () => { 858 const post = allPosts.find(p => p.id === currentPostId); 859 const isPublished = post?.published ?? false; 860 await savePost(!isPublished); 861}); 862 863document.getElementById('btn-update').addEventListener('click', () => savePost(null)); 864document.getElementById('btn-save-draft').addEventListener('click', () => savePost(false)); 865document.getElementById('btn-delete').addEventListener('click', deletePost); 866document.getElementById('btn-new-post').addEventListener('click', newPostForm); 867 868// ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 869// ADMIN TABS 870// ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 871document.querySelectorAll('.admin-tab').forEach(tab => { 872 tab.addEventListener('click', () => { 873 document.querySelectorAll('.admin-tab').forEach(t => t.classList.remove('active')); 874 document.querySelectorAll('.admin-panel').forEach(p => p.classList.remove('active')); 875 tab.classList.add('active'); 876 document.getElementById('panel-' + tab.dataset.tab).classList.add('active'); 877 // Show correct editor 878 if (tab.dataset.tab === 'eventos') { 879 document.getElementById('editor-form').style.display = 'none'; 880 document.getElementById('empty-state').style.display = 'flex'; 881 document.getElementById('evento-form').style.display = 'none'; 882 loadEventos(); 883 } else { 884 document.getElementById('evento-form').style.display = 'none'; 885 document.getElementById('empty-state').style.display = 'flex'; 886 } 887 }); 888}); 889 890// ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 891// EVENTOS MANAGEMENT 892// ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 893let allEventos = []; 894let currentEventoId = null; 895 896async function loadEventos() { 897 const { data, error } = await sb 898 .from('eventos') 899 .select('id,nome,data,local,publicado,created_at') 900 .order('data', { ascending: false }); 901 902 if (error) { toast('Erro ao carregar eventos.', 'error'); return; } 903 allEventos = data || []; 904 renderEventosList(); 905} 906 907function renderEventosList() { 908 const container = document.getElementById('eventos-list'); 909 if (!allEventos.length) { 910 container.innerHTML = '<div style="padding:.5rem;font-size:.8rem;color:#aaa;">Nenhum evento ainda.</div>'; 911 return; 912 } 913 container.innerHTML = allEventos.map(ev => ` 914 <div class="evento-item ${ev.id === currentEventoId ? 'active' : ''}" data-id="${ev.id}">
915 <span class="evento-item-title">${ev.nome || '(sem nome)'}</span> 916 <span class="evento-item-meta"> 917 <span class="badge ${ev.publicado ? 'badge-pub' : 'badge-draft'}">${ev.publicado ? 'Publicado' : 'Oculto'}</span> 918 ${ev.data ? new Date(ev.data + 'T12:00:00').toLocaleDateString('pt-BR') : ''} 919 ${ev.local ? ' · ' + ev.local : ''} 920 </span> 921 </div> 922 `).join(''); 923 924 container.querySelectorAll('.evento-item').forEach(el => { 925 el.addEventListener('click', () => openEvento(el.dataset.id)); 926 }); 927} 928 929async function openEvento(id) { 930 const { data, error } = await sb.from('eventos').select('*').eq('id', id).single(); 931 if (error) { toast('Erro ao carregar evento.', 'error'); return; } 932 currentEventoId = id; 933 fillEventoForm(data); 934 showEventoEditor(); 935 renderEventosList(); 936} 937 938function fillEventoForm(ev) { 939 document.getElementById('ev-nome').value = ev.nome || ''; 940 document.getElementById('ev-data').value = ev.data || ''; 941 document.getElementById('ev-local').value = ev.local || ''; 942 document.getElementById('ev-desc').value = ev.descricao || ''; 943 document.getElementById('ev-image').value = ev.image_url || ''; 944 updateEvImagePreview(ev.image_url || ''); 945 document.getElementById('ev-btn-delete').style.display = 'inline-flex'; 946 document.getElementById('ev-btn-update').style.display = 'inline-flex'; 947 document.getElementById('ev-btn-publish').textContent = ev.publicado ? 'Despublicar' : 'Publicar'; 948 document.getElementById('ev-save-status').textContent = ev.publicado ? 'Publicado' : 'Oculto'; 949} 950 951function newEventoForm() { 952 currentEventoId = null; 953 document.getElementById('ev-nome').value = ''; 954 document.getElementById('ev-data').value = ''; 955 document.getElementById('ev-local').value = ''; 956 document.getElementById('ev-desc').value = ''; 957 document.getElementById('ev-image').value = ''; 958 updateEvImagePreview(''); 959 document.getElementById('ev-btn-delete').style.display = 'none'; 960 document.getElementById('ev-btn-update').style.display = 'none'; 961 document.getElementById('ev-btn-publish').textContent = 'Publicar'; 962 document.getElementById('ev-save-status').textContent = 'Novo evento'; 963 showEventoEditor(); 964 renderEventosList(); 965} 966 967function showEventoEditor() { 968 document.getElementById('empty-state').style.display = 'none'; 969 document.getElementById('editor-form').style.display = 'none'; 970 document.getElementById('evento-form').style.display = 'flex'; 971} 972 973function updateEvImagePreview(url) { 974 const img = document.getElementById('ev-img-preview'); 975 if (url && url.startsWith('http')) { img.src = url; img.style.display = 'block'; } 976 else { img.style.display = 'none'; } 977} 978document.getElementById('ev-image').addEventListener('input', e => updateEvImagePreview(e.target.value)); 979 980async function saveEvento(publish = null) { 981 const nome = document.getElementById('ev-nome').value.trim(); 982 const data = document.getElementById('ev-data').value; 983 const local = document.getElementById('ev-local').value.trim(); 984 const descricao = document.getElementById('ev-desc').value.trim(); 985 const image_url = document.getElementById('ev-image').value.trim(); 986 987 if (!nome) { toast('O nome do evento é obrigatório.', 'error'); return; } 988 if (!data) { toast('A data é obrigatória.', 'error'); return; } 989 990 document.getElementById('ev-save-status').textContent = 'Salvandoâ¦'; 991 992 const payload = { nome, data, local, descricao, image_url }; 993 if (publish !== null) payload.publicado = publish; 994 995 let error; 996 if (currentEventoId) { 997 ({ error } = await sb.from('eventos').update(payload).eq('id', currentEventoId)); 998 } else { 999 if (publish === null) payload.publicado = false; 1000 const { data: newEv, error: e } = await sb.from('eventos').insert(payload).select().single(); 1001 error = e; 1002 if (!error && newEv) { 1003 currentEventoId = newEv.id; 1004 document.getElementById('ev-btn-delete').style.display = 'inline-flex'; 1005 document.getElementById('ev-btn-update').style.display = 'inline-flex'; 1006 } 1007 } 1008 1009 if (error) { 1010 toast(error.message || 'Erro ao salvar evento.', 'error'); 1011 document.getElementById('ev-save-status').textContent = 'Erro'; 1012 return; 1013 } 1014 1015 const pub = publish !== null ? publish : (allEventos.find(e => e.id === currentEventoId)?.publicado ?? false); 1016 toast(publish === true ? 'Evento publicado!' : publish === false ? 'Evento salvo (oculto).' : 'Evento salvo!'); 1017 document.getElementById('ev-save-status').textContent = pub ? 'Publicado' : 'Oculto'; 1018 document.getElementById('ev-btn-publish').textContent = pub ? 'Despublicar' : 'Publicar'; 1019 await loadEventos(); 1020} 1021 1022async function deleteEvento() { 1023 const ok = await confirm('Excluir evento', 'Esta ação não pode ser desfeita. Deseja continuar?'); 1024 if (!ok) return; 1025 const { error } = await sb.from('eventos').delete().eq('id', currentEventoId); 1026 if (error) { toast('Erro ao excluir.', 'error'); return; } 1027 toast('Evento excluÃdo.'); 1028 currentEventoId = null; 1029 document.getElementById('evento-form').style.display = 'none'; 1030 document.getElementById('empty-state').style.display = 'flex'; 1031 await loadEventos(); 1032} 1033 1034// Evento image upload 1035document.getElementById('ev-file-input').addEventListener('change', async e => { 1036 const file = e.target.files[0]; 1037 if (!file) return; 1038 if (file.size > 5 * 1024 * 1024) { toast('Imagem muito grande (máx. 5 MB).', 'error'); return; } 1039 toast('Enviando imagemâ¦'); 1040 const ext = file.name.split('.').pop(); 1041 const name = `evento-${Date.now()}.${ext}`; 1042 const { data, error } = await sb.storage.from('blog-images').upload(name, file, { upsert: false, contentType: file.type }); 1043 if (error) { toast('Erro no upload.', 'error'); e.target.value = ''; return; } 1044 const { data: urlData } = sb.storage.from('blog-images').getPublicUrl(data.path); 1045 document.getElementById('ev-image').value = urlData.publicUrl; 1046 updateEvImagePreview(urlData.publicUrl); 1047 e.target.value = ''; 1048 toast('Imagem enviada!'); 1049}); 1050 1051// Evento drag & drop 1052const evUpload = document.getElementById('ev-upload-area'); 1053evUpload.addEventListener('dragover', e => { e.preventDefault(); evUpload.classList.add('drag-over'); }); 1054evUpload.addEventListener('dragleave', () => evUpload.classList.remove('drag-over')); 1055evUpload.addEventListener('drop', e => { 1056 e.preventDefault(); 1057 evUpload.classList.remove('drag-over'); 1058 const file = e.dataTransfer.files[0]; 1059 if (file && file.type.startsWith('image/')) { 1060 const input = document.getElementById('ev-file-input'); 1061 const dt = new DataTransfer(); 1062 dt.items.add(file); 1063 input.files = dt.files; 1064 input.dispatchEvent(new Event('change')); 1065 } 1066}); 1067 1068// Evento button listeners 1069document.getElementById('ev-btn-publish').addEventListener('click', async () => { 1070 const ev = allEventos.find(e => e.id === currentEventoId); 1071 const isPub = ev?.publicado ?? false; 1072 await saveEvento(!isPub); 1073}); 1074document.getElementById('ev-btn-update').addEventListener('click', () => saveEvento(null)); 1075document.getElementById('ev-btn-draft').addEventListener('click', () => saveEvento(false)); 1076document.getElementById('ev-btn-delete').addEventListener('click', deleteEvento); 1077document.getElementById('btn-new-evento').addEventListener('click', newEventoForm); 1078</script>
1078 1079</body> 1080</html>
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.