Major updates: LinkedIn auto-posting, timezone fixes, and Docker improvements
Features: - Add LinkedIn OAuth integration and auto-posting functionality - Add scheduler service for automated post publishing - Add metadata field to generated_posts for LinkedIn URLs - Add privacy policy page for LinkedIn API compliance - Add company management features and employee accounts - Add license key system for company registrations Fixes: - Fix timezone issues (use UTC consistently across app) - Fix datetime serialization errors in database operations - Fix scheduling timezone conversion (local time to UTC) - Fix import errors (get_database -> db) Infrastructure: - Update Docker setup to use port 8001 (avoid conflicts) - Add SSL support with nginx-proxy and Let's Encrypt - Add LinkedIn setup documentation - Add migration scripts for schema updates Services: - Add linkedin_service.py for LinkedIn API integration - Add scheduler_service.py for background job processing - Add storage_service.py for Supabase Storage - Add email_service.py improvements - Add encryption utilities for token storage Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
393
src/web/templates/user/company_manage_post_detail.html
Normal file
393
src/web/templates/user/company_manage_post_detail.html
Normal file
@@ -0,0 +1,393 @@
|
||||
{% extends "company_base.html" %}
|
||||
{% block title %}{{ post.topic_title }} - {{ employee_name }} - {{ session.company_name }}{% endblock %}
|
||||
|
||||
{% block head %}
|
||||
<style>
|
||||
.section-card {
|
||||
background: rgba(61, 72, 72, 0.3);
|
||||
border: 1px solid rgba(61, 72, 72, 0.6);
|
||||
}
|
||||
.linkedin-preview {
|
||||
background: #ffffff;
|
||||
border-radius: 8px;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
|
||||
color: rgba(0, 0, 0, 0.9);
|
||||
overflow: hidden;
|
||||
}
|
||||
.linkedin-header {
|
||||
padding: 12px 16px;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
}
|
||||
.linkedin-avatar {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(135deg, #0a66c2 0%, #004182 100%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: white;
|
||||
font-weight: 600;
|
||||
font-size: 18px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.linkedin-user-info { flex: 1; min-width: 0; }
|
||||
.linkedin-name { font-weight: 600; font-size: 14px; color: rgba(0, 0, 0, 0.9); }
|
||||
.linkedin-headline { font-size: 12px; color: rgba(0, 0, 0, 0.6); margin-top: 2px; }
|
||||
.linkedin-timestamp { font-size: 12px; color: rgba(0, 0, 0, 0.6); display: flex; align-items: center; gap: 4px; margin-top: 2px; }
|
||||
.linkedin-content { padding: 0 16px 12px; font-size: 14px; line-height: 1.5; color: rgba(0, 0, 0, 0.9); white-space: pre-wrap; }
|
||||
.linkedin-engagement { padding: 8px 16px; border-top: 1px solid rgba(0, 0, 0, 0.08); display: flex; align-items: center; gap: 4px; font-size: 12px; color: rgba(0, 0, 0, 0.6); }
|
||||
.linkedin-actions { display: flex; border-top: 1px solid rgba(0, 0, 0, 0.08); }
|
||||
.linkedin-action-btn { flex: 1; display: flex; align-items: center; justify-content: center; gap: 6px; padding: 12px 8px; font-size: 14px; font-weight: 600; color: rgba(0, 0, 0, 0.6); }
|
||||
.linkedin-action-btn svg { width: 20px; height: 20px; }
|
||||
.linkedin-post-image { width: 100%; max-height: 400px; object-fit: cover; }
|
||||
.image-upload-zone { border: 2px dashed rgba(61, 72, 72, 0.8); border-radius: 0.75rem; padding: 1.5rem; text-align: center; cursor: pointer; transition: all 0.2s; }
|
||||
.image-upload-zone:hover, .image-upload-zone.dragover { border-color: #ffc700; background: rgba(255, 199, 0, 0.05); }
|
||||
.image-upload-zone input[type="file"] { display: none; }
|
||||
.image-preview-container { position: relative; border-radius: 0.75rem; overflow: hidden; }
|
||||
.image-preview-container img { width: 100%; border-radius: 0.75rem; }
|
||||
.image-upload-progress { height: 4px; background: rgba(61, 72, 72, 0.8); border-radius: 2px; overflow: hidden; margin-top: 0.5rem; }
|
||||
.image-upload-progress-bar { height: 100%; background: #ffc700; border-radius: 2px; transition: width 0.3s; }
|
||||
.loading-spinner { border: 2px solid rgba(255, 199, 0, 0.2); border-top-color: #ffc700; border-radius: 50%; width: 20px; height: 20px; animation: spin 0.8s linear infinite; }
|
||||
@keyframes spin { to { transform: rotate(360deg); } }
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<!-- Breadcrumb -->
|
||||
<div class="mb-6">
|
||||
<nav class="flex items-center gap-2 text-sm">
|
||||
<a href="/company/manage" class="text-gray-400 hover:text-white transition-colors">Inhalte verwalten</a>
|
||||
<svg class="w-4 h-4 text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/></svg>
|
||||
<a href="/company/manage?employee_id={{ employee_id }}" class="text-gray-400 hover:text-white transition-colors">{{ employee_name }}</a>
|
||||
<svg class="w-4 h-4 text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/></svg>
|
||||
<a href="/company/manage/posts?employee_id={{ employee_id }}" class="text-gray-400 hover:text-white transition-colors">Posts</a>
|
||||
<svg class="w-4 h-4 text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/></svg>
|
||||
<span class="text-white truncate max-w-xs">{{ post.topic_title or 'Post' }}</span>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<!-- Header -->
|
||||
<div class="mb-6">
|
||||
<div class="flex items-start justify-between gap-4">
|
||||
<div class="flex-1 min-w-0">
|
||||
<h1 class="text-2xl font-bold text-white mb-2">{{ post.topic_title or 'Untitled Post' }}</h1>
|
||||
<div class="flex items-center gap-3 text-sm text-gray-400 flex-wrap">
|
||||
<span>{{ post.created_at.strftime('%d.%m.%Y um %H:%M Uhr') if post.created_at else 'N/A' }}</span>
|
||||
<span class="text-gray-600">|</span>
|
||||
<span>{{ post.iterations }} Iteration{{ 's' if post.iterations != 1 else '' }}</span>
|
||||
<span class="text-gray-600">|</span>
|
||||
<span>{{ employee_name }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-3 flex-shrink-0">
|
||||
<span class="px-3 py-1.5 rounded-lg text-sm font-medium {{ 'bg-green-600/30 text-green-300 border border-green-600/50' if post.status == 'approved' else 'bg-yellow-600/30 text-yellow-300 border border-yellow-600/50' if post.status == 'draft' else 'bg-blue-600/30 text-blue-300 border border-blue-600/50' }}">
|
||||
{% if post.status == 'draft' %}Vorschlag{% elif post.status == 'approved' %}Bearbeitet{% elif post.status == 'published' %}Veröffentlicht{% else %}{{ post.status | capitalize }}{% endif %}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 xl:grid-cols-3 gap-6">
|
||||
<!-- Post Content -->
|
||||
<div class="xl:col-span-2">
|
||||
<div class="section-card rounded-xl p-6">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h2 class="text-lg font-semibold text-white flex items-center gap-2">
|
||||
<svg class="w-5 h-5 text-brand-highlight" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
|
||||
LinkedIn Post
|
||||
</h2>
|
||||
<button onclick="copyToClipboard()" class="px-3 py-1.5 bg-brand-bg hover:bg-brand-bg-light rounded-lg text-sm text-gray-300 transition-colors flex items-center gap-2">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"/></svg>
|
||||
Kopieren
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- LinkedIn Preview -->
|
||||
<div class="linkedin-preview shadow-lg">
|
||||
<div class="linkedin-header">
|
||||
<div class="linkedin-avatar">{{ employee_name[:2] | upper if employee_name else 'UN' }}</div>
|
||||
<div class="linkedin-user-info">
|
||||
<div class="linkedin-name">{{ employee_name }}</div>
|
||||
<div class="linkedin-headline">{{ session.company_name }}</div>
|
||||
<div class="linkedin-timestamp">
|
||||
<span>{{ post.created_at.strftime('%d.%m.%Y') if post.created_at else 'Jetzt' }}</span>
|
||||
<span>•</span>
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
|
||||
<path d="M8 1a7 7 0 107 7 7 7 0 00-7-7zM3 8a5 5 0 011-3l.55.55A1.5 1.5 0 015 6.62v1.07a.75.75 0 00.22.53l.56.56a.75.75 0 00.53.22H7v.69a.75.75 0 00.22.53l.56.56a.75.75 0 01.22.53V13a5 5 0 01-5-5zm6.24 4.83l2-2.46a.75.75 0 00.09-.8l-.58-1.16A.76.76 0 0010 8H7v-.19a.51.51 0 01.28-.45l.38-.19a.74.74 0 00.3-1L7.4 5.19a.75.75 0 00-.67-.41H5.67a.75.75 0 01-.44-.14l-.34-.26a5 5 0 017.35 8.44z"/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="linkedin-content">{{ post.post_content }}</div>
|
||||
{% if post.image_url %}
|
||||
<img id="linkedinPostImage" src="{{ post.image_url }}" alt="Post image" class="linkedin-post-image">
|
||||
{% else %}
|
||||
<img id="linkedinPostImage" src="" alt="Post image" class="linkedin-post-image" style="display: none;">
|
||||
{% endif %}
|
||||
<div class="linkedin-engagement">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="#0a66c2">
|
||||
<path d="M19.46 11l-3.91-3.91a7 7 0 01-1.69-2.74l-.49-1.47A2.76 2.76 0 0010.76 1 2.75 2.75 0 008 3.74v1.12a9.19 9.19 0 00.46 2.85L8.89 9H4.12A2.12 2.12 0 002 11.12a2.16 2.16 0 00.92 1.76A2.11 2.11 0 002 14.62a2.14 2.14 0 001.28 2 2 2 0 00-.28 1 2.12 2.12 0 002 2.12v.14A2.12 2.12 0 007.12 22h7.49a8.08 8.08 0 003.58-.84l.31-.16H21V11z"/>
|
||||
</svg>
|
||||
<span style="margin-left: 4px;">42</span>
|
||||
<span style="margin-left: auto;">12 Kommentare • 3 Reposts</span>
|
||||
</div>
|
||||
<div class="linkedin-actions">
|
||||
<button class="linkedin-action-btn">
|
||||
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M19.46 11l-3.91-3.91a7 7 0 01-1.69-2.74l-.49-1.47A2.76 2.76 0 0010.76 1 2.75 2.75 0 008 3.74v1.12a9.19 9.19 0 00.46 2.85L8.89 9H4.12A2.12 2.12 0 002 11.12a2.16 2.16 0 00.92 1.76A2.11 2.11 0 002 14.62a2.14 2.14 0 001.28 2 2 2 0 00-.28 1 2.12 2.12 0 002 2.12v.14A2.12 2.12 0 007.12 22h7.49a8.08 8.08 0 003.58-.84l.31-.16H21V11z"/></svg>
|
||||
Gefällt mir
|
||||
</button>
|
||||
<button class="linkedin-action-btn">
|
||||
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M7 9h10v1H7zm0 4h7v-1H7zm16-2a6.78 6.78 0 01-2.84 5.61L12 22v-4H8A7 7 0 018 4h8a7 7 0 017 7z"/></svg>
|
||||
Kommentieren
|
||||
</button>
|
||||
<button class="linkedin-action-btn">
|
||||
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M13.96 5H6c-.55 0-1 .45-1 1v11H3V6c0-1.66 1.34-3 3-3h7.96L12 0l1.96 5zM17 7h-7c-1.1 0-2 .9-2 2v10c0 1.1.9 2 2 2h7c1.1 0 2-.9 2-2V9c0-1.1-.9-2-2-2z"/></svg>
|
||||
Reposten
|
||||
</button>
|
||||
<button class="linkedin-action-btn">
|
||||
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M21 3L0 10l7.66 4.26L16 8l-6.26 8.34L14 24l7-21z"/></svg>
|
||||
Senden
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Sidebar -->
|
||||
<div class="space-y-6">
|
||||
<!-- Actions -->
|
||||
<div class="section-card rounded-xl p-6">
|
||||
<h3 class="font-semibold text-white mb-4">Aktionen</h3>
|
||||
<div class="space-y-3">
|
||||
<button onclick="updateStatus('approved')" class="w-full px-4 py-3 bg-blue-600/20 hover:bg-blue-600/30 text-blue-300 rounded-lg transition-colors flex items-center justify-center gap-2 {% if post.status == 'approved' %}ring-2 ring-blue-500{% endif %}">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"/></svg>
|
||||
Als bearbeitet markieren
|
||||
</button>
|
||||
<button onclick="updateStatus('published')" class="w-full px-4 py-3 bg-green-600/20 hover:bg-green-600/30 text-green-300 rounded-lg transition-colors flex items-center justify-center gap-2 {% if post.status == 'published' %}ring-2 ring-green-500{% endif %}">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
|
||||
Als veröffentlicht markieren
|
||||
</button>
|
||||
<button onclick="updateStatus('draft')" class="w-full px-4 py-3 bg-yellow-600/20 hover:bg-yellow-600/30 text-yellow-300 rounded-lg transition-colors flex items-center justify-center gap-2 {% if post.status == 'draft' %}ring-2 ring-yellow-500{% endif %}">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z"/></svg>
|
||||
Zurück zu Vorschlag
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Image Upload -->
|
||||
<div class="section-card rounded-xl p-6">
|
||||
<h3 class="font-semibold text-white mb-4 flex items-center gap-2">
|
||||
<svg class="w-5 h-5 text-brand-highlight" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"/></svg>
|
||||
Bild
|
||||
</h3>
|
||||
<div id="imageUploadZone" class="image-upload-zone {% if post.image_url %}hidden{% endif %}">
|
||||
<input type="file" id="imageFileInput" accept="image/jpeg,image/png,image/gif,image/webp">
|
||||
<svg class="w-8 h-8 mx-auto mb-2 text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12"/></svg>
|
||||
<p class="text-sm text-gray-400">Bild hierher ziehen oder <span class="text-brand-highlight cursor-pointer">durchsuchen</span></p>
|
||||
<p class="text-xs text-gray-500 mt-1">JPEG, PNG, GIF, WebP - max. 5 MB</p>
|
||||
</div>
|
||||
<div id="imageUploadProgress" class="hidden">
|
||||
<div class="image-upload-progress">
|
||||
<div id="imageProgressBar" class="image-upload-progress-bar" style="width: 0%"></div>
|
||||
</div>
|
||||
<p class="text-xs text-gray-400 mt-1 text-center">Wird hochgeladen...</p>
|
||||
</div>
|
||||
<div id="imagePreviewSection" class="{% if not post.image_url %}hidden{% endif %}">
|
||||
<div class="image-preview-container mb-3">
|
||||
<img id="sidebarImagePreview" src="{{ post.image_url or '' }}" alt="Post-Bild">
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<button onclick="document.getElementById('imageReplaceInput').click()" class="flex-1 px-3 py-2 bg-brand-bg hover:bg-brand-bg-light text-gray-300 rounded-lg transition-colors text-sm flex items-center justify-center gap-2">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/></svg>
|
||||
Ersetzen
|
||||
</button>
|
||||
<button onclick="removeImage()" id="removeImageBtn" class="flex-1 px-3 py-2 bg-red-600/20 hover:bg-red-600/30 text-red-400 rounded-lg transition-colors text-sm flex items-center justify-center gap-2">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/></svg>
|
||||
Entfernen
|
||||
</button>
|
||||
</div>
|
||||
<input type="file" id="imageReplaceInput" accept="image/jpeg,image/png,image/gif,image/webp" class="hidden">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Post Info -->
|
||||
<div class="section-card rounded-xl p-6">
|
||||
<h3 class="font-semibold text-white mb-4">Details</h3>
|
||||
<div class="space-y-3 text-sm">
|
||||
<div class="flex justify-between">
|
||||
<span class="text-gray-400">Erstellt</span>
|
||||
<span class="text-white">{{ post.created_at.strftime('%d.%m.%Y') if post.created_at else 'N/A' }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span class="text-gray-400">Iterationen</span>
|
||||
<span class="text-white">{{ post.iterations }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span class="text-gray-400">Zeichen</span>
|
||||
<span class="text-white">{{ post.post_content | length }}</span>
|
||||
</div>
|
||||
{% if post.topic_title %}
|
||||
<div class="pt-3 border-t border-brand-bg-light">
|
||||
<span class="text-gray-400 block mb-1">Topic</span>
|
||||
<span class="text-white">{{ post.topic_title }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
const POST_ID = '{{ post.id }}';
|
||||
const EMPLOYEE_ID = '{{ employee_id }}';
|
||||
|
||||
function copyToClipboard() {
|
||||
const content = document.querySelector('.linkedin-content').textContent;
|
||||
navigator.clipboard.writeText(content).then(() => {
|
||||
const btn = document.querySelector('[onclick="copyToClipboard()"]');
|
||||
const original = btn.innerHTML;
|
||||
btn.innerHTML = '<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/></svg> Kopiert!';
|
||||
setTimeout(() => { btn.innerHTML = original; }, 2000);
|
||||
});
|
||||
}
|
||||
|
||||
async function updateStatus(newStatus) {
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append('status', newStatus);
|
||||
|
||||
const response = await fetch(`/api/posts/${POST_ID}/status`, {
|
||||
method: 'PATCH',
|
||||
body: formData
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
location.reload();
|
||||
} else {
|
||||
alert('Fehler beim Aktualisieren des Status');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
alert('Fehler beim Aktualisieren des Status');
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== IMAGE UPLOAD ====================
|
||||
|
||||
function showToast(message, type = 'info') {
|
||||
const toast = document.createElement('div');
|
||||
toast.className = `fixed bottom-4 right-4 px-4 py-3 rounded-lg shadow-lg z-50 transition-all transform translate-y-0 opacity-100 ${
|
||||
type === 'success' ? 'bg-green-600 text-white' :
|
||||
type === 'error' ? 'bg-red-600 text-white' :
|
||||
'bg-brand-bg-light text-white'
|
||||
}`;
|
||||
toast.textContent = message;
|
||||
document.body.appendChild(toast);
|
||||
setTimeout(() => {
|
||||
toast.classList.add('opacity-0', 'translate-y-2');
|
||||
setTimeout(() => toast.remove(), 300);
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
async function handleImageUpload(file) {
|
||||
if (!file) return;
|
||||
|
||||
const uploadZone = document.getElementById('imageUploadZone');
|
||||
const progressEl = document.getElementById('imageUploadProgress');
|
||||
const progressBar = document.getElementById('imageProgressBar');
|
||||
const previewSection = document.getElementById('imagePreviewSection');
|
||||
|
||||
uploadZone.classList.add('hidden');
|
||||
progressEl.classList.remove('hidden');
|
||||
progressBar.style.width = '30%';
|
||||
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append('image', file);
|
||||
progressBar.style.width = '60%';
|
||||
|
||||
const response = await fetch(`/api/posts/${POST_ID}/image`, {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
|
||||
progressBar.style.width = '90%';
|
||||
|
||||
if (!response.ok) {
|
||||
const err = await response.json();
|
||||
throw new Error(err.detail || 'Upload fehlgeschlagen');
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
progressBar.style.width = '100%';
|
||||
|
||||
document.getElementById('linkedinPostImage').src = result.image_url;
|
||||
document.getElementById('linkedinPostImage').style.display = 'block';
|
||||
document.getElementById('sidebarImagePreview').src = result.image_url;
|
||||
|
||||
setTimeout(() => {
|
||||
progressEl.classList.add('hidden');
|
||||
previewSection.classList.remove('hidden');
|
||||
}, 300);
|
||||
|
||||
showToast('Bild erfolgreich hochgeladen!', 'success');
|
||||
|
||||
} catch (error) {
|
||||
console.error('Image upload error:', error);
|
||||
showToast('Fehler: ' + error.message, 'error');
|
||||
progressEl.classList.add('hidden');
|
||||
uploadZone.classList.remove('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
async function removeImage() {
|
||||
const btn = document.getElementById('removeImageBtn');
|
||||
const originalHTML = btn.innerHTML;
|
||||
btn.innerHTML = '<div class="loading-spinner" style="width:16px;height:16px;"></div>';
|
||||
btn.disabled = true;
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/posts/${POST_ID}/image`, { method: 'DELETE' });
|
||||
if (!response.ok) throw new Error('Löschen fehlgeschlagen');
|
||||
|
||||
document.getElementById('linkedinPostImage').style.display = 'none';
|
||||
document.getElementById('imagePreviewSection').classList.add('hidden');
|
||||
document.getElementById('imageUploadZone').classList.remove('hidden');
|
||||
showToast('Bild entfernt.', 'success');
|
||||
|
||||
} catch (error) {
|
||||
console.error('Image delete error:', error);
|
||||
showToast('Fehler: ' + error.message, 'error');
|
||||
} finally {
|
||||
btn.innerHTML = originalHTML;
|
||||
btn.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const uploadZone = document.getElementById('imageUploadZone');
|
||||
const fileInput = document.getElementById('imageFileInput');
|
||||
const replaceInput = document.getElementById('imageReplaceInput');
|
||||
|
||||
if (uploadZone) {
|
||||
uploadZone.addEventListener('click', () => fileInput.click());
|
||||
fileInput.addEventListener('change', (e) => { if (e.target.files[0]) handleImageUpload(e.target.files[0]); });
|
||||
if (replaceInput) {
|
||||
replaceInput.addEventListener('change', (e) => { if (e.target.files[0]) handleImageUpload(e.target.files[0]); });
|
||||
}
|
||||
uploadZone.addEventListener('dragover', (e) => { e.preventDefault(); uploadZone.classList.add('dragover'); });
|
||||
uploadZone.addEventListener('dragleave', () => { uploadZone.classList.remove('dragover'); });
|
||||
uploadZone.addEventListener('drop', (e) => {
|
||||
e.preventDefault();
|
||||
uploadZone.classList.remove('dragover');
|
||||
if (e.dataTransfer.files[0]) handleImageUpload(e.dataTransfer.files[0]);
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user