v3
This commit is contained in:
@@ -0,0 +1,951 @@
|
||||
<?php
|
||||
/**
|
||||
* post.php — Browser admin UI for updates.json with media upload support
|
||||
* ─────────────────────────────────────────────────────────────────────────────
|
||||
* Requires: api.php · media_upload.php · index_config/api_keys.txt
|
||||
* Media saved to: /media/ (configured in media_upload.php)
|
||||
*/
|
||||
|
||||
// ══════════════════════════════════════════════════════════════════════════════
|
||||
// CONFIG
|
||||
// ══════════════════════════════════════════════════════════════════════════════
|
||||
define('POST_API_URL', './api.php');
|
||||
define('POST_UPLOAD_URL', './media_upload.php');
|
||||
define('POST_PAGE_TITLE', 'Updates — Post Manager');
|
||||
// ══════════════════════════════════════════════════════════════════════════════
|
||||
?>
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title><?= htmlspecialchars(POST_PAGE_TITLE) ?></title>
|
||||
<meta name="robots" content="noindex, nofollow">
|
||||
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Share+Tech+Mono&display=swap" rel="stylesheet">
|
||||
|
||||
<style>
|
||||
/* ── Design tokens — exact gate.php match ───────────────────────────────── */
|
||||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
|
||||
:root {
|
||||
--bg: #09090f;
|
||||
--surface: #0f0f1a;
|
||||
--surface-2: #141425;
|
||||
--border: #1e1e38;
|
||||
--border-2: #2a2a48;
|
||||
--text: #e8e8f8;
|
||||
--text-2: #9898c0;
|
||||
--text-3: #58587a;
|
||||
--green: #00e87a;
|
||||
--blue: #00c8ff;
|
||||
--purple: #b060ff;
|
||||
--red: #ff3355;
|
||||
--orange: #ff8800;
|
||||
--pink: #ff50a0;
|
||||
--pat: #ff424d;
|
||||
--pat-glow: rgba(255,66,77,.18);
|
||||
--r: 6px;
|
||||
}
|
||||
|
||||
html { font-size: 16px; -webkit-text-size-adjust: 100%; }
|
||||
|
||||
body {
|
||||
min-height: 100dvh;
|
||||
background: var(--bg);
|
||||
background-image:
|
||||
radial-gradient(ellipse 70% 50% at 20% 10%, rgba(176,96,255,.05) 0%, transparent 60%),
|
||||
radial-gradient(ellipse 60% 40% at 80% 80%, rgba(0,200,255,.04) 0%, transparent 60%),
|
||||
radial-gradient(ellipse 50% 60% at 50% 50%, rgba(255,66,77,.03) 0%, transparent 70%);
|
||||
font-family: 'Inter', system-ui, sans-serif;
|
||||
color: var(--text);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 1.5rem 1rem 3rem;
|
||||
}
|
||||
|
||||
body::after {
|
||||
content: '';
|
||||
position: fixed; inset: 0; pointer-events: none; z-index: 999;
|
||||
background: repeating-linear-gradient(
|
||||
0deg, transparent 0px, transparent 3px,
|
||||
rgba(0,0,0,.07) 3px, rgba(0,0,0,.07) 4px
|
||||
);
|
||||
}
|
||||
|
||||
@keyframes rise {
|
||||
from { opacity: 0; transform: translateY(14px) scale(.98); }
|
||||
to { opacity: 1; transform: translateY(0) scale(1); }
|
||||
}
|
||||
@keyframes shimmer {
|
||||
from { background-position: 0% 50%; }
|
||||
to { background-position: 200% 50%; }
|
||||
}
|
||||
@keyframes spin { to { transform: rotate(360deg); } }
|
||||
@keyframes shake {
|
||||
0%,100% { transform: translateX(0); }
|
||||
25% { transform: translateX(-5px); }
|
||||
75% { transform: translateX(5px); }
|
||||
}
|
||||
@keyframes fade-in {
|
||||
from { opacity: 0; transform: translateY(6px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
/* ── Page wrapper ───────────────────────────────────────────────────────── */
|
||||
.page {
|
||||
width: 100%; max-width: 760px;
|
||||
display: flex; flex-direction: column; gap: 1.25rem;
|
||||
animation: rise .4s cubic-bezier(.22,.68,0,1.2) both;
|
||||
position: relative; z-index: 1;
|
||||
}
|
||||
|
||||
/* ── Card ───────────────────────────────────────────────────────────────── */
|
||||
.card {
|
||||
background: rgba(15,15,26,.88);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: calc(var(--r) + 4px);
|
||||
overflow: hidden; position: relative;
|
||||
backdrop-filter: blur(12px);
|
||||
-webkit-backdrop-filter: blur(12px);
|
||||
box-shadow: 0 0 0 1px rgba(255,255,255,.03), 0 6px 40px rgba(0,0,0,.6);
|
||||
}
|
||||
.card::before {
|
||||
content: '';
|
||||
position: absolute; top: 0; left: 0; right: 0; height: 2px; z-index: 2;
|
||||
background: linear-gradient(90deg,
|
||||
var(--pat), var(--purple), var(--blue), var(--green),
|
||||
var(--blue), var(--purple), var(--pat));
|
||||
background-size: 300% 100%;
|
||||
animation: shimmer 5s linear infinite;
|
||||
}
|
||||
.card::after {
|
||||
content: '';
|
||||
position: absolute; bottom: -1px; right: -1px;
|
||||
width: 14px; height: 14px;
|
||||
border-bottom: 2px solid var(--purple);
|
||||
border-right: 2px solid var(--purple);
|
||||
border-radius: 0 0 calc(var(--r) + 4px) 0;
|
||||
opacity: .5;
|
||||
}
|
||||
|
||||
/* ── Top bar ────────────────────────────────────────────────────────────── */
|
||||
.topbar {
|
||||
display: flex; align-items: center; justify-content: space-between;
|
||||
flex-wrap: wrap; gap: .75rem; padding: 1rem 1.25rem;
|
||||
}
|
||||
.topbar-brand { display: flex; flex-direction: column; gap: .15rem; }
|
||||
.topbar-eyebrow {
|
||||
font-family: 'Share Tech Mono', monospace;
|
||||
font-size: .6rem; letter-spacing: .2em; text-transform: uppercase;
|
||||
color: var(--green); text-shadow: 0 0 10px rgba(0,232,122,.5);
|
||||
}
|
||||
.topbar-eyebrow::before { content: '> '; opacity: .5; }
|
||||
.topbar-title { font-size: 1.05rem; font-weight: 700; letter-spacing: -.01em; }
|
||||
.topbar-actions { display: flex; align-items: center; gap: .5rem; }
|
||||
|
||||
/* ── Auth ───────────────────────────────────────────────────────────────── */
|
||||
#auth-screen {
|
||||
padding: 2rem 1.75rem 2.25rem;
|
||||
display: flex; flex-direction: column; gap: 1.25rem;
|
||||
}
|
||||
.auth-heading {
|
||||
font-size: 1.25rem; font-weight: 700; letter-spacing: -.01em;
|
||||
background: linear-gradient(135deg, var(--blue), var(--purple));
|
||||
-webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text;
|
||||
}
|
||||
.auth-sub { font-family: 'Share Tech Mono', monospace; font-size: .72rem; color: var(--text-3); }
|
||||
|
||||
/* ── Form elements ──────────────────────────────────────────────────────── */
|
||||
.field { display: flex; flex-direction: column; gap: .4rem; }
|
||||
.field label {
|
||||
font-family: 'Share Tech Mono', monospace;
|
||||
font-size: .62rem; letter-spacing: .14em; text-transform: uppercase;
|
||||
color: var(--blue); text-shadow: 0 0 8px rgba(0,200,255,.35);
|
||||
}
|
||||
.field input[type="text"],
|
||||
.field input[type="password"],
|
||||
.field input[type="url"],
|
||||
.field textarea {
|
||||
width: 100%; padding: .72rem .95rem;
|
||||
font-family: 'Share Tech Mono', monospace; font-size: .88rem;
|
||||
color: var(--text); background: rgba(255,255,255,.03);
|
||||
border: 1px solid var(--border-2); border-radius: var(--r); outline: none;
|
||||
transition: border-color .2s, box-shadow .2s, background .2s;
|
||||
resize: vertical;
|
||||
}
|
||||
.field input:focus, .field textarea:focus {
|
||||
border-color: var(--blue); background: rgba(0,200,255,.04);
|
||||
box-shadow: 0 0 0 3px rgba(0,200,255,.1);
|
||||
}
|
||||
.field input::placeholder, .field textarea::placeholder { color: var(--text-3); }
|
||||
.field textarea { min-height: 90px; line-height: 1.6; }
|
||||
.field-row { display: grid; grid-template-columns: 1fr 1fr; gap: .75rem; }
|
||||
@media (max-width: 520px) { .field-row { grid-template-columns: 1fr; } }
|
||||
.field-hint { font-family: 'Share Tech Mono', monospace; font-size: .6rem; color: var(--text-3); }
|
||||
|
||||
/* ── File drop zone ─────────────────────────────────────────────────────── */
|
||||
.drop-zone {
|
||||
position: relative;
|
||||
border: 2px dashed var(--border-2);
|
||||
border-radius: var(--r);
|
||||
padding: 1.25rem 1rem;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
transition: border-color .2s, background .2s;
|
||||
background: rgba(255,255,255,.02);
|
||||
}
|
||||
.drop-zone.drag-over {
|
||||
border-color: var(--green);
|
||||
background: rgba(0,232,122,.05);
|
||||
}
|
||||
.drop-zone input[type="file"] {
|
||||
position: absolute; inset: 0; opacity: 0; cursor: pointer; width: 100%; height: 100%;
|
||||
}
|
||||
.drop-zone-label {
|
||||
pointer-events: none;
|
||||
display: flex; flex-direction: column; align-items: center; gap: .5rem;
|
||||
}
|
||||
.drop-zone-icon { color: var(--text-3); }
|
||||
.drop-zone-text {
|
||||
font-family: 'Share Tech Mono', monospace;
|
||||
font-size: .68rem; letter-spacing: .08em; color: var(--text-3);
|
||||
}
|
||||
.drop-zone-hint { font-size: .62rem; color: var(--text-3); opacity: .6; }
|
||||
|
||||
/* ── Upload progress bar ────────────────────────────────────────────────── */
|
||||
.upload-progress {
|
||||
display: none;
|
||||
height: 3px;
|
||||
background: var(--border-2);
|
||||
border-radius: 99px;
|
||||
overflow: hidden;
|
||||
margin-top: .4rem;
|
||||
}
|
||||
.upload-progress.visible { display: block; }
|
||||
.upload-progress-bar {
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, var(--green), var(--blue));
|
||||
border-radius: 99px;
|
||||
transition: width .2s ease;
|
||||
width: 0%;
|
||||
}
|
||||
|
||||
/* ── Media preview grid (in compose / edit forms) ───────────────────────── */
|
||||
.media-previews {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: .5rem;
|
||||
margin-top: .25rem;
|
||||
}
|
||||
.media-preview-item {
|
||||
position: relative;
|
||||
border: 1px solid var(--border-2);
|
||||
border-radius: var(--r);
|
||||
overflow: hidden;
|
||||
width: 90px; height: 70px;
|
||||
background: var(--surface-2);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.media-preview-item img,
|
||||
.media-preview-item video {
|
||||
width: 100%; height: 100%;
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
}
|
||||
.media-preview-item .media-remove {
|
||||
position: absolute; top: 3px; right: 3px;
|
||||
width: 18px; height: 18px;
|
||||
background: rgba(255,51,85,.85);
|
||||
border: none; border-radius: 50%;
|
||||
color: #fff; cursor: pointer;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
font-size: 10px; font-weight: 700;
|
||||
line-height: 1;
|
||||
transition: background .15s;
|
||||
}
|
||||
.media-preview-item .media-remove:hover { background: var(--red); }
|
||||
.media-preview-type {
|
||||
position: absolute; bottom: 3px; left: 4px;
|
||||
font-family: 'Share Tech Mono', monospace;
|
||||
font-size: .48rem; letter-spacing: .06em; text-transform: uppercase;
|
||||
color: rgba(255,255,255,.8);
|
||||
background: rgba(0,0,0,.55);
|
||||
padding: .05em .3em; border-radius: 2px;
|
||||
}
|
||||
|
||||
/* ── Buttons ────────────────────────────────────────────────────────────── */
|
||||
.btn {
|
||||
display: inline-flex; align-items: center; justify-content: center; gap: .4rem;
|
||||
padding: .6rem 1.1rem; border-radius: var(--r);
|
||||
font-family: 'Inter', sans-serif; font-size: .84rem; font-weight: 600;
|
||||
border: none; cursor: pointer; text-decoration: none; white-space: nowrap;
|
||||
transition: transform .15s, filter .15s, box-shadow .2s;
|
||||
min-height: 38px; position: relative; overflow: hidden;
|
||||
}
|
||||
.btn::before {
|
||||
content: ''; position: absolute; inset: 0;
|
||||
background: linear-gradient(180deg, rgba(255,255,255,.09) 0%, transparent 60%);
|
||||
pointer-events: none;
|
||||
}
|
||||
.btn:hover { transform: translateY(-1px); filter: brightness(1.08); }
|
||||
.btn:active { transform: scale(.97); filter: brightness(.95); }
|
||||
.btn:disabled { opacity: .45; cursor: not-allowed; transform: none; filter: none; }
|
||||
.btn-green { background: var(--green); color: #050510; box-shadow: 0 4px 16px rgba(0,232,122,.25); }
|
||||
.btn-blue { background: var(--blue); color: #050510; box-shadow: 0 4px 14px rgba(0,200,255,.2); }
|
||||
.btn-red { background: var(--red); color: #fff; box-shadow: 0 4px 14px rgba(255,51,85,.25); }
|
||||
.btn-ghost {
|
||||
background: rgba(255,255,255,.04); color: var(--text-2);
|
||||
border: 1px solid var(--border-2);
|
||||
}
|
||||
.btn-ghost:hover { border-color: var(--green); color: var(--text); }
|
||||
.btn-sm { padding: .35rem .75rem; font-size: .76rem; min-height: 30px; }
|
||||
.btn .spinner {
|
||||
width: 13px; height: 13px; border-radius: 50%;
|
||||
border: 2px solid rgba(0,0,0,.2); border-top-color: currentColor;
|
||||
animation: spin .6s linear infinite;
|
||||
}
|
||||
|
||||
/* ── Alerts ─────────────────────────────────────────────────────────────── */
|
||||
.alert {
|
||||
display: flex; align-items: flex-start; gap: .6rem;
|
||||
border-radius: var(--r); padding: .7rem 1rem;
|
||||
font-family: 'Share Tech Mono', monospace; font-size: .78rem; line-height: 1.55;
|
||||
}
|
||||
.alert-error { background: rgba(255,51,85,.08); border: 1px solid rgba(255,51,85,.3); color: var(--red); animation: shake .35s ease; }
|
||||
.alert-success{ background: rgba(0,232,122,.07); border: 1px solid rgba(0,232,122,.25); color: var(--green); }
|
||||
|
||||
/* ── Section head ───────────────────────────────────────────────────────── */
|
||||
.section-head {
|
||||
display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: .5rem;
|
||||
padding: .85rem 1.25rem .7rem; border-bottom: 1px solid var(--border);
|
||||
}
|
||||
.section-title {
|
||||
font-family: 'Share Tech Mono', monospace;
|
||||
font-size: .62rem; letter-spacing: .2em; text-transform: uppercase;
|
||||
color: var(--green); text-shadow: 0 0 10px rgba(0,232,122,.5);
|
||||
}
|
||||
.section-title::before { content: '> '; opacity: .5; }
|
||||
.section-badge { font-family: 'Share Tech Mono', monospace; font-size: .58rem; color: var(--text-3); }
|
||||
|
||||
/* ── Compose ────────────────────────────────────────────────────────────── */
|
||||
#compose-form {
|
||||
padding: 1rem 1.25rem 1.25rem;
|
||||
display: flex; flex-direction: column; gap: .85rem;
|
||||
}
|
||||
.compose-actions {
|
||||
display: flex; align-items: center; justify-content: space-between; gap: .5rem; flex-wrap: wrap;
|
||||
}
|
||||
|
||||
/* ── Post list ──────────────────────────────────────────────────────────── */
|
||||
#post-list { list-style: none; }
|
||||
.post-item {
|
||||
border-bottom: 1px solid var(--border);
|
||||
padding: 1rem 1.25rem;
|
||||
display: flex; flex-direction: column; gap: .6rem;
|
||||
animation: fade-in .25s ease both;
|
||||
}
|
||||
.post-item:last-child { border-bottom: none; }
|
||||
.post-meta { display: flex; align-items: center; gap: .5rem; flex-wrap: wrap; }
|
||||
.post-id {
|
||||
font-family: 'Share Tech Mono', monospace; font-size: .6rem; color: var(--text-3);
|
||||
background: rgba(255,255,255,.04); border: 1px solid var(--border-2);
|
||||
border-radius: 3px; padding: .05em .4em;
|
||||
}
|
||||
.post-date { font-family: 'Share Tech Mono', monospace; font-size: .6rem; color: var(--text-3); }
|
||||
.post-tag {
|
||||
font-family: 'Share Tech Mono', monospace; font-size: .58rem; letter-spacing: .07em;
|
||||
text-transform: uppercase; padding: .05em .4em;
|
||||
border: 1px solid var(--border-2); border-radius: 3px; color: var(--blue);
|
||||
}
|
||||
.post-text { font-size: .9rem; color: var(--text); line-height: 1.65; }
|
||||
.post-link {
|
||||
font-family: 'Share Tech Mono', monospace; font-size: .65rem;
|
||||
color: var(--blue); text-decoration: none; opacity: .8;
|
||||
}
|
||||
.post-link:hover { opacity: 1; }
|
||||
|
||||
/* ── Media strip in feed (admin view — compact) ─────────────────────────── */
|
||||
.post-media-strip {
|
||||
display: flex; flex-wrap: wrap; gap: .4rem;
|
||||
}
|
||||
.post-media-thumb {
|
||||
width: 72px; height: 56px;
|
||||
border: 1px solid var(--border-2); border-radius: 4px;
|
||||
overflow: hidden; background: var(--surface-2);
|
||||
position: relative; flex-shrink: 0;
|
||||
}
|
||||
.post-media-thumb img {
|
||||
width: 100%; height: 100%; object-fit: cover; display: block;
|
||||
}
|
||||
.post-media-thumb .vid-icon {
|
||||
position: absolute; inset: 0;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
background: rgba(0,0,0,.55);
|
||||
color: var(--green);
|
||||
}
|
||||
.post-media-thumb .media-thumb-label {
|
||||
position: absolute; bottom: 2px; left: 3px;
|
||||
font-family: 'Share Tech Mono', monospace; font-size: .45rem;
|
||||
color: rgba(255,255,255,.7); background: rgba(0,0,0,.5);
|
||||
padding: .05em .25em; border-radius: 2px; text-transform: uppercase;
|
||||
}
|
||||
|
||||
.post-actions { display: flex; align-items: center; gap: .4rem; flex-wrap: wrap; }
|
||||
|
||||
/* ── Inline edit form ───────────────────────────────────────────────────── */
|
||||
.edit-form {
|
||||
display: none; flex-direction: column; gap: .75rem;
|
||||
padding-top: .75rem; border-top: 1px solid var(--border); margin-top: .25rem;
|
||||
}
|
||||
.edit-form.open { display: flex; animation: fade-in .2s ease; }
|
||||
|
||||
/* ── Misc ───────────────────────────────────────────────────────────────── */
|
||||
.feed-status {
|
||||
padding: 2.5rem 1.25rem;
|
||||
font-family: 'Share Tech Mono', monospace; font-size: .72rem;
|
||||
color: var(--text-3); text-align: center; letter-spacing: .06em;
|
||||
}
|
||||
.feed-status::before { content: '// '; opacity: .4; }
|
||||
.loading-row {
|
||||
display: flex; align-items: center; justify-content: center; gap: .6rem; padding: 2rem;
|
||||
}
|
||||
.big-spinner {
|
||||
width: 20px; height: 20px; border-radius: 50%;
|
||||
border: 2px solid var(--border-2); border-top-color: var(--green);
|
||||
animation: spin .7s linear infinite;
|
||||
}
|
||||
.loading-row span { font-family: 'Share Tech Mono', monospace; font-size: .7rem; color: var(--text-3); }
|
||||
.key-badge {
|
||||
display: inline-flex; align-items: center; gap: .35rem; padding: .25rem .6rem;
|
||||
background: rgba(0,232,122,.07); border: 1px solid rgba(0,232,122,.2); border-radius: 99px;
|
||||
font-family: 'Share Tech Mono', monospace; font-size: .6rem; color: var(--green);
|
||||
}
|
||||
.key-dot { width: 6px; height: 6px; border-radius: 50%; background: var(--green); box-shadow: 0 0 5px var(--green); }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="page">
|
||||
|
||||
<!-- Auth card -->
|
||||
<div class="card" id="auth-card">
|
||||
<div id="auth-screen">
|
||||
<div>
|
||||
<p class="auth-heading">Post Manager</p>
|
||||
<p class="auth-sub">Enter your API key to continue</p>
|
||||
</div>
|
||||
<div id="auth-alert"></div>
|
||||
<div class="field">
|
||||
<label for="key-input">API Key</label>
|
||||
<input type="password" id="key-input" placeholder="your-api-key" autocomplete="current-password" autofocus>
|
||||
</div>
|
||||
<div>
|
||||
<button class="btn btn-green" id="auth-btn" onclick="doAuth()">
|
||||
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="11" width="18" height="11" rx="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg>
|
||||
Unlock
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Main UI -->
|
||||
<div id="main-ui" style="display:none;flex-direction:column;gap:1.25rem;">
|
||||
|
||||
<!-- Topbar -->
|
||||
<div class="card">
|
||||
<div class="topbar">
|
||||
<div class="topbar-brand">
|
||||
<span class="topbar-eyebrow">Updates</span>
|
||||
<span class="topbar-title"><?= htmlspecialchars(POST_PAGE_TITLE) ?></span>
|
||||
</div>
|
||||
<div class="topbar-actions">
|
||||
<span class="key-badge" id="key-indicator"><span class="key-dot"></span>authenticated</span>
|
||||
<button class="btn btn-ghost btn-sm" onclick="doLogout()">
|
||||
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/><polyline points="16 17 21 12 16 7"/><line x1="21" y1="12" x2="9" y2="12"/></svg>
|
||||
Sign out
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Compose -->
|
||||
<div class="card">
|
||||
<div class="section-head"><span class="section-title">New Post</span></div>
|
||||
<div id="compose-form">
|
||||
<div id="compose-alert"></div>
|
||||
|
||||
<div class="field">
|
||||
<label for="c-text">Text <span style="color:var(--red)">*</span></label>
|
||||
<textarea id="c-text" placeholder="What's happening?" rows="3"></textarea>
|
||||
</div>
|
||||
|
||||
<div class="field-row">
|
||||
<div class="field">
|
||||
<label for="c-tags">Tags</label>
|
||||
<input type="text" id="c-tags" placeholder="stream, update, blog">
|
||||
<span class="field-hint">comma-separated</span>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="c-date">Date</label>
|
||||
<input type="text" id="c-date" placeholder="leave blank for now">
|
||||
<span class="field-hint">ISO-8601 or blank = now</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field-row">
|
||||
<div class="field">
|
||||
<label for="c-link">Link URL</label>
|
||||
<input type="url" id="c-link" placeholder="https://…">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="c-link-label">Link label</label>
|
||||
<input type="text" id="c-link-label" placeholder="Read more">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Media upload -->
|
||||
<div class="field">
|
||||
<label>Media (images / video)</label>
|
||||
<div class="drop-zone" id="c-drop-zone">
|
||||
<input type="file" id="c-media-input" multiple
|
||||
accept="image/jpeg,image/png,image/gif,image/webp,image/avif,image/svg+xml,video/mp4,video/quicktime,video/webm,video/ogg,video/x-msvideo,video/x-matroska">
|
||||
<div class="drop-zone-label">
|
||||
<svg class="drop-zone-icon" width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="8.5" cy="8.5" r="1.5"/><polyline points="21 15 16 10 5 21"/></svg>
|
||||
<span class="drop-zone-text">Drop files here or click to browse</span>
|
||||
<span class="drop-zone-hint">JPEG · PNG · GIF · WEBP · MP4 · MOV · WEBM — up to 200 MB each</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="upload-progress" id="c-upload-progress">
|
||||
<div class="upload-progress-bar" id="c-progress-bar"></div>
|
||||
</div>
|
||||
<div class="media-previews" id="c-media-previews"></div>
|
||||
</div>
|
||||
|
||||
<div class="compose-actions">
|
||||
<button class="btn btn-green" id="compose-btn" onclick="submitPost()">
|
||||
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>
|
||||
Post update
|
||||
</button>
|
||||
<button class="btn btn-ghost btn-sm" onclick="clearCompose()">Clear</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Feed -->
|
||||
<div class="card">
|
||||
<div class="section-head">
|
||||
<span class="section-title">All Posts</span>
|
||||
<span class="section-badge" id="post-count">—</span>
|
||||
</div>
|
||||
<ul id="post-list">
|
||||
<li class="loading-row"><div class="big-spinner"></div><span>loading posts…</span></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
</div><!-- /main-ui -->
|
||||
</div><!-- /page -->
|
||||
|
||||
<script>
|
||||
/* ════════════════════════════════════════════════════════════════════════════
|
||||
Post Manager — vanilla JS with media upload support
|
||||
════════════════════════════════════════════════════════════════════════════ */
|
||||
const API = '<?= htmlspecialchars(POST_API_URL) ?>';
|
||||
const UPLOAD_URL = '<?= htmlspecialchars(POST_UPLOAD_URL) ?>';
|
||||
const STORE_KEY = 'tc_post_mgr_key';
|
||||
|
||||
let apiKey = '';
|
||||
|
||||
/* ── SVG icons ──────────────────────────────────────────────────────────────── */
|
||||
const ICO = {
|
||||
edit: `<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/></svg>`,
|
||||
trash: `<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"/><path d="M10 11v6M14 11v6"/><path d="M9 6V4a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v2"/></svg>`,
|
||||
save: `<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z"/><polyline points="17 21 17 13 7 13 7 21"/><polyline points="7 3 7 8 15 8"/></svg>`,
|
||||
cancel: `<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>`,
|
||||
warn: `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg>`,
|
||||
ok: `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>`,
|
||||
play: `<svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor"><polygon points="5 3 19 12 5 21 5 3"/></svg>`,
|
||||
};
|
||||
|
||||
/* ── Helpers ────────────────────────────────────────────────────────────────── */
|
||||
function esc(str) {
|
||||
return String(str ?? '')
|
||||
.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>')
|
||||
.replace(/"/g,'"').replace(/'/g,''');
|
||||
}
|
||||
|
||||
function showAlert(id, type, msg) {
|
||||
const el = document.getElementById(id);
|
||||
if (!el) return;
|
||||
el.innerHTML = `<div class="alert alert-${type}">${ICO[type==='error'?'warn':'ok']} ${esc(msg)}</div>`;
|
||||
setTimeout(() => { el.innerHTML = ''; }, 6000);
|
||||
}
|
||||
|
||||
function setLoading(btnId, on) {
|
||||
const btn = document.getElementById(btnId);
|
||||
if (!btn) return;
|
||||
btn.disabled = on;
|
||||
const sp = btn.querySelector('.spinner');
|
||||
if (on && !sp) btn.insertAdjacentHTML('afterbegin', '<span class="spinner"></span>');
|
||||
if (!on && sp) sp.remove();
|
||||
}
|
||||
|
||||
async function apiFetch(path, opts = {}) {
|
||||
const res = await fetch(API + (path ? '?' + path : ''), {
|
||||
...opts,
|
||||
headers: { 'Content-Type': 'application/json', 'X-API-Key': apiKey, ...(opts.headers||{}) },
|
||||
});
|
||||
const json = await res.json();
|
||||
if (!json.ok) throw new Error(json.error || 'API error');
|
||||
return json.data;
|
||||
}
|
||||
|
||||
function parseTags(str) { return str.split(',').map(t=>t.trim()).filter(Boolean); }
|
||||
|
||||
function relTime(iso) {
|
||||
if (!iso) return '';
|
||||
const d = Math.floor((Date.now() - new Date(iso)) / 1000);
|
||||
if (d < 60) return 'just now';
|
||||
if (d < 3600) return Math.floor(d/60) + 'm ago';
|
||||
if (d < 86400) return Math.floor(d/3600) + 'h ago';
|
||||
if (d < 604800) return Math.floor(d/86400) + 'd ago';
|
||||
return new Date(iso).toLocaleDateString('en-US',{month:'short',day:'numeric',year:'numeric'});
|
||||
}
|
||||
|
||||
/* ════════════════════════════════════════════════════════════════════════════
|
||||
MEDIA UPLOAD SYSTEM
|
||||
Each compose / edit form has its own pending media array.
|
||||
Uploads happen immediately on file selection; results stored in pendingMedia[formId].
|
||||
════════════════════════════════════════════════════════════════════════════ */
|
||||
const pendingMedia = {}; // formId → [{url, type, mime, filename}, …]
|
||||
|
||||
function initDropZone(formId, dropZoneId, inputId, progressId, barId, previewsId) {
|
||||
pendingMedia[formId] = pendingMedia[formId] || [];
|
||||
|
||||
const zone = document.getElementById(dropZoneId);
|
||||
const input = document.getElementById(inputId);
|
||||
const progress = document.getElementById(progressId);
|
||||
const bar = document.getElementById(barId);
|
||||
const previews = document.getElementById(previewsId);
|
||||
|
||||
if (!zone) return;
|
||||
|
||||
// Drag-over highlight
|
||||
zone.addEventListener('dragover', e => { e.preventDefault(); zone.classList.add('drag-over'); });
|
||||
zone.addEventListener('dragleave', () => zone.classList.remove('drag-over'));
|
||||
zone.addEventListener('drop', e => {
|
||||
e.preventDefault();
|
||||
zone.classList.remove('drag-over');
|
||||
uploadFiles(formId, e.dataTransfer.files, progress, bar, previews);
|
||||
});
|
||||
|
||||
input.addEventListener('change', () => {
|
||||
uploadFiles(formId, input.files, progress, bar, previews);
|
||||
input.value = ''; // allow re-selecting the same file
|
||||
});
|
||||
}
|
||||
|
||||
async function uploadFiles(formId, fileList, progress, bar, previews) {
|
||||
for (const file of fileList) {
|
||||
progress.classList.add('visible');
|
||||
bar.style.width = '0%';
|
||||
|
||||
try {
|
||||
const fd = new FormData();
|
||||
fd.append('file', file);
|
||||
|
||||
// XHR for progress events
|
||||
const result = await new Promise((resolve, reject) => {
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.open('POST', UPLOAD_URL);
|
||||
xhr.setRequestHeader('X-API-Key', apiKey);
|
||||
xhr.upload.addEventListener('progress', e => {
|
||||
if (e.lengthComputable) bar.style.width = Math.round(e.loaded / e.total * 100) + '%';
|
||||
});
|
||||
xhr.addEventListener('load', () => {
|
||||
try { resolve(JSON.parse(xhr.responseText)); }
|
||||
catch(e) { reject(new Error('Bad response')); }
|
||||
});
|
||||
xhr.addEventListener('error', () => reject(new Error('Network error')));
|
||||
xhr.send(fd);
|
||||
});
|
||||
|
||||
if (!result.ok) throw new Error(result.error || 'Upload failed');
|
||||
|
||||
bar.style.width = '100%';
|
||||
pendingMedia[formId].push({ url: result.url, type: result.type, mime: result.mime, filename: result.filename });
|
||||
addPreviewThumb(formId, result, previews);
|
||||
|
||||
} catch (err) {
|
||||
alert('Upload failed: ' + err.message);
|
||||
} finally {
|
||||
setTimeout(() => { progress.classList.remove('visible'); bar.style.width = '0%'; }, 600);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function addPreviewThumb(formId, item, previewsEl) {
|
||||
const idx = pendingMedia[formId].length - 1;
|
||||
const wrap = document.createElement('div');
|
||||
wrap.className = 'media-preview-item';
|
||||
wrap.dataset.idx = idx;
|
||||
|
||||
if (item.type === 'image') {
|
||||
wrap.innerHTML = `<img src="${esc(item.url)}" alt="">
|
||||
<span class="media-preview-type">img</span>
|
||||
<button class="media-remove" title="Remove" onclick="removeMedia('${formId}',${idx})">✕</button>`;
|
||||
} else {
|
||||
wrap.innerHTML = `<div style="width:100%;height:100%;background:var(--surface-2);display:flex;align-items:center;justify-content:center;">
|
||||
<svg width="22" height="22" viewBox="0 0 24 24" fill="var(--green)"><polygon points="5 3 19 12 5 21 5 3"/></svg>
|
||||
</div>
|
||||
<span class="media-preview-type">vid</span>
|
||||
<button class="media-remove" title="Remove" onclick="removeMedia('${formId}',${idx})">✕</button>`;
|
||||
}
|
||||
previewsEl.appendChild(wrap);
|
||||
}
|
||||
|
||||
function removeMedia(formId, idx) {
|
||||
pendingMedia[formId][idx] = null; // null = removed (preserve index alignment)
|
||||
const previews = document.querySelectorAll(`[data-idx="${idx}"]`);
|
||||
previews.forEach(el => el.remove());
|
||||
}
|
||||
|
||||
function getMedia(formId) {
|
||||
return (pendingMedia[formId] || []).filter(Boolean);
|
||||
}
|
||||
|
||||
function clearMedia(formId, previewsId) {
|
||||
pendingMedia[formId] = [];
|
||||
const el = document.getElementById(previewsId);
|
||||
if (el) el.innerHTML = '';
|
||||
}
|
||||
|
||||
/* ── Render media strip in admin feed ───────────────────────────────────────── */
|
||||
function mediaStripHTML(mediaArr) {
|
||||
if (!mediaArr || !mediaArr.length) return '';
|
||||
const items = mediaArr.map(m => {
|
||||
if (m.type === 'image') {
|
||||
return `<div class="post-media-thumb"><img src="${esc(m.url)}" alt="" loading="lazy"><span class="media-thumb-label">img</span></div>`;
|
||||
}
|
||||
return `<div class="post-media-thumb">
|
||||
<div class="vid-icon">${ICO.play}</div>
|
||||
<span class="media-thumb-label">vid</span>
|
||||
</div>`;
|
||||
}).join('');
|
||||
return `<div class="post-media-strip">${items}</div>`;
|
||||
}
|
||||
|
||||
/* ── Auth ────────────────────────────────────────────────────────────────────── */
|
||||
function doAuth() {
|
||||
const key = document.getElementById('key-input').value.trim();
|
||||
if (!key) { showAlert('auth-alert','error','Enter your API key.'); return; }
|
||||
apiKey = key;
|
||||
setLoading('auth-btn', true);
|
||||
apiFetch('')
|
||||
.then(() => {
|
||||
sessionStorage.setItem(STORE_KEY, key);
|
||||
document.getElementById('auth-card').style.display = 'none';
|
||||
document.getElementById('main-ui').style.display = 'flex';
|
||||
initDropZone('compose', 'c-drop-zone', 'c-media-input', 'c-upload-progress', 'c-progress-bar', 'c-media-previews');
|
||||
loadFeed();
|
||||
})
|
||||
.catch(err => {
|
||||
apiKey = '';
|
||||
setLoading('auth-btn', false);
|
||||
showAlert('auth-alert','error','Invalid key: ' + err.message);
|
||||
});
|
||||
}
|
||||
|
||||
function doLogout() { sessionStorage.removeItem(STORE_KEY); location.reload(); }
|
||||
|
||||
/* ── Feed ────────────────────────────────────────────────────────────────────── */
|
||||
function loadFeed() {
|
||||
const list = document.getElementById('post-list');
|
||||
list.innerHTML = `<li class="loading-row"><div class="big-spinner"></div><span>loading…</span></li>`;
|
||||
apiFetch('')
|
||||
.then(data => renderFeed(data.items, data.total))
|
||||
.catch(err => { list.innerHTML = `<li class="feed-status">failed: ${esc(err.message)}</li>`; });
|
||||
}
|
||||
|
||||
function renderFeed(posts, total) {
|
||||
document.getElementById('post-count').textContent = total + ' post' + (total!==1?'s':'');
|
||||
const list = document.getElementById('post-list');
|
||||
list.innerHTML = posts.length
|
||||
? posts.map(p => postHTML(p)).join('')
|
||||
: '<li class="feed-status">no posts yet</li>';
|
||||
}
|
||||
|
||||
function postHTML(p) {
|
||||
const tags = (p.tags||[]).map(t=>`<span class="post-tag">${esc(t)}</span>`).join('');
|
||||
const link = p.link ? `<a class="post-link" href="${esc(p.link)}" target="_blank" rel="noopener">${esc(p.link_label||p.link)}</a>` : '';
|
||||
const media = mediaStripHTML(p.media||[]);
|
||||
|
||||
return `<li class="post-item" id="pi-${esc(p.id)}">
|
||||
<div class="post-meta">
|
||||
<span class="post-id">#${esc(p.id)}</span>
|
||||
<span class="post-date">${esc(relTime(p.date))}</span>
|
||||
${tags}
|
||||
</div>
|
||||
<p class="post-text">${esc(p.text)}</p>
|
||||
${link}
|
||||
${media}
|
||||
<div class="post-actions">
|
||||
<button class="btn btn-ghost btn-sm" onclick="toggleEdit('${esc(p.id)}')">${ICO.edit} Edit</button>
|
||||
<button class="btn btn-red btn-sm" onclick="deletePost('${esc(p.id)}')">${ICO.trash} Delete</button>
|
||||
</div>
|
||||
<div class="edit-form" id="ef-${esc(p.id)}">
|
||||
<div class="field">
|
||||
<label>Text</label>
|
||||
<textarea id="ef-text-${esc(p.id)}" rows="3">${esc(p.text)}</textarea>
|
||||
</div>
|
||||
<div class="field-row">
|
||||
<div class="field">
|
||||
<label>Tags</label>
|
||||
<input type="text" id="ef-tags-${esc(p.id)}" value="${esc((p.tags||[]).join(', '))}">
|
||||
<span class="field-hint">comma-separated</span>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>Date (ISO-8601)</label>
|
||||
<input type="text" id="ef-date-${esc(p.id)}" value="${esc(p.date||'')}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="field-row">
|
||||
<div class="field">
|
||||
<label>Link URL</label>
|
||||
<input type="url" id="ef-link-${esc(p.id)}" value="${esc(p.link||'')}">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>Link label</label>
|
||||
<input type="text" id="ef-linklabel-${esc(p.id)}" value="${esc(p.link_label||'')}">
|
||||
</div>
|
||||
</div>
|
||||
<!-- Edit: add more media (cannot remove existing via UI — use api.php directly) -->
|
||||
<div class="field">
|
||||
<label>Add more media</label>
|
||||
<div class="drop-zone" id="ef-drop-${esc(p.id)}">
|
||||
<input type="file" id="ef-media-input-${esc(p.id)}" multiple
|
||||
accept="image/jpeg,image/png,image/gif,image/webp,image/avif,image/svg+xml,video/mp4,video/quicktime,video/webm,video/ogg,video/x-msvideo,video/x-matroska">
|
||||
<div class="drop-zone-label">
|
||||
<span class="drop-zone-text">Drop or click to add media</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="upload-progress" id="ef-progress-${esc(p.id)}">
|
||||
<div class="upload-progress-bar" id="ef-bar-${esc(p.id)}"></div>
|
||||
</div>
|
||||
<div class="media-previews" id="ef-previews-${esc(p.id)}"></div>
|
||||
</div>
|
||||
<div style="display:flex;gap:.5rem;flex-wrap:wrap;">
|
||||
<button class="btn btn-blue btn-sm" onclick="saveEdit('${esc(p.id)}')">${ICO.save} Save</button>
|
||||
<button class="btn btn-ghost btn-sm" onclick="toggleEdit('${esc(p.id)}')">${ICO.cancel} Cancel</button>
|
||||
</div>
|
||||
<div id="ef-alert-${esc(p.id)}"></div>
|
||||
</div>
|
||||
</li>`;
|
||||
}
|
||||
|
||||
/* ── Compose ─────────────────────────────────────────────────────────────────── */
|
||||
function clearCompose() {
|
||||
['c-text','c-tags','c-date','c-link','c-link-label'].forEach(id => {
|
||||
document.getElementById(id).value = '';
|
||||
});
|
||||
clearMedia('compose', 'c-media-previews');
|
||||
}
|
||||
|
||||
function submitPost() {
|
||||
const text = document.getElementById('c-text').value.trim();
|
||||
if (!text) { showAlert('compose-alert','error','Text is required.'); return; }
|
||||
|
||||
const body = {
|
||||
text,
|
||||
tags: parseTags(document.getElementById('c-tags').value),
|
||||
link: document.getElementById('c-link').value.trim(),
|
||||
link_label: document.getElementById('c-link-label').value.trim(),
|
||||
media: getMedia('compose'),
|
||||
};
|
||||
const date = document.getElementById('c-date').value.trim();
|
||||
if (date) body.date = date;
|
||||
|
||||
setLoading('compose-btn', true);
|
||||
apiFetch('', { method: 'POST', body: JSON.stringify(body) })
|
||||
.then(post => {
|
||||
clearCompose();
|
||||
setLoading('compose-btn', false);
|
||||
showAlert('compose-alert','success','Posted: #' + post.id);
|
||||
loadFeed();
|
||||
})
|
||||
.catch(err => { setLoading('compose-btn', false); showAlert('compose-alert','error',err.message); });
|
||||
}
|
||||
|
||||
/* ── Edit ────────────────────────────────────────────────────────────────────── */
|
||||
function toggleEdit(id) {
|
||||
const ef = document.getElementById('ef-' + id);
|
||||
ef.classList.toggle('open');
|
||||
if (ef.classList.contains('open')) {
|
||||
// Init drop zone for this edit form
|
||||
initDropZone('ef-'+id, 'ef-drop-'+id, 'ef-media-input-'+id,
|
||||
'ef-progress-'+id, 'ef-bar-'+id, 'ef-previews-'+id);
|
||||
}
|
||||
}
|
||||
|
||||
function saveEdit(id) {
|
||||
const text = document.getElementById('ef-text-' + id).value.trim();
|
||||
if (!text) { showAlert('ef-alert-'+id,'error','Text cannot be empty.'); return; }
|
||||
|
||||
// Fetch current post to merge existing media
|
||||
apiFetch('id=' + encodeURIComponent(id))
|
||||
.then(current => {
|
||||
const existingMedia = current.media || [];
|
||||
const newMedia = getMedia('ef-' + id);
|
||||
const body = {
|
||||
text,
|
||||
tags: parseTags(document.getElementById('ef-tags-' + id).value),
|
||||
link: document.getElementById('ef-link-' + id).value.trim(),
|
||||
link_label: document.getElementById('ef-linklabel-' + id).value.trim(),
|
||||
media: [...existingMedia, ...newMedia],
|
||||
};
|
||||
const date = document.getElementById('ef-date-' + id).value.trim();
|
||||
if (date) body.date = date;
|
||||
|
||||
return apiFetch('id=' + encodeURIComponent(id), { method: 'PUT', body: JSON.stringify(body) });
|
||||
})
|
||||
.then(updated => {
|
||||
document.getElementById('pi-' + id).outerHTML = postHTML(updated);
|
||||
})
|
||||
.catch(err => showAlert('ef-alert-'+id,'error',err.message));
|
||||
}
|
||||
|
||||
/* ── Delete ──────────────────────────────────────────────────────────────────── */
|
||||
function deletePost(id) {
|
||||
if (!confirm('Delete post #' + id + '? This cannot be undone.')) return;
|
||||
apiFetch('id=' + encodeURIComponent(id), { method: 'DELETE' })
|
||||
.then(() => {
|
||||
const li = document.getElementById('pi-' + id);
|
||||
li.style.transition = 'opacity .2s, transform .2s';
|
||||
li.style.opacity = '0'; li.style.transform = 'translateX(10px)';
|
||||
setTimeout(() => { li.remove(); loadFeed(); }, 220);
|
||||
})
|
||||
.catch(err => alert('Delete failed: ' + err.message));
|
||||
}
|
||||
|
||||
/* ── Enter key + session restore ─────────────────────────────────────────────── */
|
||||
document.getElementById('key-input').addEventListener('keydown', e => { if (e.key==='Enter') doAuth(); });
|
||||
|
||||
(function() {
|
||||
const saved = sessionStorage.getItem(STORE_KEY);
|
||||
if (!saved) return;
|
||||
document.getElementById('key-input').value = saved;
|
||||
apiKey = saved;
|
||||
apiFetch('')
|
||||
.then(() => {
|
||||
document.getElementById('auth-card').style.display = 'none';
|
||||
document.getElementById('main-ui').style.display = 'flex';
|
||||
initDropZone('compose','c-drop-zone','c-media-input','c-upload-progress','c-progress-bar','c-media-previews');
|
||||
loadFeed();
|
||||
})
|
||||
.catch(() => { sessionStorage.removeItem(STORE_KEY); apiKey = ''; });
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user