PageSourceSearch

https://www.trintaporcento.com.br/admin.html

html trintaporcento.com.br collected 2026-10-06 04:44:54 UTC 45,972 bytes, 1,080 lines download raw bytes

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        &nbsp;${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        &nbsp;${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.