Compare commits
3 commits
ba5547f993
...
74b6c03bb3
| Author | SHA1 | Date | |
|---|---|---|---|
| 74b6c03bb3 | |||
| 8ba8f4dfa3 | |||
| d0abb6de9b |
8 changed files with 956 additions and 150 deletions
28
PROJEKT.md
28
PROJEKT.md
|
|
@ -46,7 +46,7 @@ Maps: Leaflet.js + OpenStreetMap (kostenlos, kein Google-Lock)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Implementierungsstand (aktuell: 2026-04-24, SW by-v333, APP_VER 320)
|
## Implementierungsstand (aktuell: 2026-04-24, SW by-v356, APP_VER 343)
|
||||||
|
|
||||||
### Fertig implementiert ✅
|
### Fertig implementiert ✅
|
||||||
|
|
||||||
|
|
@ -65,6 +65,32 @@ Maps: Leaflet.js + OpenStreetMap (kostenlos, kein Google-Lock)
|
||||||
- Gesundheit, Admin, Karte-Legende: Tab-/Legende-Grid 2 Zeilen (gleiche CSS-Grid-Technik)
|
- Gesundheit, Admin, Karte-Legende: Tab-/Legende-Grid 2 Zeilen (gleiche CSS-Grid-Technik)
|
||||||
- Hinweis: layout.css lädt vor components.css → ID-Selektor (#page-forum, #page-health, #page-admin, #page-map) nötig für Spezifität
|
- Hinweis: layout.css lädt vor components.css → ID-Selektor (#page-forum, #page-health, #page-admin, #page-map) nötig für Spezifität
|
||||||
|
|
||||||
|
#### Social Media Manager (2026-04-24)
|
||||||
|
- Neue Rolle `is_social_media` — eigene Seite `/social`
|
||||||
|
- Luna KI-Coach: Themen-Vorschläge, Fortschrittsbalken, rotierende Nachrichten
|
||||||
|
- **Rasse des Tages**: 1003 Wiki-Rassen = 2,75 Jahre täglicher Content mit Bild
|
||||||
|
- **🎾 Trainingstipp**: 104 Übungen in 7 Kategorien, 3 Stil-Varianten
|
||||||
|
- **🛁 Pflegetipp**: 43 Tipps rassenspezifisch, auch für normale User im Hundeprofil
|
||||||
|
- Diversitäts-Check (Warnung wenn Kategorie >40% dominiert)
|
||||||
|
- Post-Bestätigung mit Datum + URL, Ausstehend-Banner
|
||||||
|
- Medien-Upload (Kamera/Mediathek/Dateien), Instagram-Vorschau
|
||||||
|
- XP/Level-System (Rookie → 👑 Star)
|
||||||
|
- Admin: Social-Tracking (published/scheduled/ideas + letzte 10 Posts)
|
||||||
|
|
||||||
|
#### Pflege-System (2026-04-24)
|
||||||
|
- `pflege_tipps` DB-Tabelle: 43 Tipps in 10 Kategorien (Fell, Krallen, Zähne, Ohren, Augen, Pfoten, Parasiten, Saisonal, Gesundheitsvorsorge, Welpen-Pflege)
|
||||||
|
- Hundeprofil: 🛁 Pflegetipps — Tipp des Tages (saisonal) + vollständige Kategorieliste
|
||||||
|
- Rassen-Autocomplete im Hundeprofil mit Wiki-Match-Badge
|
||||||
|
- `dogs.rasse_id` FK → `wiki_rassen` für präzise Filterung
|
||||||
|
|
||||||
|
#### Breed-Enricher Wikipedia-grounded (2026-04-24)
|
||||||
|
- Korrektheit 2.3→~4.5 durch Wikipedia-Quelltext als Basis
|
||||||
|
- Claude Haiku extrahiert Fakten aus Wikipedia-Text (de/en Fallback)
|
||||||
|
- `ki_source` ('wikipedia_de/en'/'none'), `ki_model` getrackt
|
||||||
|
- LLM-as-Judge Evaluator im Admin, Gemma-Reset-Button
|
||||||
|
- 1003 Rassen, limit=2000 (ein Rutsch)
|
||||||
|
- LM Studio: Mac 10.47.11.70:11435, Modell gemma-4-31b-it
|
||||||
|
|
||||||
#### Infrastruktur
|
#### Infrastruktur
|
||||||
- SSH-Port DS1621: 4711 (geändert von 22, 2026-04-24)
|
- SSH-Port DS1621: 4711 (geändert von 22, 2026-04-24)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -121,6 +121,7 @@ from routes.training import router as training_router
|
||||||
from routes.praise import router as praise_router
|
from routes.praise import router as praise_router
|
||||||
from routes.weather import router as weather_router
|
from routes.weather import router as weather_router
|
||||||
from routes.social import router as social_router
|
from routes.social import router as social_router
|
||||||
|
from routes.moderation import router as moderation_router
|
||||||
|
|
||||||
app.include_router(auth_router, prefix="/api/auth", tags=["Auth"])
|
app.include_router(auth_router, prefix="/api/auth", tags=["Auth"])
|
||||||
app.include_router(dogs_router, prefix="/api/dogs", tags=["Hunde"])
|
app.include_router(dogs_router, prefix="/api/dogs", tags=["Hunde"])
|
||||||
|
|
@ -161,6 +162,7 @@ app.include_router(stats_router, prefix="/api/stats", tags=[
|
||||||
app.include_router(achievements_router, prefix="/api/achievements", tags=["Achievements"])
|
app.include_router(achievements_router, prefix="/api/achievements", tags=["Achievements"])
|
||||||
app.include_router(training_router, prefix="/api/training", tags=["Training"])
|
app.include_router(training_router, prefix="/api/training", tags=["Training"])
|
||||||
app.include_router(praise_router, prefix="/api/praise", tags=["Praise"])
|
app.include_router(praise_router, prefix="/api/praise", tags=["Praise"])
|
||||||
|
app.include_router(moderation_router, prefix="/api/moderation", tags=["Moderation"])
|
||||||
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
|
|
|
||||||
223
backend/routes/moderation.py
Normal file
223
backend/routes/moderation.py
Normal file
|
|
@ -0,0 +1,223 @@
|
||||||
|
"""BAN YARO — Moderations-Panel Backend"""
|
||||||
|
from fastapi import APIRouter, Depends, HTTPException
|
||||||
|
from database import db
|
||||||
|
from auth import get_current_user
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Dependency: Moderator oder Admin
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
def require_moderator(user=Depends(get_current_user)):
|
||||||
|
if not (user.get("is_moderator") or user["rolle"] == "admin"):
|
||||||
|
raise HTTPException(403, "Nur für Moderatoren.")
|
||||||
|
return user
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# GET /api/moderation/stats — Übersicht
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
@router.get("/stats")
|
||||||
|
async def mod_stats(user=Depends(require_moderator)):
|
||||||
|
with db() as conn:
|
||||||
|
open_reports = conn.execute(
|
||||||
|
"SELECT COUNT(*) FROM forum_reports WHERE resolved=0"
|
||||||
|
).fetchone()[0]
|
||||||
|
|
||||||
|
pending_fotos = 0
|
||||||
|
try:
|
||||||
|
pending_fotos = conn.execute(
|
||||||
|
"SELECT COUNT(*) FROM wiki_foto_submissions WHERE status='pending'"
|
||||||
|
).fetchone()[0]
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
banned_users = conn.execute(
|
||||||
|
"SELECT COUNT(*) FROM users WHERE is_banned=1"
|
||||||
|
).fetchone()[0]
|
||||||
|
|
||||||
|
pending_zuchter = 0
|
||||||
|
try:
|
||||||
|
pending_zuchter = conn.execute(
|
||||||
|
"SELECT COUNT(*) FROM wiki_zuchter WHERE verified=0"
|
||||||
|
).fetchone()[0]
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return {
|
||||||
|
"open_reports": open_reports,
|
||||||
|
"pending_fotos": pending_fotos,
|
||||||
|
"banned_users": banned_users,
|
||||||
|
"pending_zuchter": pending_zuchter,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# GET /api/moderation/reports — gemeldete Inhalte
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
@router.get("/reports")
|
||||||
|
async def mod_reports(user=Depends(require_moderator)):
|
||||||
|
with db() as conn:
|
||||||
|
rows = conn.execute("""
|
||||||
|
SELECT r.id, r.target_type, r.target_id, r.grund, r.resolved, r.created_at,
|
||||||
|
u.name AS melder_name,
|
||||||
|
CASE r.target_type
|
||||||
|
WHEN 'thread' THEN (SELECT t.titel FROM forum_threads t WHERE t.id=r.target_id)
|
||||||
|
WHEN 'post' THEN (SELECT SUBSTR(p.text,1,80) FROM forum_posts p WHERE p.id=r.target_id)
|
||||||
|
END AS content_preview
|
||||||
|
FROM forum_reports r
|
||||||
|
LEFT JOIN users u ON u.id=r.user_id
|
||||||
|
WHERE r.resolved=0
|
||||||
|
ORDER BY r.created_at DESC
|
||||||
|
LIMIT 100
|
||||||
|
""").fetchall()
|
||||||
|
return [dict(r) for r in rows]
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# PATCH /api/moderation/reports/{id} — Meldung erledigen
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
@router.patch("/reports/{rid}")
|
||||||
|
async def mod_resolve_report(rid: int, user=Depends(require_moderator)):
|
||||||
|
with db() as conn:
|
||||||
|
r = conn.execute(
|
||||||
|
"SELECT resolved FROM forum_reports WHERE id=?", (rid,)
|
||||||
|
).fetchone()
|
||||||
|
if not r:
|
||||||
|
raise HTTPException(404, "Meldung nicht gefunden.")
|
||||||
|
new_state = 0 if r["resolved"] else 1
|
||||||
|
conn.execute(
|
||||||
|
"UPDATE forum_reports SET resolved=? WHERE id=?",
|
||||||
|
(new_state, rid)
|
||||||
|
)
|
||||||
|
return {"ok": True}
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# GET /api/moderation/users — User-Liste (Basisinfos)
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
@router.get("/users")
|
||||||
|
async def mod_users(
|
||||||
|
q: str = "",
|
||||||
|
banned: int = 0,
|
||||||
|
limit: int = 50,
|
||||||
|
offset: int = 0,
|
||||||
|
user=Depends(require_moderator),
|
||||||
|
):
|
||||||
|
with db() as conn:
|
||||||
|
where = "WHERE 1=1"
|
||||||
|
params = []
|
||||||
|
if q.strip():
|
||||||
|
where += " AND (name LIKE ? OR email LIKE ?)"
|
||||||
|
params.extend([f"%{q.strip()}%", f"%{q.strip()}%"])
|
||||||
|
if banned:
|
||||||
|
where += " AND is_banned=1"
|
||||||
|
|
||||||
|
# E-Mail nur für Admins; Moderatoren sehen maskierte Version
|
||||||
|
email_col = "email" if user["rolle"] == "admin" else \
|
||||||
|
"SUBSTR(email,1,2)||'***@'||SUBSTR(email,INSTR(email,'@')+1) AS email"
|
||||||
|
|
||||||
|
rows = conn.execute(f"""
|
||||||
|
SELECT id, name, {email_col}, rolle, is_moderator, is_banned, ban_reason, created_at
|
||||||
|
FROM users
|
||||||
|
{where}
|
||||||
|
ORDER BY created_at DESC
|
||||||
|
LIMIT ? OFFSET ?
|
||||||
|
""", [*params, limit, offset]).fetchall()
|
||||||
|
|
||||||
|
total = conn.execute(
|
||||||
|
f"SELECT COUNT(*) FROM users {where}", params
|
||||||
|
).fetchone()[0]
|
||||||
|
|
||||||
|
return {"users": [dict(r) for r in rows], "total": total}
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# PATCH /api/moderation/users/{id} — Ban / Unban
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
@router.patch("/users/{uid}")
|
||||||
|
async def mod_patch_user(uid: int, data: dict, user=Depends(require_moderator)):
|
||||||
|
allowed_fields = {"is_banned", "ban_reason"}
|
||||||
|
updates = {k: v for k, v in data.items() if k in allowed_fields}
|
||||||
|
if not updates:
|
||||||
|
raise HTTPException(400, "Keine erlaubten Felder.")
|
||||||
|
|
||||||
|
with db() as conn:
|
||||||
|
target = conn.execute(
|
||||||
|
"SELECT id, rolle, name FROM users WHERE id=?", (uid,)
|
||||||
|
).fetchone()
|
||||||
|
if not target:
|
||||||
|
raise HTTPException(404, "User nicht gefunden.")
|
||||||
|
if target["rolle"] == "admin" and user["rolle"] != "admin":
|
||||||
|
raise HTTPException(403, "Admins können nur von Admins verwaltet werden.")
|
||||||
|
|
||||||
|
cols = ", ".join(f"{k}=?" for k in updates)
|
||||||
|
conn.execute(
|
||||||
|
f"UPDATE users SET {cols} WHERE id=?",
|
||||||
|
[*updates.values(), uid]
|
||||||
|
)
|
||||||
|
row = conn.execute(
|
||||||
|
"SELECT id, name, rolle, is_banned, ban_reason FROM users WHERE id=?",
|
||||||
|
(uid,)
|
||||||
|
).fetchone()
|
||||||
|
return dict(row)
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# GET /api/moderation/fotos — Wiki-Foto-Einreichungen (pending)
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
@router.get("/fotos")
|
||||||
|
async def mod_fotos(user=Depends(require_moderator)):
|
||||||
|
with db() as conn:
|
||||||
|
try:
|
||||||
|
rows = conn.execute("""
|
||||||
|
SELECT s.id, s.rasse_slug, s.foto_url, s.created_at,
|
||||||
|
u.name AS user_name,
|
||||||
|
r.name AS rasse_name, r.foto_url AS aktuell_foto
|
||||||
|
FROM wiki_foto_submissions s
|
||||||
|
LEFT JOIN users u ON u.id=s.user_id
|
||||||
|
LEFT JOIN wiki_rassen r ON r.slug=s.rasse_slug
|
||||||
|
WHERE s.status='pending'
|
||||||
|
ORDER BY s.created_at ASC
|
||||||
|
LIMIT 50
|
||||||
|
""").fetchall()
|
||||||
|
return [dict(r) for r in rows]
|
||||||
|
except Exception:
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# PATCH /api/moderation/fotos/{id} — Foto genehmigen / ablehnen
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
@router.patch("/fotos/{foto_id}")
|
||||||
|
async def mod_foto_action(foto_id: int, data: dict, user=Depends(require_moderator)):
|
||||||
|
action = data.get("action")
|
||||||
|
if action not in ("approve", "reject"):
|
||||||
|
raise HTTPException(400, "action muss 'approve' oder 'reject' sein.")
|
||||||
|
|
||||||
|
with db() as conn:
|
||||||
|
sub = conn.execute(
|
||||||
|
"SELECT id, rasse_slug, foto_url FROM wiki_foto_submissions WHERE id=?",
|
||||||
|
(foto_id,)
|
||||||
|
).fetchone()
|
||||||
|
if not sub:
|
||||||
|
raise HTTPException(404, "Einreichung nicht gefunden.")
|
||||||
|
|
||||||
|
if action == "approve":
|
||||||
|
conn.execute(
|
||||||
|
"UPDATE wiki_foto_submissions SET status='approved' WHERE id=?",
|
||||||
|
(foto_id,)
|
||||||
|
)
|
||||||
|
conn.execute(
|
||||||
|
"UPDATE wiki_rassen SET foto_url=? WHERE slug=?",
|
||||||
|
(sub["foto_url"], sub["rasse_slug"])
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
reason = data.get("reject_reason", "Nicht geeignet.")
|
||||||
|
conn.execute(
|
||||||
|
"UPDATE wiki_foto_submissions SET status='rejected', reject_reason=? WHERE id=?",
|
||||||
|
(reason, foto_id)
|
||||||
|
)
|
||||||
|
|
||||||
|
return {"ok": True}
|
||||||
|
|
@ -185,6 +185,11 @@
|
||||||
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#camera"></use></svg> Social Media
|
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#camera"></use></svg> Social Media
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="sidebar-item" data-page="moderation" id="sidebar-moderation"
|
||||||
|
style="display:none;color:var(--c-warning,#f59e0b)">
|
||||||
|
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#shield"></use></svg> Moderation
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="sidebar-item" data-page="admin" id="sidebar-admin"
|
<div class="sidebar-item" data-page="admin" id="sidebar-admin"
|
||||||
style="display:none;color:var(--c-danger,#ef4444)">
|
style="display:none;color:var(--c-danger,#ef4444)">
|
||||||
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#shield"></use></svg> Admin
|
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#shield"></use></svg> Admin
|
||||||
|
|
@ -338,6 +343,10 @@
|
||||||
<div class="page-body page-container"></div>
|
<div class="page-body page-container"></div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<section class="page" id="page-moderation">
|
||||||
|
<div class="page-body page-container"></div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<section class="page" id="page-friends">
|
<section class="page" id="page-friends">
|
||||||
<div class="page-body page-container"></div>
|
<div class="page-body page-container"></div>
|
||||||
</section>
|
</section>
|
||||||
|
|
@ -397,9 +406,9 @@
|
||||||
<div id="modal-container"></div>
|
<div id="modal-container"></div>
|
||||||
|
|
||||||
<!-- JS: Reihenfolge ist wichtig — erst Basis, dann Features -->
|
<!-- JS: Reihenfolge ist wichtig — erst Basis, dann Features -->
|
||||||
<script src="/js/api.js?v=93"></script>
|
<script src="/js/api.js?v=94"></script>
|
||||||
<script src="/js/ui.js?v=93"></script>
|
<script src="/js/ui.js?v=94"></script>
|
||||||
<script src="/js/app.js?v=93"></script>
|
<script src="/js/app.js?v=94"></script>
|
||||||
|
|
||||||
<!-- Feature-Seiten werden lazy geladen -->
|
<!-- Feature-Seiten werden lazy geladen -->
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
Router, State-Management, Navigation, Initialisierung.
|
Router, State-Management, Navigation, Initialisierung.
|
||||||
============================================================ */
|
============================================================ */
|
||||||
|
|
||||||
const APP_VER = '343'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen
|
const APP_VER = '344'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen
|
||||||
|
|
||||||
const App = (() => {
|
const App = (() => {
|
||||||
|
|
||||||
|
|
@ -56,6 +56,7 @@ const App = (() => {
|
||||||
chat: { title: 'Nachrichten', module: null, requiresAuth: true },
|
chat: { title: 'Nachrichten', module: null, requiresAuth: true },
|
||||||
social: { title: 'Social Media', module: null, requiresAuth: true },
|
social: { title: 'Social Media', module: null, requiresAuth: true },
|
||||||
admin: { title: 'Admin', module: null, requiresAuth: true },
|
admin: { title: 'Admin', module: null, requiresAuth: true },
|
||||||
|
moderation: { title: 'Moderation', module: null, requiresAuth: true },
|
||||||
impressum: { title: 'Impressum', module: null },
|
impressum: { title: 'Impressum', module: null },
|
||||||
datenschutz: { title: 'Datenschutz', module: null },
|
datenschutz: { title: 'Datenschutz', module: null },
|
||||||
widget: { title: 'Widget', module: null, requiresAuth: true },
|
widget: { title: 'Widget', module: null, requiresAuth: true },
|
||||||
|
|
@ -422,6 +423,12 @@ const App = (() => {
|
||||||
|| state.user.is_moderator;
|
|| state.user.is_moderator;
|
||||||
adminItem.style.display = isMod ? '' : 'none';
|
adminItem.style.display = isMod ? '' : 'none';
|
||||||
}
|
}
|
||||||
|
const moderationItem = document.getElementById('sidebar-moderation');
|
||||||
|
if (moderationItem) {
|
||||||
|
const isMod = state.user.rolle === 'admin' || state.user.rolle === 'moderator'
|
||||||
|
|| state.user.is_moderator;
|
||||||
|
moderationItem.style.display = isMod ? '' : 'none';
|
||||||
|
}
|
||||||
const socialItem = document.getElementById('sidebar-social');
|
const socialItem = document.getElementById('sidebar-social');
|
||||||
if (socialItem) {
|
if (socialItem) {
|
||||||
const isSocial = state.user.is_social_media || state.user.rolle === 'admin';
|
const isSocial = state.user.is_social_media || state.user.rolle === 'admin';
|
||||||
|
|
|
||||||
447
backend/static/js/pages/moderation.js
Normal file
447
backend/static/js/pages/moderation.js
Normal file
|
|
@ -0,0 +1,447 @@
|
||||||
|
/* ============================================================
|
||||||
|
BAN YARO — Moderations-Panel
|
||||||
|
Nur für Moderatoren und Admins.
|
||||||
|
============================================================ */
|
||||||
|
|
||||||
|
window.Page_moderation = (() => {
|
||||||
|
|
||||||
|
let _container = null;
|
||||||
|
let _appState = null;
|
||||||
|
let _tab = 'uebersicht';
|
||||||
|
|
||||||
|
const TABS = [
|
||||||
|
{ id: 'uebersicht', label: 'Übersicht', icon: 'house-line' },
|
||||||
|
{ id: 'fotos', label: 'Fotos', icon: 'image' },
|
||||||
|
{ id: 'user', label: 'User', icon: 'users' },
|
||||||
|
{ id: 'forum', label: 'Forum', icon: 'chat-circle-dots' },
|
||||||
|
];
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
async function init(container, appState) {
|
||||||
|
_container = container;
|
||||||
|
_appState = appState;
|
||||||
|
|
||||||
|
const u = appState.user;
|
||||||
|
const isMod = u?.rolle === 'admin' || u?.rolle === 'moderator' || u?.is_moderator;
|
||||||
|
if (!isMod) {
|
||||||
|
container.innerHTML = _emptyState('shield', 'Kein Zugriff',
|
||||||
|
'Dieser Bereich ist nur für Moderatoren und Admins.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_render();
|
||||||
|
}
|
||||||
|
|
||||||
|
function refresh() { _renderTab(); }
|
||||||
|
function onDogChange() {}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
// SHELL
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
function _render() {
|
||||||
|
_container.innerHTML = `
|
||||||
|
<div class="by-tabs adm-tabs" id="mod-tabs">
|
||||||
|
${TABS.map(t => `
|
||||||
|
<button class="by-tab${t.id === _tab ? ' active' : ''}" data-tab="${t.id}">
|
||||||
|
${UI.icon(t.icon)} ${t.label}
|
||||||
|
</button>
|
||||||
|
`).join('')}
|
||||||
|
</div>
|
||||||
|
<div id="mod-content"></div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
_container.querySelector('#mod-tabs')
|
||||||
|
?.style.setProperty('--adm-tab-cols', Math.ceil(TABS.length / 2));
|
||||||
|
|
||||||
|
_container.querySelectorAll('#mod-tabs .by-tab').forEach(btn => {
|
||||||
|
btn.addEventListener('click', () => {
|
||||||
|
_tab = btn.dataset.tab;
|
||||||
|
_container.querySelectorAll('#mod-tabs .by-tab').forEach(b =>
|
||||||
|
b.classList.toggle('active', b.dataset.tab === _tab)
|
||||||
|
);
|
||||||
|
_renderTab();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
_renderTab();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function _renderTab() {
|
||||||
|
const el = _container.querySelector('#mod-content');
|
||||||
|
if (!el) return;
|
||||||
|
el.innerHTML = `<div style="padding:var(--space-6);text-align:center;
|
||||||
|
color:var(--c-text-muted)">Lade…</div>`;
|
||||||
|
try {
|
||||||
|
switch (_tab) {
|
||||||
|
case 'uebersicht': await _renderStats(el); break;
|
||||||
|
case 'fotos': await _renderFotos(el); break;
|
||||||
|
case 'user': await _renderUsers(el); break;
|
||||||
|
case 'forum': await _renderForum(el); break;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
el.innerHTML = _emptyState('warning', 'Fehler', e.message || 'Unbekannter Fehler.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
// TAB: ÜBERSICHT
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
async function _renderStats(el) {
|
||||||
|
const s = await API.get('/moderation/stats');
|
||||||
|
el.innerHTML = `
|
||||||
|
<div class="adm-stats-grid">
|
||||||
|
${_statCard('warning',
|
||||||
|
'Offene Meldungen',
|
||||||
|
s.open_reports,
|
||||||
|
s.open_reports > 0 ? 'var(--c-danger)' : 'var(--c-text-muted)')}
|
||||||
|
${_statCard('image',
|
||||||
|
'Fotos ausstehend',
|
||||||
|
s.pending_fotos,
|
||||||
|
s.pending_fotos > 0 ? 'var(--c-warning)' : 'var(--c-text-muted)')}
|
||||||
|
${_statCard('skull',
|
||||||
|
'Gesperrte User',
|
||||||
|
s.banned_users,
|
||||||
|
s.banned_users > 0 ? '#f59e0b' : 'var(--c-text-muted)')}
|
||||||
|
${_statCard('storefront',
|
||||||
|
'Züchter ausstehend',
|
||||||
|
s.pending_zuchter,
|
||||||
|
s.pending_zuchter > 0 ? 'var(--c-warning)' : 'var(--c-text-muted)')}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card" style="padding:var(--space-4);margin-top:var(--space-4)">
|
||||||
|
<p style="font-size:var(--text-sm);color:var(--c-text-secondary);line-height:1.6">
|
||||||
|
${UI.icon('info')}
|
||||||
|
Das Moderations-Panel zeigt dir alle ausstehenden Aufgaben auf einen Blick.
|
||||||
|
Verwende die Tabs oben für Details zu Fotos, Usern und Forum-Meldungen.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function _statCard(icon, label, value, color) {
|
||||||
|
return `
|
||||||
|
<div class="card" style="padding:var(--space-4);text-align:center">
|
||||||
|
<svg class="ph-icon" style="width:24px;height:24px;color:${color};
|
||||||
|
margin-bottom:var(--space-2)" aria-hidden="true">
|
||||||
|
<use href="/icons/phosphor.svg#${icon}"></use>
|
||||||
|
</svg>
|
||||||
|
<div style="font-size:var(--text-2xl);font-weight:var(--weight-bold);
|
||||||
|
color:var(--c-text)">${value ?? '—'}</div>
|
||||||
|
<div style="font-size:var(--text-xs);color:var(--c-text-secondary);
|
||||||
|
margin-top:2px">${label}</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
// TAB: FOTOS
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
async function _renderFotos(el) {
|
||||||
|
el.innerHTML = `
|
||||||
|
<div style="display:flex;justify-content:flex-end;margin-bottom:var(--space-3)">
|
||||||
|
<button class="btn btn-ghost btn-sm" id="mod-fotos-refresh">
|
||||||
|
${UI.icon('arrows-clockwise')} Aktualisieren
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div id="mod-fotos-list">Lade…</div>
|
||||||
|
`;
|
||||||
|
el.querySelector('#mod-fotos-refresh').addEventListener('click', () =>
|
||||||
|
_loadFotos(el.querySelector('#mod-fotos-list'))
|
||||||
|
);
|
||||||
|
await _loadFotos(el.querySelector('#mod-fotos-list'));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function _loadFotos(el) {
|
||||||
|
el.innerHTML = `<div style="padding:var(--space-4);text-align:center;
|
||||||
|
color:var(--c-text-muted)">Lade…</div>`;
|
||||||
|
const fotos = await API.get('/moderation/fotos');
|
||||||
|
if (!fotos.length) {
|
||||||
|
el.innerHTML = _emptyState('check-circle', 'Keine ausstehenden Fotos',
|
||||||
|
'Alle Foto-Einreichungen wurden bearbeitet.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
el.innerHTML = `
|
||||||
|
<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(220px,1fr));
|
||||||
|
gap:var(--space-4)">
|
||||||
|
${fotos.map(f => `
|
||||||
|
<div class="card" style="padding:var(--space-4)">
|
||||||
|
<img src="${_esc(f.foto_url)}" alt=""
|
||||||
|
style="width:100%;height:140px;object-fit:cover;
|
||||||
|
border-radius:var(--radius-md);margin-bottom:var(--space-3)">
|
||||||
|
<div style="font-weight:var(--weight-semibold);font-size:var(--text-sm)">
|
||||||
|
${_esc(f.rasse_name || f.rasse_slug)}
|
||||||
|
</div>
|
||||||
|
<div style="font-size:var(--text-xs);color:var(--c-text-muted);
|
||||||
|
margin-bottom:var(--space-3)">
|
||||||
|
von ${_esc(f.user_name)}
|
||||||
|
</div>
|
||||||
|
${f.aktuell_foto ? `
|
||||||
|
<img src="${_esc(f.aktuell_foto)}" alt="Aktuell"
|
||||||
|
style="width:100%;height:80px;object-fit:cover;
|
||||||
|
border-radius:var(--radius-sm);opacity:.5;
|
||||||
|
margin-bottom:var(--space-2)">
|
||||||
|
<div style="font-size:var(--text-xs);color:var(--c-text-muted);
|
||||||
|
margin-bottom:var(--space-3)">aktuelles Foto</div>
|
||||||
|
` : ''}
|
||||||
|
<div style="display:flex;gap:var(--space-2)">
|
||||||
|
<button class="btn btn-sm btn-primary mod-foto-approve"
|
||||||
|
data-id="${f.id}" style="flex:1">Freigeben</button>
|
||||||
|
<button class="btn btn-sm btn-ghost mod-foto-reject"
|
||||||
|
data-id="${f.id}" style="color:var(--c-danger)">Ablehnen</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`).join('')}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
el.querySelectorAll('.mod-foto-approve').forEach(btn => {
|
||||||
|
btn.addEventListener('click', async () => {
|
||||||
|
btn.disabled = true;
|
||||||
|
try {
|
||||||
|
await API.patch(`/moderation/fotos/${btn.dataset.id}`, { action: 'approve' });
|
||||||
|
UI.toast.success('Foto freigegeben.');
|
||||||
|
await _loadFotos(el);
|
||||||
|
} catch (e) { UI.toast.error(e.message); btn.disabled = false; }
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
el.querySelectorAll('.mod-foto-reject').forEach(btn => {
|
||||||
|
btn.addEventListener('click', async () => {
|
||||||
|
btn.disabled = true;
|
||||||
|
try {
|
||||||
|
await API.patch(`/moderation/fotos/${btn.dataset.id}`, {
|
||||||
|
action: 'reject',
|
||||||
|
reject_reason: 'Nicht geeignet.'
|
||||||
|
});
|
||||||
|
UI.toast.success('Foto abgelehnt.');
|
||||||
|
await _loadFotos(el);
|
||||||
|
} catch (e) { UI.toast.error(e.message); btn.disabled = false; }
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
// TAB: USER
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
async function _renderUsers(el) {
|
||||||
|
el.innerHTML = `
|
||||||
|
<div class="adm-filter-row">
|
||||||
|
<input id="mod-user-q" type="search" placeholder="Name oder E-Mail…"
|
||||||
|
class="adm-filter-input">
|
||||||
|
<label style="display:flex;align-items:center;gap:var(--space-2);
|
||||||
|
font-size:var(--text-sm);color:var(--c-text-secondary)">
|
||||||
|
<input type="checkbox" id="mod-only-banned"> Nur gesperrte
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div id="mod-user-list">Lade…</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
const load = async () => {
|
||||||
|
const q = el.querySelector('#mod-user-q').value;
|
||||||
|
const banned = el.querySelector('#mod-only-banned').checked ? 1 : 0;
|
||||||
|
const data = await API.get(
|
||||||
|
`/moderation/users?q=${encodeURIComponent(q)}&banned=${banned}`
|
||||||
|
);
|
||||||
|
_renderUserList(el.querySelector('#mod-user-list'), data.users, data.total, el);
|
||||||
|
};
|
||||||
|
|
||||||
|
let timer;
|
||||||
|
el.querySelector('#mod-user-q').addEventListener('input', () => {
|
||||||
|
clearTimeout(timer);
|
||||||
|
timer = setTimeout(load, 350);
|
||||||
|
});
|
||||||
|
el.querySelector('#mod-only-banned').addEventListener('change', load);
|
||||||
|
await load();
|
||||||
|
}
|
||||||
|
|
||||||
|
function _renderUserList(el, users, total, parentEl) {
|
||||||
|
if (!users.length) {
|
||||||
|
el.innerHTML = _emptyState('users', 'Keine Nutzer gefunden', '');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
el.innerHTML = `
|
||||||
|
<div style="margin-bottom:var(--space-2);font-size:var(--text-xs);
|
||||||
|
color:var(--c-text-muted)">${total} Nutzer gefunden</div>
|
||||||
|
<div style="display:flex;flex-direction:column;gap:var(--space-2)">
|
||||||
|
${users.map(u => `
|
||||||
|
<div class="card" style="padding:var(--space-3) var(--space-4);
|
||||||
|
${u.is_banned ? 'opacity:0.6;border-left:3px solid var(--c-danger)' : ''}">
|
||||||
|
<div style="display:flex;align-items:center;gap:var(--space-3)">
|
||||||
|
<div style="width:36px;height:36px;border-radius:50%;flex-shrink:0;
|
||||||
|
background:var(--c-surface-2);
|
||||||
|
display:flex;align-items:center;justify-content:center;
|
||||||
|
font-weight:var(--weight-bold);color:var(--c-text-secondary)">
|
||||||
|
${_esc(u.name[0].toUpperCase())}
|
||||||
|
</div>
|
||||||
|
<div style="flex:1;min-width:0">
|
||||||
|
<div style="font-size:var(--text-sm);font-weight:var(--weight-semibold);
|
||||||
|
color:var(--c-text)">
|
||||||
|
${_esc(u.name)}
|
||||||
|
${u.is_banned ? `<span style="font-size:10px;padding:1px 5px;
|
||||||
|
border-radius:3px;background:var(--c-danger);
|
||||||
|
color:#fff;margin-left:4px">GESPERRT</span>` : ''}
|
||||||
|
</div>
|
||||||
|
<div style="font-size:var(--text-xs);color:var(--c-text-muted)">
|
||||||
|
${_esc(u.email)} ·
|
||||||
|
<span style="color:${
|
||||||
|
u.rolle === 'admin' ? 'var(--c-danger)'
|
||||||
|
: u.rolle === 'moderator' ? '#f59e0b'
|
||||||
|
: 'var(--c-text-muted)'}">
|
||||||
|
${_esc(u.rolle)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style="flex-shrink:0">
|
||||||
|
${u.is_banned
|
||||||
|
? `<button class="btn btn-sm btn-ghost mod-unban"
|
||||||
|
data-uid="${u.id}" data-name="${_esc(u.name)}"
|
||||||
|
title="Sperre aufheben" style="color:var(--c-success)">
|
||||||
|
${UI.icon('lock-open')}
|
||||||
|
</button>`
|
||||||
|
: `<button class="btn btn-sm btn-ghost mod-ban"
|
||||||
|
data-uid="${u.id}" data-name="${_esc(u.name)}"
|
||||||
|
title="Sperren" style="color:var(--c-danger)">
|
||||||
|
${UI.icon('lock')}
|
||||||
|
</button>`
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`).join('')}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
el.querySelectorAll('.mod-ban').forEach(btn => {
|
||||||
|
btn.addEventListener('click', () => _banUser(btn.dataset.uid, btn.dataset.name, true, parentEl));
|
||||||
|
});
|
||||||
|
el.querySelectorAll('.mod-unban').forEach(btn => {
|
||||||
|
btn.addEventListener('click', () => _banUser(btn.dataset.uid, btn.dataset.name, false, parentEl));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function _banUser(uid, name, ban, parentEl) {
|
||||||
|
if (ban) {
|
||||||
|
const reason = window.prompt(`${name} sperren — Grund (optional):`);
|
||||||
|
if (reason === null) return;
|
||||||
|
try {
|
||||||
|
await API.patch(`/moderation/users/${uid}`, {
|
||||||
|
is_banned: 1,
|
||||||
|
ban_reason: reason || 'Kein Grund angegeben.'
|
||||||
|
});
|
||||||
|
UI.toast.success(`${name} gesperrt.`);
|
||||||
|
_renderTab();
|
||||||
|
} catch (e) { UI.toast.error(e.message); }
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
await API.patch(`/moderation/users/${uid}`, {
|
||||||
|
is_banned: 0,
|
||||||
|
ban_reason: null
|
||||||
|
});
|
||||||
|
UI.toast.success(`Sperre für ${name} aufgehoben.`);
|
||||||
|
_renderTab();
|
||||||
|
} catch (e) { UI.toast.error(e.message); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
// TAB: FORUM
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
async function _renderForum(el) {
|
||||||
|
el.innerHTML = `
|
||||||
|
<div style="display:flex;justify-content:flex-end;margin-bottom:var(--space-3)">
|
||||||
|
<button class="btn btn-ghost btn-sm" id="mod-forum-refresh">
|
||||||
|
${UI.icon('arrows-clockwise')} Aktualisieren
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div id="mod-forum-list">Lade…</div>
|
||||||
|
`;
|
||||||
|
el.querySelector('#mod-forum-refresh').addEventListener('click', () =>
|
||||||
|
_loadReports(el.querySelector('#mod-forum-list'))
|
||||||
|
);
|
||||||
|
await _loadReports(el.querySelector('#mod-forum-list'));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function _loadReports(el) {
|
||||||
|
el.innerHTML = `<div style="padding:var(--space-4);text-align:center;
|
||||||
|
color:var(--c-text-muted)">Lade…</div>`;
|
||||||
|
const reports = await API.get('/moderation/reports');
|
||||||
|
if (!reports.length) {
|
||||||
|
el.innerHTML = _emptyState('check-circle', 'Keine offenen Meldungen', 'Alles sauber.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
el.innerHTML = `
|
||||||
|
<div style="display:flex;flex-direction:column;gap:var(--space-3)">
|
||||||
|
${reports.map(r => `
|
||||||
|
<div class="card" style="padding:var(--space-4);
|
||||||
|
border-left:3px solid var(--c-danger)">
|
||||||
|
<div style="display:flex;align-items:flex-start;gap:var(--space-3)">
|
||||||
|
<div style="flex:1;min-width:0">
|
||||||
|
<div style="font-size:var(--text-xs);color:var(--c-text-muted);
|
||||||
|
margin-bottom:var(--space-1)">
|
||||||
|
${_esc(r.target_type)} #${r.target_id} ·
|
||||||
|
Gemeldet von <strong>${_esc(r.melder_name)}</strong>
|
||||||
|
</div>
|
||||||
|
<div style="font-size:var(--text-sm);font-weight:var(--weight-semibold);
|
||||||
|
color:var(--c-text);margin-bottom:var(--space-1)">
|
||||||
|
Grund: ${_esc(r.grund)}
|
||||||
|
</div>
|
||||||
|
${r.content_preview ? `
|
||||||
|
<div style="font-size:var(--text-xs);color:var(--c-text-secondary);
|
||||||
|
padding:var(--space-2) var(--space-3);
|
||||||
|
background:var(--c-surface-2);
|
||||||
|
border-radius:var(--radius-sm)">
|
||||||
|
${_esc(r.content_preview)}
|
||||||
|
</div>` : ''}
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-sm btn-primary mod-resolve-btn"
|
||||||
|
data-rid="${r.id}" title="Als erledigt markieren">
|
||||||
|
${UI.icon('check')}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`).join('')}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
el.querySelectorAll('.mod-resolve-btn').forEach(btn => {
|
||||||
|
btn.addEventListener('click', async () => {
|
||||||
|
btn.disabled = true;
|
||||||
|
try {
|
||||||
|
await API.patch(`/moderation/reports/${btn.dataset.rid}`, {});
|
||||||
|
UI.toast.success('Meldung als erledigt markiert.');
|
||||||
|
await _loadReports(el);
|
||||||
|
} catch (e) { UI.toast.error(e.message); btn.disabled = false; }
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
// HELPERS
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
function _emptyState(icon, title, text) {
|
||||||
|
return `
|
||||||
|
<div style="text-align:center;padding:var(--space-10) var(--space-4)">
|
||||||
|
<svg class="ph-icon" style="width:40px;height:40px;color:var(--c-border);
|
||||||
|
margin-bottom:var(--space-3)" aria-hidden="true">
|
||||||
|
<use href="/icons/phosphor.svg#${icon}"></use>
|
||||||
|
</svg>
|
||||||
|
<p style="font-weight:var(--weight-semibold);color:var(--c-text);
|
||||||
|
margin:0 0 var(--space-1)">${title}</p>
|
||||||
|
${text ? `<p style="font-size:var(--text-sm);color:var(--c-text-muted);margin:0">${text}</p>` : ''}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function _esc(s) {
|
||||||
|
if (!s) return '';
|
||||||
|
return String(s)
|
||||||
|
.replace(/&/g, '&')
|
||||||
|
.replace(/</g, '<')
|
||||||
|
.replace(/>/g, '>')
|
||||||
|
.replace(/"/g, '"');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
return { init, refresh, onDogChange };
|
||||||
|
|
||||||
|
})();
|
||||||
|
|
@ -43,9 +43,11 @@ window.Page_social = (() => {
|
||||||
function _render() {
|
function _render() {
|
||||||
const lvlBar = _stats ? _levelBar(_stats) : '';
|
const lvlBar = _stats ? _levelBar(_stats) : '';
|
||||||
_el.innerHTML = `
|
_el.innerHTML = `
|
||||||
<div style="margin-bottom:var(--space-3)">
|
<div style="background:var(--c-surface);border-radius:var(--radius-lg);
|
||||||
<div style="display:flex;align-items:center;gap:var(--space-2);margin-bottom:var(--space-2)">
|
box-shadow:var(--shadow-sm);padding:var(--space-4);
|
||||||
<span style="font-size:1.5em">📱</span>
|
margin-bottom:var(--space-4)">
|
||||||
|
<div style="display:flex;align-items:center;gap:var(--space-3);margin-bottom:var(--space-3)">
|
||||||
|
<span style="font-size:1.6em">📱</span>
|
||||||
<div style="flex:1;min-width:0">
|
<div style="flex:1;min-width:0">
|
||||||
<div style="font-size:var(--text-base);font-weight:700">Social Media</div>
|
<div style="font-size:var(--text-base);font-weight:700">Social Media</div>
|
||||||
<div style="font-size:11px;color:var(--c-text-muted)">Luna ist dein KI-Coach</div>
|
<div style="font-size:11px;color:var(--c-text-muted)">Luna ist dein KI-Coach</div>
|
||||||
|
|
@ -60,7 +62,7 @@ window.Page_social = (() => {
|
||||||
|
|
||||||
<!-- Tabs -->
|
<!-- Tabs -->
|
||||||
<div style="display:flex;border-bottom:2px solid var(--c-border);
|
<div style="display:flex;border-bottom:2px solid var(--c-border);
|
||||||
margin-bottom:var(--space-3)">
|
margin-bottom:var(--space-4)">
|
||||||
${[['idee','✨ Ideen'],['archiv','📂 Archiv'],['bewerten','🔍 Prüfen']].map(([t,l]) => `
|
${[['idee','✨ Ideen'],['archiv','📂 Archiv'],['bewerten','🔍 Prüfen']].map(([t,l]) => `
|
||||||
<button class="sm-tab" data-tab="${t}" style="flex:1;padding:10px 4px;border:none;
|
<button class="sm-tab" data-tab="${t}" style="flex:1;padding:10px 4px;border:none;
|
||||||
background:none;cursor:pointer;font-size:13px;white-space:nowrap;
|
background:none;cursor:pointer;font-size:13px;white-space:nowrap;
|
||||||
|
|
@ -86,13 +88,13 @@ window.Page_social = (() => {
|
||||||
((s.xp - s.xp_current_min) / (s.xp_next - s.xp_current_min)) * 100));
|
((s.xp - s.xp_current_min) / (s.xp_next - s.xp_current_min)) * 100));
|
||||||
return `<div>
|
return `<div>
|
||||||
<div style="display:flex;justify-content:space-between;font-size:12px;
|
<div style="display:flex;justify-content:space-between;font-size:12px;
|
||||||
color:var(--c-text);margin-bottom:4px;font-weight:500">
|
color:var(--c-text);margin-bottom:6px;font-weight:500">
|
||||||
<span>${s.level}</span>
|
<span>${s.level}</span>
|
||||||
${s.next_level ? `<span style="color:var(--c-text-secondary)">${s.xp_next - s.xp} XP bis ${s.next_level}</span>` : ''}
|
${s.next_level ? `<span style="color:var(--c-text-secondary)">${s.xp_next - s.xp} XP bis ${s.next_level}</span>` : ''}
|
||||||
</div>
|
</div>
|
||||||
<div style="background:var(--c-border);border-radius:4px;height:6px;overflow:hidden">
|
<div style="background:var(--c-surface-2);border-radius:var(--radius-full);height:8px;overflow:hidden">
|
||||||
<div style="height:100%;background:var(--c-primary);width:${pct}%;
|
<div style="height:100%;background:linear-gradient(90deg,var(--c-primary),var(--c-primary-light));
|
||||||
border-radius:4px;transition:width .5s"></div>
|
width:${pct}%;border-radius:var(--radius-full);transition:width .5s"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
|
|
@ -105,14 +107,15 @@ window.Page_social = (() => {
|
||||||
|
|
||||||
el.innerHTML = `
|
el.innerHTML = `
|
||||||
<!-- Luna -->
|
<!-- Luna -->
|
||||||
<div style="background:var(--c-surface-2);
|
<div style="background:var(--c-surface);border-radius:var(--radius-lg);
|
||||||
border-radius:12px;padding:12px;margin-bottom:var(--space-3);
|
box-shadow:var(--shadow-sm);padding:var(--space-4);
|
||||||
display:flex;gap:10px;align-items:flex-start">
|
margin-bottom:var(--space-4);
|
||||||
<span style="font-size:1.8em;flex-shrink:0">🌙</span>
|
display:flex;gap:var(--space-3);align-items:flex-start">
|
||||||
|
<span style="font-size:2em;flex-shrink:0">🌙</span>
|
||||||
<div>
|
<div>
|
||||||
<div style="font-weight:700;font-size:var(--text-sm);margin-bottom:2px">
|
<div style="font-weight:700;font-size:var(--text-sm);margin-bottom:4px">
|
||||||
Hey, ich bin Luna 👋</div>
|
Hey, ich bin Luna 👋</div>
|
||||||
<div style="font-size:13px;color:var(--c-text);line-height:1.5">
|
<div style="font-size:13px;color:var(--c-text-secondary);line-height:1.6">
|
||||||
Ich schlage dir Ideen vor und erkläre warum sie funktionieren.
|
Ich schlage dir Ideen vor und erkläre warum sie funktionieren.
|
||||||
Lern Social Media richtig — nicht nur kopieren!</div>
|
Lern Social Media richtig — nicht nur kopieren!</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -120,9 +123,9 @@ window.Page_social = (() => {
|
||||||
|
|
||||||
<!-- Diversitäts-Warnung -->
|
<!-- Diversitäts-Warnung -->
|
||||||
${_diversity?.warning ? `
|
${_diversity?.warning ? `
|
||||||
<div style="background:var(--c-surface-2);border:2px solid var(--c-warning);
|
<div style="background:var(--c-warning-subtle);border:1.5px solid var(--c-warning);
|
||||||
border-radius:12px;padding:12px;margin-bottom:var(--space-3)">
|
border-radius:var(--radius-lg);padding:var(--space-4);margin-bottom:var(--space-4)">
|
||||||
<div style="display:flex;gap:8px;align-items:flex-start">
|
<div style="display:flex;gap:var(--space-3);align-items:flex-start">
|
||||||
<span style="font-size:1.3em;flex-shrink:0">⚠️</span>
|
<span style="font-size:1.3em;flex-shrink:0">⚠️</span>
|
||||||
<div>
|
<div>
|
||||||
<div style="font-weight:700;font-size:var(--text-sm);color:var(--c-warning);
|
<div style="font-weight:700;font-size:var(--text-sm);color:var(--c-warning);
|
||||||
|
|
@ -139,64 +142,74 @@ window.Page_social = (() => {
|
||||||
</div>` : ''}
|
</div>` : ''}
|
||||||
|
|
||||||
<!-- Vorschläge -->
|
<!-- Vorschläge -->
|
||||||
<div style="margin-bottom:var(--space-4)">
|
<div style="background:var(--c-surface);border-radius:var(--radius-lg);
|
||||||
|
box-shadow:var(--shadow-sm);padding:var(--space-4);
|
||||||
|
margin-bottom:var(--space-4)">
|
||||||
<div style="display:flex;align-items:center;justify-content:space-between;
|
<div style="display:flex;align-items:center;justify-content:space-between;
|
||||||
margin-bottom:var(--space-2)">
|
margin-bottom:var(--space-3)">
|
||||||
<div style="font-weight:600;font-size:var(--text-sm)">💡 Was könntest du heute posten?</div>
|
<div style="font-weight:700;font-size:var(--text-sm)">💡 Was könntest du heute posten?</div>
|
||||||
<button id="sm-refresh" class="btn btn-sm btn-secondary"
|
<button id="sm-refresh" class="btn btn-sm btn-secondary"
|
||||||
style="font-size:11px;padding:3px 10px;min-height:30px">↻ Neue</button>
|
style="font-size:11px;padding:4px 12px;min-height:32px;
|
||||||
|
border-radius:var(--radius-full)">↻ Neue</button>
|
||||||
</div>
|
</div>
|
||||||
<div id="sm-suggestions">
|
<div id="sm-suggestions">
|
||||||
${_lunaThinking('Klein...')}
|
${_lunaThinking('Klein...')}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style="border-top:1px solid var(--c-border);padding-top:var(--space-3);
|
<div style="display:flex;align-items:center;gap:var(--space-3);
|
||||||
margin-bottom:var(--space-2)">
|
margin-bottom:var(--space-3)">
|
||||||
<div style="font-weight:600;font-size:var(--text-sm)">✏️ Oder eigenes Thema</div>
|
<div style="flex:1;height:1px;background:var(--c-border)"></div>
|
||||||
|
<div style="font-weight:600;font-size:var(--text-sm);color:var(--c-text-muted);
|
||||||
|
white-space:nowrap">✏️ Oder eigenes Thema</div>
|
||||||
|
<div style="flex:1;height:1px;background:var(--c-border)"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Formular -->
|
<!-- Formular -->
|
||||||
<div class="card" style="padding:var(--space-3)">
|
<div style="background:var(--c-surface);border-radius:var(--radius-lg);
|
||||||
|
box-shadow:var(--shadow-sm);padding:var(--space-4)">
|
||||||
<!-- Plattform -->
|
<!-- Plattform -->
|
||||||
<div style="margin-bottom:var(--space-3)">
|
<div style="margin-bottom:var(--space-4)">
|
||||||
<div class="sm-label">Plattform</div>
|
<div class="sm-label">Plattform</div>
|
||||||
<div style="display:flex;gap:var(--space-2)">
|
<div style="display:flex;gap:var(--space-2)">
|
||||||
${['both','instagram','tiktok'].map((p,i) => `
|
${['both','instagram','tiktok'].map((p,i) => `
|
||||||
<button class="btn btn-sm sm-plat ${i===0?'btn-primary':'btn-secondary'}"
|
<button class="btn btn-sm sm-plat ${i===0?'btn-primary':'btn-secondary'}"
|
||||||
data-p="${p}" style="flex:1;min-height:38px;font-size:12px;padding:4px">
|
data-p="${p}" style="flex:1;min-height:36px;font-size:12px;padding:4px 8px;
|
||||||
|
border-radius:var(--radius-full)">
|
||||||
${_PL[p]}</button>`).join('')}
|
${_PL[p]}</button>`).join('')}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- Format -->
|
<!-- Format -->
|
||||||
<div style="margin-bottom:var(--space-3)">
|
<div style="margin-bottom:var(--space-4)">
|
||||||
<div class="sm-label">Format</div>
|
<div class="sm-label">Format</div>
|
||||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:var(--space-2)">
|
<div style="display:grid;grid-template-columns:1fr 1fr;gap:var(--space-2)">
|
||||||
${['post','reel','story','carousel'].map((f,i) => `
|
${['post','reel','story','carousel'].map((f,i) => `
|
||||||
<button class="btn btn-sm sm-fmt ${i===0?'btn-primary':'btn-secondary'}"
|
<button class="btn btn-sm sm-fmt ${i===0?'btn-primary':'btn-secondary'}"
|
||||||
data-f="${f}" style="min-height:38px;font-size:12px;padding:4px">
|
data-f="${f}" style="min-height:36px;font-size:12px;padding:4px 8px;
|
||||||
|
border-radius:var(--radius-full)">
|
||||||
${_FL[f]}</button>`).join('')}
|
${_FL[f]}</button>`).join('')}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- Thema -->
|
<!-- Thema -->
|
||||||
<div style="margin-bottom:var(--space-3)">
|
<div style="margin-bottom:var(--space-4)">
|
||||||
<div class="sm-label">Thema</div>
|
<div class="sm-label">Thema</div>
|
||||||
<textarea id="sm-topic" rows="3"
|
<textarea id="sm-topic" rows="3"
|
||||||
placeholder="z.B. Mein Hund beim ersten Schnee 🐾"
|
placeholder="z.B. Mein Hund beim ersten Schnee 🐾"
|
||||||
style="width:100%;font-size:var(--text-sm);resize:none;line-height:1.5;
|
style="width:100%;font-size:var(--text-sm);resize:none;line-height:1.5;
|
||||||
background:var(--c-surface-2);color:var(--c-text);
|
background:var(--c-surface-2);color:var(--c-text);
|
||||||
border:1.5px solid var(--c-border);border-radius:8px;
|
border:1.5px solid var(--c-border);border-radius:var(--radius-md);
|
||||||
padding:10px 12px;box-sizing:border-box;
|
padding:10px 12px;box-sizing:border-box;
|
||||||
font-family:inherit"></textarea>
|
font-family:inherit"></textarea>
|
||||||
</div>
|
</div>
|
||||||
<!-- "Was jetzt?"-Banner -->
|
<!-- "Was jetzt?"-Banner -->
|
||||||
<div id="sm-next-hint" style="display:none;background:var(--c-primary);
|
<div id="sm-next-hint" style="display:none;background:var(--c-primary);
|
||||||
color:#fff;border-radius:8px;padding:10px 12px;margin-bottom:var(--space-3);
|
color:#fff;border-radius:var(--radius-md);padding:10px 14px;
|
||||||
|
margin-bottom:var(--space-4);
|
||||||
font-size:var(--text-sm);font-weight:600;text-align:center">
|
font-size:var(--text-sm);font-weight:600;text-align:center">
|
||||||
✓ Idee übernommen — prüf die Einstellungen und tippe auf <strong>Los geht's!</strong> 👇
|
✓ Idee übernommen — prüf die Einstellungen und tippe auf <strong>Los geht's!</strong> 👇
|
||||||
</div>
|
</div>
|
||||||
<!-- Medien-Upload -->
|
<!-- Medien-Upload -->
|
||||||
<div style="margin-bottom:var(--space-3)">
|
<div style="margin-bottom:var(--space-4)">
|
||||||
<div class="sm-label">Foto / Video (optional)</div>
|
<div class="sm-label">Foto / Video (optional)</div>
|
||||||
<div style="display:flex;gap:var(--space-2);flex-wrap:wrap">
|
<div style="display:flex;gap:var(--space-2);flex-wrap:wrap">
|
||||||
<label style="cursor:pointer;flex:1">
|
<label style="cursor:pointer;flex:1">
|
||||||
|
|
@ -204,7 +217,7 @@ window.Page_social = (() => {
|
||||||
capture="environment" style="display:none">
|
capture="environment" style="display:none">
|
||||||
<span class="btn btn-secondary btn-sm"
|
<span class="btn btn-secondary btn-sm"
|
||||||
style="min-height:40px;display:flex;align-items:center;justify-content:center;
|
style="min-height:40px;display:flex;align-items:center;justify-content:center;
|
||||||
gap:6px;font-size:12px;width:100%">
|
gap:6px;font-size:12px;width:100%;border-radius:var(--radius-full)">
|
||||||
📷 Kamera / Mediathek</span>
|
📷 Kamera / Mediathek</span>
|
||||||
</label>
|
</label>
|
||||||
<label style="cursor:pointer;flex:1">
|
<label style="cursor:pointer;flex:1">
|
||||||
|
|
@ -212,32 +225,35 @@ window.Page_social = (() => {
|
||||||
style="display:none">
|
style="display:none">
|
||||||
<span class="btn btn-secondary btn-sm"
|
<span class="btn btn-secondary btn-sm"
|
||||||
style="min-height:40px;display:flex;align-items:center;justify-content:center;
|
style="min-height:40px;display:flex;align-items:center;justify-content:center;
|
||||||
gap:6px;font-size:12px;width:100%">
|
gap:6px;font-size:12px;width:100%;border-radius:var(--radius-full)">
|
||||||
📁 Dateien</span>
|
📁 Dateien</span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div id="sm-media-preview" style="display:none;margin-top:8px;
|
<div id="sm-media-preview" style="display:none;margin-top:8px;
|
||||||
max-width:100px;max-height:100px;border-radius:8px;overflow:hidden"></div>
|
max-width:100px;max-height:100px;border-radius:var(--radius-md);overflow:hidden"></div>
|
||||||
</div>
|
</div>
|
||||||
<!-- Rasse — Luna-Vorschlag + Suche -->
|
<!-- Rasse — Luna-Vorschlag + Suche -->
|
||||||
<div style="margin-bottom:var(--space-3)">
|
<div style="margin-bottom:var(--space-4)">
|
||||||
<div class="sm-label">Rasse (optional)</div>
|
<div class="sm-label">Rasse (optional)</div>
|
||||||
${_unusedBreeds.length ? `
|
${_unusedBreeds.length ? `
|
||||||
<div style="font-size:11px;color:var(--c-text-muted);margin-bottom:6px">
|
<div style="font-size:11px;color:var(--c-text-muted);margin-bottom:8px">
|
||||||
🌙 Noch nicht gezeigt:
|
🌙 Noch nicht gezeigt:
|
||||||
|
<div style="display:flex;flex-wrap:wrap;gap:6px;margin-top:6px">
|
||||||
${_unusedBreeds.map(b =>
|
${_unusedBreeds.map(b =>
|
||||||
`<button class="sm-breed-chip" data-id="${b.id}" data-name="${_esc(b.name)}"
|
`<button class="sm-breed-chip" data-id="${b.id}" data-name="${_esc(b.name)}"
|
||||||
style="display:inline-block;margin:2px 4px 2px 0;padding:3px 10px;
|
style="padding:5px 12px;border-radius:var(--radius-full);
|
||||||
border-radius:20px;border:1.5px solid var(--c-border);
|
border:1.5px solid var(--c-border);
|
||||||
background:var(--c-surface-2);color:var(--c-text);
|
background:var(--c-surface-2);color:var(--c-text);
|
||||||
font-size:11px;cursor:pointer;font-family:inherit">
|
font-size:12px;cursor:pointer;font-family:inherit;
|
||||||
|
transition:all var(--transition-fast)">
|
||||||
${_esc(b.name)}</button>`).join('')}
|
${_esc(b.name)}</button>`).join('')}
|
||||||
|
</div>
|
||||||
</div>` : ''}
|
</div>` : ''}
|
||||||
<input id="sm-breed-search" list="sm-breed-list"
|
<input id="sm-breed-search" list="sm-breed-list"
|
||||||
placeholder="Rasse suchen oder leer lassen…"
|
placeholder="Rasse suchen oder leer lassen…"
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
style="width:100%;background:var(--c-surface-2);color:var(--c-text);
|
style="width:100%;background:var(--c-surface-2);color:var(--c-text);
|
||||||
border:1.5px solid var(--c-border);border-radius:8px;
|
border:1.5px solid var(--c-border);border-radius:var(--radius-md);
|
||||||
padding:9px 12px;font-size:var(--text-sm);
|
padding:9px 12px;font-size:var(--text-sm);
|
||||||
font-family:inherit;box-sizing:border-box">
|
font-family:inherit;box-sizing:border-box">
|
||||||
<datalist id="sm-breed-list">
|
<datalist id="sm-breed-list">
|
||||||
|
|
@ -245,35 +261,63 @@ window.Page_social = (() => {
|
||||||
</datalist>
|
</datalist>
|
||||||
<input type="hidden" id="sm-breed-id">
|
<input type="hidden" id="sm-breed-id">
|
||||||
</div>
|
</div>
|
||||||
<button id="sm-training-tip" class="btn btn-secondary"
|
|
||||||
style="width:100%;min-height:44px;font-size:var(--text-sm);
|
<!-- Generier-Buttons als Cards -->
|
||||||
margin-bottom:4px;border:1.5px solid #10b981;color:#10b981">
|
<div class="sm-label" style="margin-bottom:var(--space-3)">Schnell generieren</div>
|
||||||
🎾 Trainingstipp generieren
|
<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:var(--space-2);
|
||||||
<span style="font-size:10px;opacity:.7;margin-left:6px">104 Übungen</span>
|
margin-bottom:var(--space-3)">
|
||||||
|
<button id="sm-breed-day"
|
||||||
|
style="display:flex;flex-direction:column;align-items:center;
|
||||||
|
gap:6px;padding:var(--space-3) var(--space-2);
|
||||||
|
background:var(--c-primary-subtle);border:1.5px solid var(--c-primary-light);
|
||||||
|
border-radius:var(--radius-lg);cursor:pointer;
|
||||||
|
font-family:inherit;transition:all var(--transition-fast);
|
||||||
|
box-shadow:var(--shadow-xs)">
|
||||||
|
<span style="font-size:1.8em">🐾</span>
|
||||||
|
<span style="font-size:11px;font-weight:600;color:var(--c-primary-dark);
|
||||||
|
text-align:center;line-height:1.3">Rasse des Tages</span>
|
||||||
|
${_unusedBreeds.length ? `<span style="font-size:10px;color:var(--c-text-muted)">${_unusedBreeds.length} übrig</span>` : ''}
|
||||||
</button>
|
</button>
|
||||||
<button id="sm-pflege-tip" class="btn btn-secondary"
|
<button id="sm-training-tip"
|
||||||
style="width:100%;min-height:44px;font-size:var(--text-sm);
|
style="display:flex;flex-direction:column;align-items:center;
|
||||||
margin-bottom:4px;border:1.5px solid #a78bfa;color:#a78bfa">
|
gap:6px;padding:var(--space-3) var(--space-2);
|
||||||
🛁 Pflegetipp generieren
|
background:#f0fdf4;border:1.5px solid #86efac;
|
||||||
<span style="font-size:10px;opacity:.7;margin-left:6px">allg. oder für gewählte Rasse</span>
|
border-radius:var(--radius-lg);cursor:pointer;
|
||||||
|
font-family:inherit;transition:all var(--transition-fast);
|
||||||
|
box-shadow:var(--shadow-xs)">
|
||||||
|
<span style="font-size:1.8em">🎾</span>
|
||||||
|
<span style="font-size:11px;font-weight:600;color:#15803d;
|
||||||
|
text-align:center;line-height:1.3">Trainingstipp</span>
|
||||||
|
<span style="font-size:10px;color:#4ade80">104 Übungen</span>
|
||||||
</button>
|
</button>
|
||||||
<button id="sm-show-exercises" class="btn btn-secondary"
|
<button id="sm-pflege-tip"
|
||||||
style="width:100%;min-height:36px;font-size:11px;
|
style="display:flex;flex-direction:column;align-items:center;
|
||||||
margin-bottom:8px;color:var(--c-text-muted)">
|
gap:6px;padding:var(--space-3) var(--space-2);
|
||||||
📋 Alle Übungen ansehen
|
background:#faf5ff;border:1.5px solid #d8b4fe;
|
||||||
|
border-radius:var(--radius-lg);cursor:pointer;
|
||||||
|
font-family:inherit;transition:all var(--transition-fast);
|
||||||
|
box-shadow:var(--shadow-xs)">
|
||||||
|
<span style="font-size:1.8em">🛁</span>
|
||||||
|
<span style="font-size:11px;font-weight:600;color:#7c3aed;
|
||||||
|
text-align:center;line-height:1.3">Pflegetipp</span>
|
||||||
|
<span style="font-size:10px;color:#c084fc">je Rasse</span>
|
||||||
</button>
|
</button>
|
||||||
<button id="sm-breed-day" class="btn btn-secondary"
|
</div>
|
||||||
style="width:100%;min-height:44px;font-size:var(--text-sm);
|
<button id="sm-show-exercises"
|
||||||
margin-bottom:8px;border:1.5px solid var(--c-primary);
|
style="width:100%;min-height:36px;font-size:11px;cursor:pointer;
|
||||||
color:var(--c-primary)">
|
margin-bottom:var(--space-4);color:var(--c-text-muted);
|
||||||
🐾 Rasse des Tages generieren
|
background:none;border:1px dashed var(--c-border);
|
||||||
${_unusedBreeds.length ? `<span style="font-size:10px;opacity:.7;margin-left:6px">(${_breeds.length - (_breeds.length - _unusedBreeds.length)} noch übrig)</span>` : ''}
|
border-radius:var(--radius-md);font-family:inherit;
|
||||||
|
transition:all var(--transition-fast)">
|
||||||
|
📋 Alle 104 Übungen ansehen →
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button id="sm-gen" class="btn btn-primary"
|
<button id="sm-gen" class="btn btn-primary"
|
||||||
style="width:100%;min-height:48px;font-size:var(--text-base)">
|
style="width:100%;min-height:52px;font-size:var(--text-base);
|
||||||
|
border-radius:var(--radius-lg);box-shadow:var(--shadow-md)">
|
||||||
✨ Los geht's!
|
✨ Los geht's!
|
||||||
</button>
|
</button>
|
||||||
<div id="sm-gen-result" style="margin-top:var(--space-3)"></div>
|
<div id="sm-gen-result" style="margin-top:var(--space-4)"></div>
|
||||||
</div>`;
|
</div>`;
|
||||||
|
|
||||||
// Platform toggle
|
// Platform toggle
|
||||||
|
|
@ -497,13 +541,15 @@ window.Page_social = (() => {
|
||||||
await new Promise(r => setTimeout(r, 400));
|
await new Promise(r => setTimeout(r, 400));
|
||||||
const stilLabel = {tutorial:'📹 Tutorial', community:'🙋 Community', aspirational:'💪 Aspirational'}[data.stil] || '';
|
const stilLabel = {tutorial:'📹 Tutorial', community:'🙋 Community', aspirational:'💪 Aspirational'}[data.stil] || '';
|
||||||
res.innerHTML = `
|
res.innerHTML = `
|
||||||
<div style="background:var(--c-surface-2);border-radius:10px;padding:10px;
|
<div style="background:#f0fdf4;border:1px solid #86efac;
|
||||||
margin-bottom:10px;display:flex;gap:10px;align-items:center">
|
border-radius:var(--radius-lg);padding:var(--space-4);
|
||||||
<span style="font-size:2.5em;flex-shrink:0">🎾</span>
|
margin-bottom:var(--space-3);display:flex;gap:var(--space-3);align-items:center;
|
||||||
|
box-shadow:var(--shadow-xs)">
|
||||||
|
<span style="font-size:2.2em;flex-shrink:0">🎾</span>
|
||||||
<div>
|
<div>
|
||||||
<div style="font-size:11px;color:var(--c-text-muted)">
|
<div style="font-size:11px;color:#4ade80;font-weight:600;margin-bottom:2px">
|
||||||
Trainingstipp · ${_esc(data.exercise_kat||'')} · ${stilLabel}</div>
|
Trainingstipp · ${_esc(data.exercise_kat||'')} · ${stilLabel}</div>
|
||||||
<div style="font-weight:700;font-size:var(--text-base)">
|
<div style="font-weight:700;font-size:var(--text-base);color:#15803d">
|
||||||
${_esc(data.exercise_name||'')}</div>
|
${_esc(data.exercise_name||'')}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -535,14 +581,16 @@ window.Page_social = (() => {
|
||||||
_progressDone(res);
|
_progressDone(res);
|
||||||
await new Promise(r => setTimeout(r, 400));
|
await new Promise(r => setTimeout(r, 400));
|
||||||
res.innerHTML = `
|
res.innerHTML = `
|
||||||
<div style="background:var(--c-surface-2);border-radius:10px;padding:10px;
|
<div style="background:#faf5ff;border:1px solid #d8b4fe;
|
||||||
margin-bottom:10px;display:flex;gap:10px;align-items:center">
|
border-radius:var(--radius-lg);padding:var(--space-4);
|
||||||
<span style="font-size:2em;flex-shrink:0">🛁</span>
|
margin-bottom:var(--space-3);display:flex;gap:var(--space-3);align-items:center;
|
||||||
|
box-shadow:var(--shadow-xs)">
|
||||||
|
<span style="font-size:2.2em;flex-shrink:0">🛁</span>
|
||||||
<div>
|
<div>
|
||||||
<div style="font-size:11px;color:var(--c-text-muted)">
|
<div style="font-size:11px;color:#c084fc;font-weight:600;margin-bottom:2px">
|
||||||
Pflegetipp · ${_esc(data.pflege_kat||'')}
|
Pflegetipp · ${_esc(data.pflege_kat||'')}
|
||||||
${data.rasse_name ? ` · speziell für ${_esc(data.rasse_name)}` : ''}</div>
|
${data.rasse_name ? ` · speziell für ${_esc(data.rasse_name)}` : ''}</div>
|
||||||
<div style="font-weight:700;font-size:var(--text-base)">
|
<div style="font-weight:700;font-size:var(--text-base);color:#7c3aed">
|
||||||
${_esc(data.pflege_titel||'')}</div>
|
${_esc(data.pflege_titel||'')}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -572,14 +620,18 @@ window.Page_social = (() => {
|
||||||
// Foto anzeigen wenn vorhanden
|
// Foto anzeigen wenn vorhanden
|
||||||
const mediaUrl = data.breed_foto || data.media_url || null;
|
const mediaUrl = data.breed_foto || data.media_url || null;
|
||||||
res.innerHTML = `
|
res.innerHTML = `
|
||||||
<div style="background:var(--c-surface-2);border-radius:10px;padding:10px;
|
<div style="background:var(--c-primary-subtle);border:1px solid var(--c-primary-light);
|
||||||
margin-bottom:10px;display:flex;gap:10px;align-items:center">
|
border-radius:var(--radius-lg);padding:var(--space-4);
|
||||||
|
margin-bottom:var(--space-3);display:flex;gap:var(--space-3);align-items:center;
|
||||||
|
box-shadow:var(--shadow-xs)">
|
||||||
${mediaUrl ? `<img src="${mediaUrl}"
|
${mediaUrl ? `<img src="${mediaUrl}"
|
||||||
style="width:64px;height:64px;border-radius:8px;object-fit:cover;flex-shrink:0"
|
style="width:60px;height:60px;border-radius:var(--radius-md);object-fit:cover;flex-shrink:0"
|
||||||
onerror="this.style.display='none'">` : '<span style="font-size:2.5em">🐶</span>'}
|
onerror="this.style.display='none'">` : '<span style="font-size:2.2em">🐶</span>'}
|
||||||
<div>
|
<div>
|
||||||
<div style="font-size:11px;color:var(--c-text-muted)">Rasse des Tages</div>
|
<div style="font-size:11px;color:var(--c-primary);font-weight:600;margin-bottom:2px">
|
||||||
<div style="font-weight:700;font-size:var(--text-base)">${_esc(data.topic?.replace('Rasse des Tages: ',''))}</div>
|
Rasse des Tages</div>
|
||||||
|
<div style="font-weight:700;font-size:var(--text-base);color:var(--c-primary-dark)">
|
||||||
|
${_esc(data.topic?.replace('Rasse des Tages: ',''))}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
${_renderResult(data, mediaUrl)}`;
|
${_renderResult(data, mediaUrl)}`;
|
||||||
|
|
@ -724,33 +776,43 @@ window.Page_social = (() => {
|
||||||
|
|
||||||
return `
|
return `
|
||||||
${data.coaching ? `
|
${data.coaching ? `
|
||||||
<div style="background:var(--c-surface-2);
|
<div style="background:var(--c-primary-subtle);
|
||||||
border-radius:12px;padding:12px;margin-bottom:12px;
|
border-radius:var(--radius-lg);padding:var(--space-4);margin-bottom:var(--space-3);
|
||||||
border-left:3px solid var(--c-primary)">
|
border-left:4px solid var(--c-primary)">
|
||||||
<div style="display:flex;gap:8px">
|
<div style="display:flex;gap:var(--space-3)">
|
||||||
<span style="font-size:1.2em;flex-shrink:0">🌙</span>
|
<span style="font-size:1.3em;flex-shrink:0">🌙</span>
|
||||||
<div>
|
<div>
|
||||||
<div style="font-size:11px;font-weight:700;color:var(--c-primary);margin-bottom:3px">
|
<div style="font-size:11px;font-weight:700;color:var(--c-primary);margin-bottom:4px;
|
||||||
Luna sagt:</div>
|
text-transform:uppercase;letter-spacing:.5px">Luna sagt:</div>
|
||||||
<div style="font-size:var(--text-sm);line-height:1.5;color:var(--c-text)">${_esc(data.coaching)}</div>
|
<div style="font-size:var(--text-sm);line-height:1.6;color:var(--c-text)">${_esc(data.coaching)}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>` : ''}
|
</div>` : ''}
|
||||||
|
|
||||||
<div style="display:flex;align-items:center;gap:8px;margin-bottom:12px;flex-wrap:wrap">
|
<div style="display:flex;align-items:center;gap:var(--space-2);
|
||||||
<span style="background:#f0fdf4;color:var(--c-success);border-radius:8px;
|
margin-bottom:var(--space-3);flex-wrap:wrap">
|
||||||
padding:4px 10px;font-size:11px;font-weight:600">✓ Gespeichert</span>
|
<span style="background:var(--c-success-subtle);color:var(--c-success);
|
||||||
${score ? `<span>${score}</span>` : ''}
|
border-radius:var(--radius-full);
|
||||||
<button class="btn btn-sm btn-secondary sm-preview-btn"
|
padding:4px 12px;font-size:11px;font-weight:700">✓ Gespeichert</span>
|
||||||
|
${score ? `<span style="font-size:13px">${score}</span>` : ''}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Aktions-Buttons: Primär volle Breite, Sekundär nebeneinander -->
|
||||||
|
<div style="margin-bottom:var(--space-4)">
|
||||||
|
<button class="btn btn-primary sm-posted-btn"
|
||||||
data-id="${data.id}"
|
data-id="${data.id}"
|
||||||
style="font-size:11px;padding:4px 10px;min-height:30px">
|
style="width:100%;min-height:48px;font-size:var(--text-sm);
|
||||||
👁 Vorschau</button>
|
margin-bottom:var(--space-2);border-radius:var(--radius-lg);
|
||||||
<button class="btn btn-sm btn-primary sm-posted-btn"
|
background:#10b981;border-color:#10b981;box-shadow:var(--shadow-sm)">
|
||||||
data-id="${data.id}"
|
|
||||||
style="margin-left:auto;font-size:11px;padding:4px 12px;min-height:30px;
|
|
||||||
background:#10b981;border-color:#10b981">
|
|
||||||
📤 Habe ich gepostet!
|
📤 Habe ich gepostet!
|
||||||
</button>
|
</button>
|
||||||
|
<div style="display:flex;gap:var(--space-2)">
|
||||||
|
<button class="btn btn-sm btn-secondary sm-preview-btn"
|
||||||
|
data-id="${data.id}"
|
||||||
|
style="flex:1;font-size:12px;padding:6px 10px;min-height:36px;
|
||||||
|
border-radius:var(--radius-full)">
|
||||||
|
👁 Vorschau</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="sm-posted-form-${data.id}" style="display:none;background:var(--c-surface-2);
|
<div id="sm-posted-form-${data.id}" style="display:none;background:var(--c-surface-2);
|
||||||
border-radius:10px;padding:12px;margin-bottom:12px">
|
border-radius:10px;padding:12px;margin-bottom:12px">
|
||||||
|
|
@ -782,49 +844,62 @@ window.Page_social = (() => {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
${mediaUrl ? `
|
${mediaUrl ? `
|
||||||
<div class="card" style="padding:12px;margin-bottom:10px">
|
<div style="background:var(--c-surface);border:1px solid var(--c-border);
|
||||||
|
border-radius:var(--radius-lg);padding:var(--space-4);
|
||||||
|
margin-bottom:var(--space-3);box-shadow:var(--shadow-xs)">
|
||||||
<div class="sm-label">📎 Dein Medien-Upload</div>
|
<div class="sm-label">📎 Dein Medien-Upload</div>
|
||||||
<img src="${mediaUrl}" style="max-width:100%;max-height:200px;border-radius:8px;
|
<img src="${mediaUrl}" style="max-width:100%;max-height:200px;
|
||||||
object-fit:cover;margin-top:6px" onerror="this.style.display='none'">
|
border-radius:var(--radius-md);object-fit:cover;margin-top:8px"
|
||||||
|
onerror="this.style.display='none'">
|
||||||
</div>` : ''}
|
</div>` : ''}
|
||||||
|
|
||||||
${_resultBlock('📝 Caption', data.caption, true)}
|
${_resultBlock('📝 Caption', data.caption, true)}
|
||||||
${data.hashtags ? `
|
${data.hashtags ? `
|
||||||
<div class="card" style="padding:12px;margin-bottom:10px">
|
<div style="background:var(--c-surface);border:1px solid var(--c-border);
|
||||||
|
border-radius:var(--radius-lg);padding:var(--space-4);
|
||||||
|
margin-bottom:var(--space-3);box-shadow:var(--shadow-xs)">
|
||||||
<div class="sm-label">🏷 Hashtags</div>
|
<div class="sm-label">🏷 Hashtags</div>
|
||||||
<div style="font-size:var(--text-sm);color:var(--c-primary);margin-bottom:8px;
|
<div style="font-size:var(--text-sm);color:var(--c-primary);margin-bottom:var(--space-3);
|
||||||
line-height:1.8;word-break:break-word">
|
line-height:1.9;word-break:break-word">
|
||||||
${data.hashtags.split(',').map(h=>`#${h.trim()}`).join(' ')}</div>
|
${data.hashtags.split(',').map(h=>`#${h.trim()}`).join(' ')}</div>
|
||||||
${_copyBtn(data.hashtags.split(',').map(h=>`#${h.trim()}`).join(' '))}
|
${_copyBtn(data.hashtags.split(',').map(h=>`#${h.trim()}`).join(' '))}
|
||||||
</div>` : ''}
|
</div>` : ''}
|
||||||
${(data.hook||data.cta) ? `
|
${(data.hook||data.cta) ? `
|
||||||
<div class="card" style="padding:12px;margin-bottom:10px">
|
<div style="background:var(--c-surface);border:1px solid var(--c-border);
|
||||||
|
border-radius:var(--radius-lg);padding:var(--space-4);
|
||||||
|
margin-bottom:var(--space-3);box-shadow:var(--shadow-xs)">
|
||||||
${data.hook ? `<div class="sm-label">🎣 Hook</div>
|
${data.hook ? `<div class="sm-label">🎣 Hook</div>
|
||||||
<div style="font-size:var(--text-sm);font-style:italic;margin-bottom:8px">
|
<div style="font-size:var(--text-sm);font-style:italic;margin-bottom:var(--space-3);
|
||||||
|
line-height:1.6">
|
||||||
"${_esc(data.hook)}"</div>` : ''}
|
"${_esc(data.hook)}"</div>` : ''}
|
||||||
${data.cta ? `<div class="sm-label">📣 Call-to-Action</div>
|
${data.cta ? `<div class="sm-label">📣 Call-to-Action</div>
|
||||||
<div style="font-size:var(--text-sm)">${_esc(data.cta)}</div>` : ''}
|
<div style="font-size:var(--text-sm);line-height:1.6">${_esc(data.cta)}</div>` : ''}
|
||||||
</div>` : ''}
|
</div>` : ''}
|
||||||
${_resultBlock('📸 Was du filmen/fotografieren solltest', data.visual_brief, false)}
|
${_resultBlock('📸 Was du filmen/fotografieren solltest', data.visual_brief, false)}
|
||||||
${data.script ? `
|
${data.script ? `
|
||||||
<div class="card" style="padding:12px;margin-bottom:10px">
|
<div style="background:var(--c-surface);border:1px solid var(--c-border);
|
||||||
|
border-radius:var(--radius-lg);padding:var(--space-4);
|
||||||
|
margin-bottom:var(--space-3);box-shadow:var(--shadow-xs)">
|
||||||
<div class="sm-label">🎬 Video-Aufbau</div>
|
<div class="sm-label">🎬 Video-Aufbau</div>
|
||||||
<div style="font-size:var(--text-sm);white-space:pre-wrap;
|
<div style="font-size:var(--text-sm);white-space:pre-wrap;
|
||||||
line-height:1.5">${_esc(data.script)}</div>
|
line-height:1.7">${_esc(data.script)}</div>
|
||||||
</div>` : ''}
|
</div>` : ''}
|
||||||
${(data.image_prompt||data.canva_notes||unsplash) ? `
|
${(data.image_prompt||data.canva_notes||unsplash) ? `
|
||||||
<div class="card" style="padding:12px;margin-bottom:10px">
|
<div style="background:var(--c-surface);border:1px solid var(--c-border);
|
||||||
|
border-radius:var(--radius-lg);padding:var(--space-4);
|
||||||
|
margin-bottom:var(--space-3);box-shadow:var(--shadow-xs)">
|
||||||
<div class="sm-label">🛠 Wenn du kein eigenes Bild hast</div>
|
<div class="sm-label">🛠 Wenn du kein eigenes Bild hast</div>
|
||||||
${data.image_prompt ? `
|
${data.image_prompt ? `
|
||||||
<div style="font-size:11px;color:var(--c-text-muted);margin-bottom:4px">
|
<div style="font-size:11px;color:var(--c-text-muted);margin-bottom:6px">
|
||||||
DALL-E / Midjourney:</div>
|
DALL-E / Midjourney:</div>
|
||||||
<div style="font-size:11px;background:var(--c-surface-2);padding:8px;
|
<div style="font-size:11px;background:var(--c-surface-2);padding:10px;
|
||||||
border-radius:6px;font-family:monospace;word-break:break-word;
|
border-radius:var(--radius-md);font-family:monospace;word-break:break-word;
|
||||||
margin-bottom:8px">${_esc(data.image_prompt)}</div>
|
margin-bottom:var(--space-3);line-height:1.5">${_esc(data.image_prompt)}</div>
|
||||||
${_copyBtn(data.image_prompt)}` : ''}
|
${_copyBtn(data.image_prompt)}` : ''}
|
||||||
${data.canva_notes ? `
|
${data.canva_notes ? `
|
||||||
<div style="font-size:11px;color:var(--c-text-muted);margin:8px 0 4px">Canva:</div>
|
<div style="font-size:11px;color:var(--c-text-muted);margin:var(--space-3) 0 6px">Canva:</div>
|
||||||
<div style="font-size:var(--text-sm);margin-bottom:8px">${_esc(data.canva_notes)}</div>` : ''}
|
<div style="font-size:var(--text-sm);margin-bottom:var(--space-3);
|
||||||
|
line-height:1.6">${_esc(data.canva_notes)}</div>` : ''}
|
||||||
${unsplash ? `<a href="${unsplash}" target="_blank" rel="noopener"
|
${unsplash ? `<a href="${unsplash}" target="_blank" rel="noopener"
|
||||||
style="font-size:var(--text-sm);color:var(--c-primary);display:inline-block">
|
style="font-size:var(--text-sm);color:var(--c-primary);display:inline-block">
|
||||||
🔍 Kostenlose Fotos auf Unsplash →</a>` : ''}
|
🔍 Kostenlose Fotos auf Unsplash →</a>` : ''}
|
||||||
|
|
@ -833,10 +908,12 @@ window.Page_social = (() => {
|
||||||
|
|
||||||
function _resultBlock(label, text, copyable) {
|
function _resultBlock(label, text, copyable) {
|
||||||
if (!text) return '';
|
if (!text) return '';
|
||||||
return `<div class="card" style="padding:12px;margin-bottom:10px">
|
return `<div style="background:var(--c-surface);border:1px solid var(--c-border);
|
||||||
|
border-radius:var(--radius-lg);padding:var(--space-4);
|
||||||
|
margin-bottom:var(--space-3);box-shadow:var(--shadow-xs)">
|
||||||
<div class="sm-label">${label}</div>
|
<div class="sm-label">${label}</div>
|
||||||
<div style="font-size:var(--text-sm);white-space:pre-wrap;line-height:1.6;
|
<div style="font-size:var(--text-sm);white-space:pre-wrap;line-height:1.7;
|
||||||
margin-bottom:${copyable?'8px':'0'}">${_esc(text)}</div>
|
margin-bottom:${copyable?'var(--space-3)':'0'}">${_esc(text)}</div>
|
||||||
${copyable ? _copyBtn(text) : ''}
|
${copyable ? _copyBtn(text) : ''}
|
||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
|
|
@ -844,7 +921,8 @@ window.Page_social = (() => {
|
||||||
function _copyBtn(text) {
|
function _copyBtn(text) {
|
||||||
return `<button class="btn btn-sm btn-secondary sm-copy"
|
return `<button class="btn btn-sm btn-secondary sm-copy"
|
||||||
data-copy="${_esc(text)}"
|
data-copy="${_esc(text)}"
|
||||||
style="font-size:11px;padding:4px 10px;min-height:30px">
|
style="font-size:11px;padding:5px 14px;min-height:32px;
|
||||||
|
border-radius:var(--radius-full)">
|
||||||
📋 Kopieren</button>`;
|
📋 Kopieren</button>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1023,10 +1101,11 @@ window.Page_social = (() => {
|
||||||
Tippe auf 📤 wenn du einen Post abgesetzt hast — so lernt Luna was wirklich live ging.</div>
|
Tippe auf 📤 wenn du einen Post abgesetzt hast — so lernt Luna was wirklich live ging.</div>
|
||||||
</div>
|
</div>
|
||||||
</div>` : ''}
|
</div>` : ''}
|
||||||
<div style="display:flex;gap:6px;flex-wrap:wrap;margin-bottom:var(--space-3)">
|
<div style="display:flex;gap:6px;flex-wrap:wrap;margin-bottom:var(--space-4)">
|
||||||
${['alle','idea','draft','scheduled','published','archived'].map(s => `
|
${['alle','idea','draft','scheduled','published','archived'].map(s => `
|
||||||
<button class="btn btn-sm ${filter===s?'btn-primary':'btn-secondary'}"
|
<button class="btn btn-sm ${filter===s?'btn-primary':'btn-secondary'}"
|
||||||
data-f="${s}" style="padding:3px 10px;font-size:11px;min-height:30px">
|
data-f="${s}" style="padding:4px 12px;font-size:11px;min-height:30px;
|
||||||
|
border-radius:var(--radius-full)">
|
||||||
${fLabel[s]}</button>`).join('')}
|
${fLabel[s]}</button>`).join('')}
|
||||||
</div>
|
</div>
|
||||||
${!items.length
|
${!items.length
|
||||||
|
|
@ -1139,19 +1218,22 @@ window.Page_social = (() => {
|
||||||
function _renderBewerten(el) {
|
function _renderBewerten(el) {
|
||||||
let selPlatform = 'instagram';
|
let selPlatform = 'instagram';
|
||||||
el.innerHTML = `
|
el.innerHTML = `
|
||||||
<div style="background:var(--c-surface-2);
|
<div style="background:var(--c-surface);border-radius:var(--radius-lg);
|
||||||
border-radius:12px;padding:12px;margin-bottom:var(--space-3);
|
box-shadow:var(--shadow-sm);padding:var(--space-4);
|
||||||
display:flex;gap:8px">
|
margin-bottom:var(--space-4);
|
||||||
<span style="font-size:1.2em;flex-shrink:0">🌙</span>
|
display:flex;gap:var(--space-3)">
|
||||||
<div style="font-size:var(--text-sm);color:var(--c-text);line-height:1.5">
|
<span style="font-size:1.4em;flex-shrink:0">🌙</span>
|
||||||
|
<div style="font-size:var(--text-sm);color:var(--c-text-secondary);line-height:1.6">
|
||||||
Zeig mir deinen Entwurf — ich sage dir was gut ist und wie du ihn
|
Zeig mir deinen Entwurf — ich sage dir was gut ist und wie du ihn
|
||||||
noch besser machen kannst!</div>
|
noch besser machen kannst!</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card" style="padding:var(--space-3)">
|
<div style="background:var(--c-surface);border-radius:var(--radius-lg);
|
||||||
<div style="display:flex;gap:var(--space-2);margin-bottom:var(--space-3)">
|
box-shadow:var(--shadow-sm);padding:var(--space-4)">
|
||||||
|
<div style="display:flex;gap:var(--space-2);margin-bottom:var(--space-4)">
|
||||||
${['instagram','tiktok','both'].map((p,i) => `
|
${['instagram','tiktok','both'].map((p,i) => `
|
||||||
<button class="btn btn-sm sm-ep ${i===0?'btn-primary':'btn-secondary'}"
|
<button class="btn btn-sm sm-ep ${i===0?'btn-primary':'btn-secondary'}"
|
||||||
data-p="${p}" style="flex:1;min-height:38px;font-size:12px;padding:4px">
|
data-p="${p}" style="flex:1;min-height:36px;font-size:12px;padding:4px 8px;
|
||||||
|
border-radius:var(--radius-full)">
|
||||||
${_PL[p]}</button>`).join('')}
|
${_PL[p]}</button>`).join('')}
|
||||||
</div>
|
</div>
|
||||||
<textarea id="sm-draft" class="input"
|
<textarea id="sm-draft" class="input"
|
||||||
|
|
@ -1159,11 +1241,12 @@ window.Page_social = (() => {
|
||||||
font-size:var(--text-sm);line-height:1.5"
|
font-size:var(--text-sm);line-height:1.5"
|
||||||
placeholder="Schreib hier deinen Caption-Entwurf oder einfach worum es im Post geht…"></textarea>
|
placeholder="Schreib hier deinen Caption-Entwurf oder einfach worum es im Post geht…"></textarea>
|
||||||
<button id="sm-eval" class="btn btn-primary"
|
<button id="sm-eval" class="btn btn-primary"
|
||||||
style="margin-top:var(--space-3);width:100%;min-height:48px;
|
style="margin-top:var(--space-4);width:100%;min-height:52px;
|
||||||
font-size:var(--text-base)">
|
font-size:var(--text-base);border-radius:var(--radius-lg);
|
||||||
|
box-shadow:var(--shadow-md)">
|
||||||
🔍 Luna, schau mal drüber!
|
🔍 Luna, schau mal drüber!
|
||||||
</button>
|
</button>
|
||||||
<div id="sm-eval-res" style="margin-top:var(--space-3)"></div>
|
<div id="sm-eval-res" style="margin-top:var(--space-4)"></div>
|
||||||
</div>`;
|
</div>`;
|
||||||
|
|
||||||
el.querySelectorAll('.sm-ep').forEach(b => b.addEventListener('click', () => {
|
el.querySelectorAll('.sm-ep').forEach(b => b.addEventListener('click', () => {
|
||||||
|
|
@ -1188,15 +1271,16 @@ window.Page_social = (() => {
|
||||||
_progressDone(res);
|
_progressDone(res);
|
||||||
await new Promise(r => setTimeout(r, 400));
|
await new Promise(r => setTimeout(r, 400));
|
||||||
res.innerHTML = `
|
res.innerHTML = `
|
||||||
${data.notes ? `<div style="background:var(--c-surface-2);
|
${data.notes ? `<div style="background:var(--c-primary-subtle);
|
||||||
border-radius:12px;padding:12px;margin-bottom:12px;
|
border-radius:var(--radius-lg);padding:var(--space-4);margin-bottom:var(--space-3);
|
||||||
border-left:3px solid var(--c-primary)">
|
border-left:4px solid var(--c-primary);box-shadow:var(--shadow-xs)">
|
||||||
<div style="display:flex;gap:8px">
|
<div style="display:flex;gap:var(--space-3)">
|
||||||
<span>🌙</span>
|
<span style="font-size:1.3em;flex-shrink:0">🌙</span>
|
||||||
<div>
|
<div>
|
||||||
<div style="font-size:11px;font-weight:700;color:var(--c-primary);margin-bottom:3px">
|
<div style="font-size:11px;font-weight:700;color:var(--c-primary);margin-bottom:4px;
|
||||||
|
text-transform:uppercase;letter-spacing:.5px">
|
||||||
Lunas Feedback:</div>
|
Lunas Feedback:</div>
|
||||||
<div style="font-size:var(--text-sm);line-height:1.5">${_esc(data.notes)}</div>
|
<div style="font-size:var(--text-sm);line-height:1.6">${_esc(data.notes)}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>` : ''}
|
</div>` : ''}
|
||||||
|
|
@ -1237,6 +1321,14 @@ window.Page_social = (() => {
|
||||||
.sm-label{font-size:11px;font-weight:700;color:var(--c-text-muted);
|
.sm-label{font-size:11px;font-weight:700;color:var(--c-text-muted);
|
||||||
text-transform:uppercase;letter-spacing:.5px;margin-bottom:6px;display:block}
|
text-transform:uppercase;letter-spacing:.5px;margin-bottom:6px;display:block}
|
||||||
@keyframes luna-pulse{0%,100%{transform:scale(1)}50%{transform:scale(1.15)}}
|
@keyframes luna-pulse{0%,100%{transform:scale(1)}50%{transform:scale(1.15)}}
|
||||||
|
#sm-breed-day:hover,#sm-training-tip:hover,#sm-pflege-tip:hover{
|
||||||
|
transform:translateY(-2px);box-shadow:var(--shadow-md)!important}
|
||||||
|
#sm-breed-day:active,#sm-training-tip:active,#sm-pflege-tip:active{
|
||||||
|
transform:translateY(0)}
|
||||||
|
#sm-show-exercises:hover{background:var(--c-surface-2)!important;
|
||||||
|
border-color:var(--c-border)!important;color:var(--c-text-secondary)!important}
|
||||||
|
.sm-breed-chip:hover{background:var(--c-primary-subtle)!important;
|
||||||
|
border-color:var(--c-primary)!important;color:var(--c-primary-dark)!important}
|
||||||
`;
|
`;
|
||||||
document.head.appendChild(style);
|
document.head.appendChild(style);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
Offline-Cache + Push Notifications + Tile-Cache
|
Offline-Cache + Push Notifications + Tile-Cache
|
||||||
============================================================ */
|
============================================================ */
|
||||||
|
|
||||||
const CACHE_VERSION = 'by-v356';
|
const CACHE_VERSION = 'by-v359';
|
||||||
const CACHE_STATIC = `${CACHE_VERSION}-static`;
|
const CACHE_STATIC = `${CACHE_VERSION}-static`;
|
||||||
const CACHE_TILES = 'ban-yaro-tiles-v1'; // bleibt über SW-Updates erhalten
|
const CACHE_TILES = 'ban-yaro-tiles-v1'; // bleibt über SW-Updates erhalten
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue