394 lines
18 KiB
JavaScript
394 lines
18 KiB
JavaScript
/* BAN YARO — Social Media Manager */
|
|
window.Page_social = (() => {
|
|
|
|
let _el, _state;
|
|
let _breeds = [];
|
|
let _contents = [];
|
|
let _activeTab = 'archiv';
|
|
|
|
const _STATUS_LABEL = {
|
|
idea: 'Idee', draft: 'Entwurf', scheduled: 'Geplant',
|
|
published: 'Veröffentlicht', archived: 'Archiviert',
|
|
};
|
|
const _STATUS_COLOR = {
|
|
idea: 'var(--c-text-muted)', draft: 'var(--c-warning)',
|
|
scheduled: 'var(--c-primary)', published: 'var(--c-success)',
|
|
archived: 'var(--c-text-muted)',
|
|
};
|
|
const _FORMAT_ICON = { reel: 'film-strip', story: 'circle', post: 'image', carousel: 'images' };
|
|
const _PLATFORM_ICON = { tiktok: 'music-notes-plus', instagram: 'instagram-logo', both: 'share-network' };
|
|
|
|
async function init(el, state) {
|
|
_el = el; _state = state;
|
|
_el.innerHTML = UI.skeleton(3);
|
|
_breeds = await API.get('/social/breeds').catch(() => []);
|
|
await _render();
|
|
}
|
|
|
|
function refresh() { if (_el) _render(); }
|
|
|
|
async function _render() {
|
|
_el.innerHTML = `
|
|
<div style="display:flex;align-items:center;justify-content:space-between;
|
|
margin-bottom:var(--space-4);flex-wrap:wrap;gap:var(--space-2)">
|
|
<h2 style="margin:0;font-size:var(--text-lg);font-weight:700">Social Media Manager</h2>
|
|
</div>
|
|
|
|
<!-- Tabs -->
|
|
<div style="display:flex;gap:0;border-bottom:2px solid var(--c-border);
|
|
margin-bottom:var(--space-4);overflow-x:auto">
|
|
${['archiv','generator','bewerten'].map(t => `
|
|
<button class="sm-tab${_activeTab===t?' sm-tab--active':''}" data-tab="${t}"
|
|
style="padding:var(--space-2) var(--space-4);border:none;background:none;
|
|
cursor:pointer;font-size:var(--text-sm);white-space:nowrap;
|
|
border-bottom:2px solid ${_activeTab===t?'var(--c-primary)':'transparent'};
|
|
margin-bottom:-2px;font-weight:${_activeTab===t?'600':'400'};
|
|
color:${_activeTab===t?'var(--c-primary)':'var(--c-text-secondary)'}">
|
|
${{archiv:'Archiv',generator:'Idee generieren',bewerten:'Entwurf bewerten'}[t]}
|
|
</button>`).join('')}
|
|
</div>
|
|
|
|
<div id="sm-content"></div>`;
|
|
|
|
_el.querySelectorAll('.sm-tab').forEach(btn =>
|
|
btn.addEventListener('click', () => { _activeTab = btn.dataset.tab; _render(); })
|
|
);
|
|
|
|
const cont = _el.querySelector('#sm-content');
|
|
if (_activeTab === 'archiv') await _renderArchiv(cont);
|
|
if (_activeTab === 'generator') _renderGenerator(cont);
|
|
if (_activeTab === 'bewerten') _renderBewerten(cont);
|
|
}
|
|
|
|
// ---------------------------------------------------------------
|
|
// ARCHIV
|
|
// ---------------------------------------------------------------
|
|
async function _renderArchiv(el) {
|
|
el.innerHTML = UI.skeleton(2);
|
|
const statusFilter = ['alle','idea','draft','scheduled','published','archived'];
|
|
let currentFilter = 'alle';
|
|
|
|
async function load(filter) {
|
|
currentFilter = filter;
|
|
const url = filter === 'alle' ? '/social/content' : `/social/content?status=${filter}`;
|
|
_contents = await API.get(url).catch(() => []);
|
|
renderList();
|
|
}
|
|
|
|
function renderList() {
|
|
el.innerHTML = `
|
|
<div style="display:flex;gap:var(--space-2);flex-wrap:wrap;margin-bottom:var(--space-3)">
|
|
${statusFilter.map(s => `
|
|
<button class="btn btn-sm ${currentFilter===s?'btn-primary':'btn-secondary'}"
|
|
data-filter="${s}" style="padding:2px 10px">
|
|
${{alle:'Alle',idea:'Ideen',draft:'Entwürfe',scheduled:'Geplant',
|
|
published:'Veröffentlicht',archived:'Archiv'}[s]}
|
|
</button>`).join('')}
|
|
</div>
|
|
${_contents.length === 0
|
|
? UI.emptyState({icon:'instagram-logo', title:'Noch keine Inhalte',
|
|
text:'Erstelle deinen ersten Content-Vorschlag im Tab "Idee generieren".'})
|
|
: `<div style="display:flex;flex-direction:column;gap:var(--space-3)">
|
|
${_contents.map(_renderCard).join('')}
|
|
</div>`}`;
|
|
|
|
el.querySelectorAll('[data-filter]').forEach(btn =>
|
|
btn.addEventListener('click', () => load(btn.dataset.filter))
|
|
);
|
|
el.querySelectorAll('.sm-status-btn').forEach(btn =>
|
|
btn.addEventListener('click', async () => {
|
|
const {id, status} = btn.dataset;
|
|
await API.patch(`/social/content/${id}`, {status});
|
|
await load(currentFilter);
|
|
})
|
|
);
|
|
el.querySelectorAll('.sm-delete-btn').forEach(btn =>
|
|
btn.addEventListener('click', async () => {
|
|
if (!window.confirm('Eintrag löschen?')) return;
|
|
await API.delete(`/social/content/${btn.dataset.id}`);
|
|
await load(currentFilter);
|
|
})
|
|
);
|
|
el.querySelectorAll('.sm-expand-btn').forEach(btn =>
|
|
btn.addEventListener('click', () => {
|
|
const detail = el.querySelector(`#sm-detail-${btn.dataset.id}`);
|
|
if (detail) detail.style.display = detail.style.display === 'none' ? '' : 'none';
|
|
})
|
|
);
|
|
}
|
|
|
|
await load('alle');
|
|
}
|
|
|
|
function _renderCard(c) {
|
|
const score = c.ai_score ? '⭐'.repeat(c.ai_score) : '';
|
|
const unsplashUrl = c.unsplash_query
|
|
? `https://unsplash.com/s/photos/${encodeURIComponent(c.unsplash_query)}`
|
|
: null;
|
|
const nextStatuses = {
|
|
idea: ['draft','archived'], draft: ['scheduled','archived'],
|
|
scheduled: ['published','draft'], published: ['archived'], archived: ['idea'],
|
|
}[c.status] || [];
|
|
|
|
return `
|
|
<div class="card" style="padding:var(--space-3)">
|
|
<div style="display:flex;align-items:flex-start;gap:var(--space-3)">
|
|
<div style="flex:1;min-width:0">
|
|
<div style="display:flex;align-items:center;gap:var(--space-2);flex-wrap:wrap;
|
|
margin-bottom:var(--space-1)">
|
|
<svg class="ph-icon" style="width:16px;height:16px;flex-shrink:0">
|
|
<use href="/icons/phosphor.svg#${_PLATFORM_ICON[c.platform]||'share-network'}"></use></svg>
|
|
<svg class="ph-icon" style="width:16px;height:16px;flex-shrink:0">
|
|
<use href="/icons/phosphor.svg#${_FORMAT_ICON[c.format]||'image'}"></use></svg>
|
|
<span style="font-size:var(--text-xs);padding:1px 6px;border-radius:4px;
|
|
background:var(--c-surface-2);color:${_STATUS_COLOR[c.status]}">
|
|
${_STATUS_LABEL[c.status]||c.status}</span>
|
|
${score ? `<span style="font-size:11px">${score}</span>` : ''}
|
|
<span style="font-size:var(--text-xs);color:var(--c-text-muted)">
|
|
${c.created_at?.slice(0,10)||''}</span>
|
|
</div>
|
|
<div style="font-weight:600;font-size:var(--text-sm);margin-bottom:var(--space-1)">
|
|
${_esc(c.topic)}</div>
|
|
${c.hook ? `<div style="font-size:var(--text-xs);color:var(--c-text-secondary);
|
|
font-style:italic;margin-bottom:var(--space-1)">
|
|
🎣 ${_esc(c.hook)}</div>` : ''}
|
|
</div>
|
|
<div style="display:flex;gap:var(--space-1);flex-shrink:0">
|
|
<button class="btn btn-sm btn-secondary sm-expand-btn" data-id="${c.id}"
|
|
style="padding:2px 8px;font-size:var(--text-xs)">Details</button>
|
|
<button class="btn btn-sm btn-secondary sm-delete-btn" data-id="${c.id}"
|
|
style="padding:2px 8px;font-size:var(--text-xs);color:var(--c-danger)">✕</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Detail-Bereich (eingeklappt) -->
|
|
<div id="sm-detail-${c.id}" style="display:none;margin-top:var(--space-3);
|
|
border-top:1px solid var(--c-border);padding-top:var(--space-3)">
|
|
${c.caption ? `<div style="margin-bottom:var(--space-2)">
|
|
<div class="sm-label">Caption</div>
|
|
<div style="white-space:pre-wrap;font-size:var(--text-sm)">${_esc(c.caption)}</div>
|
|
</div>` : ''}
|
|
${c.hashtags ? `<div style="margin-bottom:var(--space-2)">
|
|
<div class="sm-label">Hashtags</div>
|
|
<div style="font-size:var(--text-xs);color:var(--c-primary)">
|
|
${c.hashtags.split(',').map(h=>`#${h.trim()}`).join(' ')}</div>
|
|
</div>` : ''}
|
|
${c.cta ? `<div style="margin-bottom:var(--space-2)">
|
|
<div class="sm-label">Call-to-Action</div>
|
|
<div style="font-size:var(--text-sm)">${_esc(c.cta)}</div>
|
|
</div>` : ''}
|
|
${c.visual_brief ? `<div style="margin-bottom:var(--space-2)">
|
|
<div class="sm-label">Visual Brief</div>
|
|
<div style="font-size:var(--text-sm)">${_esc(c.visual_brief)}</div>
|
|
</div>` : ''}
|
|
${c.image_prompt ? `<div style="margin-bottom:var(--space-2)">
|
|
<div class="sm-label">DALL-E / Midjourney Prompt</div>
|
|
<div style="font-size:var(--text-xs);background:var(--c-surface-2);
|
|
padding:var(--space-2);border-radius:6px;font-family:monospace">
|
|
${_esc(c.image_prompt)}</div>
|
|
</div>` : ''}
|
|
${c.canva_notes ? `<div style="margin-bottom:var(--space-2)">
|
|
<div class="sm-label">Canva-Tipps</div>
|
|
<div style="font-size:var(--text-sm)">${_esc(c.canva_notes)}</div>
|
|
</div>` : ''}
|
|
${c.script ? `<div style="margin-bottom:var(--space-2)">
|
|
<div class="sm-label">Video-Skript</div>
|
|
<div style="font-size:var(--text-sm);white-space:pre-wrap">${_esc(c.script)}</div>
|
|
</div>` : ''}
|
|
${unsplashUrl ? `<div style="margin-bottom:var(--space-2)">
|
|
<div class="sm-label">Stockfotos</div>
|
|
<a href="${unsplashUrl}" target="_blank" rel="noopener"
|
|
style="font-size:var(--text-sm);color:var(--c-primary)">
|
|
Unsplash: "${_esc(c.unsplash_query)}" →</a>
|
|
</div>` : ''}
|
|
${c.notes ? `<div style="margin-bottom:var(--space-2)">
|
|
<div class="sm-label">Notizen / Feedback</div>
|
|
<div style="font-size:var(--text-sm);color:var(--c-text-secondary)">${_esc(c.notes)}</div>
|
|
</div>` : ''}
|
|
${nextStatuses.length ? `<div style="display:flex;gap:var(--space-2);margin-top:var(--space-2)">
|
|
${nextStatuses.map(s => `
|
|
<button class="btn btn-sm btn-secondary sm-status-btn"
|
|
data-id="${c.id}" data-status="${s}"
|
|
style="font-size:var(--text-xs)">
|
|
→ ${_STATUS_LABEL[s]}</button>`).join('')}
|
|
</div>` : ''}
|
|
</div>
|
|
</div>`;
|
|
}
|
|
|
|
// ---------------------------------------------------------------
|
|
// GENERATOR
|
|
// ---------------------------------------------------------------
|
|
function _renderGenerator(el) {
|
|
el.innerHTML = `
|
|
<div class="card" style="padding:var(--space-4)">
|
|
<div style="display:grid;gap:var(--space-3)">
|
|
<div style="display:grid;grid-template-columns:1fr 1fr;gap:var(--space-3)">
|
|
<div>
|
|
<label class="form-label">Plattform</label>
|
|
<select id="sm-platform" class="input">
|
|
<option value="both">TikTok + Instagram</option>
|
|
<option value="instagram">Instagram</option>
|
|
<option value="tiktok">TikTok</option>
|
|
</select>
|
|
</div>
|
|
<div>
|
|
<label class="form-label">Format</label>
|
|
<select id="sm-format" class="input">
|
|
<option value="post">Post / Bild</option>
|
|
<option value="reel">Reel / Video</option>
|
|
<option value="story">Story</option>
|
|
<option value="carousel">Carousel</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<label class="form-label">Thema</label>
|
|
<input id="sm-topic" class="input" placeholder="z.B. 5 Tipps gegen Hitzestress im Sommer"
|
|
style="width:100%">
|
|
</div>
|
|
<div>
|
|
<label class="form-label">Rasse (optional)</label>
|
|
<select id="sm-breed" class="input">
|
|
<option value="">— keine spezifische Rasse —</option>
|
|
${_breeds.map(b => `<option value="${b.id}">${_esc(b.name)}</option>`).join('')}
|
|
</select>
|
|
</div>
|
|
<button id="sm-gen-btn" class="btn btn-primary">
|
|
${UI.icon('sparkle')} Content generieren
|
|
</button>
|
|
<div id="sm-gen-result"></div>
|
|
</div>
|
|
</div>`;
|
|
|
|
_el.querySelector('#sm-gen-btn').addEventListener('click', async () => {
|
|
const topic = _el.querySelector('#sm-topic').value.trim();
|
|
if (!topic) { UI.toast('Bitte ein Thema eingeben.', 'warning'); return; }
|
|
|
|
const btn = _el.querySelector('#sm-gen-btn');
|
|
const res = _el.querySelector('#sm-gen-result');
|
|
btn.disabled = true;
|
|
btn.textContent = 'Generiere…';
|
|
res.innerHTML = '<div style="text-align:center;padding:var(--space-4);color:var(--c-text-muted)">KI denkt nach… (~15s)</div>';
|
|
|
|
try {
|
|
const data = await API.post('/social/generate', {
|
|
platform: _el.querySelector('#sm-platform').value,
|
|
format: _el.querySelector('#sm-format').value,
|
|
topic,
|
|
breed_id: parseInt(_el.querySelector('#sm-breed').value) || null,
|
|
});
|
|
res.innerHTML = `
|
|
<div style="background:var(--c-success-bg,#f0fdf4);border:1px solid var(--c-success);
|
|
border-radius:8px;padding:var(--space-3);margin-top:var(--space-2)">
|
|
✓ Gespeichert im Archiv (Score: ${'⭐'.repeat(data.ai_score||0)})
|
|
</div>
|
|
${_renderCard(data)}`;
|
|
res.querySelectorAll('.sm-expand-btn').forEach(btn => {
|
|
const detail = res.querySelector(`#sm-detail-${btn.dataset.id}`);
|
|
btn.addEventListener('click', () => {
|
|
if (detail) detail.style.display = detail.style.display==='none' ? '' : 'none';
|
|
});
|
|
});
|
|
_el.querySelector('#sm-topic').value = '';
|
|
} catch(e) {
|
|
res.innerHTML = `<div style="color:var(--c-danger)">${UI.icon('warning')} Fehler: ${_esc(e.message||String(e))}</div>`;
|
|
} finally {
|
|
btn.disabled = false;
|
|
btn.innerHTML = `${UI.icon('sparkle')} Content generieren`;
|
|
}
|
|
});
|
|
}
|
|
|
|
// ---------------------------------------------------------------
|
|
// BEWERTEN
|
|
// ---------------------------------------------------------------
|
|
function _renderBewerten(el) {
|
|
el.innerHTML = `
|
|
<div class="card" style="padding:var(--space-4)">
|
|
<p style="font-size:var(--text-sm);color:var(--c-text-secondary);margin-bottom:var(--space-3)">
|
|
Füge deinen eigenen Entwurf ein — die KI bewertet ihn, verbessert Caption,
|
|
Hashtags und gibt dir einen Visual Brief dazu.
|
|
</p>
|
|
<div style="display:grid;grid-template-columns:1fr 1fr;gap:var(--space-3);margin-bottom:var(--space-3)">
|
|
<div>
|
|
<label class="form-label">Plattform</label>
|
|
<select id="sm-eval-platform" class="input">
|
|
<option value="instagram">Instagram</option>
|
|
<option value="tiktok">TikTok</option>
|
|
<option value="both">Beide</option>
|
|
</select>
|
|
</div>
|
|
<div>
|
|
<label class="form-label">Format</label>
|
|
<select id="sm-eval-format" class="input">
|
|
<option value="post">Post / Bild</option>
|
|
<option value="reel">Reel / Video</option>
|
|
<option value="story">Story</option>
|
|
<option value="carousel">Carousel</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
<textarea id="sm-eval-draft" class="input"
|
|
style="width:100%;min-height:120px;resize:vertical"
|
|
placeholder="Dein Caption-Entwurf, Idee oder Rohtext…"></textarea>
|
|
<button id="sm-eval-btn" class="btn btn-primary" style="margin-top:var(--space-3)">
|
|
${UI.icon('magnifying-glass')} Bewerten & verbessern
|
|
</button>
|
|
<div id="sm-eval-result" style="margin-top:var(--space-3)"></div>
|
|
</div>`;
|
|
|
|
_el.querySelector('#sm-eval-btn').addEventListener('click', async () => {
|
|
const draft = _el.querySelector('#sm-eval-draft').value.trim();
|
|
if (!draft) { UI.toast('Bitte einen Entwurf eingeben.', 'warning'); return; }
|
|
|
|
const btn = _el.querySelector('#sm-eval-btn');
|
|
const res = _el.querySelector('#sm-eval-result');
|
|
btn.disabled = true;
|
|
btn.textContent = 'Analysiere…';
|
|
res.innerHTML = '<div style="text-align:center;padding:var(--space-4);color:var(--c-text-muted)">KI analysiert… (~10s)</div>';
|
|
|
|
try {
|
|
const data = await API.post('/social/evaluate', {
|
|
platform: _el.querySelector('#sm-eval-platform').value,
|
|
format: _el.querySelector('#sm-eval-format').value,
|
|
draft,
|
|
});
|
|
res.innerHTML = `
|
|
${data.notes ? `<div class="card" style="padding:var(--space-3);background:var(--c-surface-2);
|
|
margin-bottom:var(--space-3)">
|
|
<div class="sm-label">KI-Feedback</div>
|
|
<div style="font-size:var(--text-sm)">${_esc(data.notes)}</div>
|
|
</div>` : ''}
|
|
${_renderCard(data)}`;
|
|
res.querySelectorAll('.sm-expand-btn').forEach(btn => {
|
|
const detail = res.querySelector(`#sm-detail-${btn.dataset.id}`);
|
|
btn.addEventListener('click', () => {
|
|
if (detail) detail.style.display = detail.style.display==='none' ? '' : 'none';
|
|
});
|
|
});
|
|
} catch(e) {
|
|
res.innerHTML = `<div style="color:var(--c-danger)">${UI.icon('warning')} Fehler: ${_esc(e.message||String(e))}</div>`;
|
|
} finally {
|
|
btn.disabled = false;
|
|
btn.innerHTML = `${UI.icon('magnifying-glass')} Bewerten & verbessern`;
|
|
}
|
|
});
|
|
}
|
|
|
|
function _esc(s) {
|
|
if (!s) return '';
|
|
return String(s)
|
|
.replace(/&/g,'&').replace(/</g,'<')
|
|
.replace(/>/g,'>').replace(/"/g,'"');
|
|
}
|
|
|
|
// CSS
|
|
const style = document.createElement('style');
|
|
style.textContent = `.sm-label{font-size:var(--text-xs);font-weight:600;
|
|
color:var(--c-text-muted);text-transform:uppercase;letter-spacing:.5px;
|
|
margin-bottom:3px;}`;
|
|
document.head.appendChild(style);
|
|
|
|
return { init, refresh };
|
|
})();
|