atlas-social-command-center/client/src/pages/Calendar.tsx

305 lines
17 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* Kalender & Freigaben — Plane, manage und genehmige Content über alle Plattformen
* Referenz: docs/design-references/screen-kalender.png
*/
import { useState } from 'react';
import { ChevronLeft, ChevronRight, Plus, Filter, MoreHorizontal } from 'lucide-react';
const CHANNELS = [
{ name: 'TikTok', icon: '🎵', color: '#00d4ff', count: 5 },
{ name: 'Instagram', icon: '📸', color: '#e1306c', count: 5 },
{ name: 'LinkedIn', icon: '💼', color: '#0077b5', count: 4 },
{ name: 'X (Twitter)', icon: '✗', color: '#94a3b8', count: 5 },
{ name: 'YouTube Shorts', icon: '▶', color: '#ff0000', count: 5 },
];
const DAYS = ['Mo 26. Mai', 'Di 27. Mai', 'Mi 28. Mai', 'Do 29. Mai', 'Fr 30. Mai', 'Sa 31. Mai', 'So 1. Jun'];
const STATUS_COLORS: Record<string, { bg: string; text: string }> = {
Review: { bg: 'oklch(0.75 0.15 75 / 0.2)', text: '#f59e0b' },
Geplant: { bg: 'oklch(0.82 0.18 210 / 0.15)', text: '#00d4ff' },
Freigegeben: { bg: 'oklch(0.65 0.15 165 / 0.2)', text: '#10b981' },
Veröffentlicht: { bg: 'oklch(0.65 0.15 165 / 0.1)', text: '#10b981' },
};
const CALENDAR_DATA: Record<string, Record<string, Array<{ time: string; title: string; status: string }>>> = {
TikTok: {
'Mo 26. Mai': [{ time: '09:00', title: 'Shelly Plug S Gen3', status: 'Review' }],
'Di 27. Mai': [{ time: '18:30', title: 'Kitchen Trends 2025', status: 'Geplant' }],
'Mi 28. Mai': [{ time: '12:00', title: 'Energy Saving Tip', status: 'Freigegeben' }],
'Do 29. Mai': [{ time: '19:15', title: 'Automation Hack', status: 'Review' }],
'Fr 30. Mai': [{ time: '17:45', title: 'Weekend Setup', status: 'Freigegeben' }],
'Sa 31. Mai': [{ time: '11:30', title: 'Smart Home Tour', status: 'Veröffentlicht' }],
},
Instagram: {
'Mo 26. Mai': [{ time: '10:30', title: 'New Collection', status: 'Geplant' }],
'Di 27. Mai': [{ time: '15:00', title: 'Behind the Scenes', status: 'Review' }],
'Mi 28. Mai': [{ time: '11:00', title: 'Customer Favorite', status: 'Freigegeben' }],
'Do 29. Mai': [{ time: '16:30', title: 'Giveaway Alert', status: 'Geplant' }],
'Fr 30. Mai': [{ time: '12:15', title: 'Lifestyle Post', status: 'Veröffentlicht' }],
'Sa 31. Mai': [{ time: '10:00', title: 'Story Series', status: 'Veröffentlicht' }],
},
LinkedIn: {
'Mo 26. Mai': [{ time: '08:30', title: 'Industry Insights', status: 'Geplant' }],
'Di 27. Mai': [{ time: '12:00', title: 'Product Launch', status: 'Freigegeben' }],
'Mi 28. Mai': [{ time: '09:15', title: 'Case Study', status: 'Freigegeben' }],
'Do 29. Mai': [{ time: '13:00', title: 'Market Update', status: 'Geplant' }],
'Fr 30. Mai': [{ time: '09:30', title: 'Thought Leadership', status: 'Freigegeben' }],
},
'X (Twitter)': {
'Mo 26. Mai': [{ time: '09:45', title: 'Quick Tip', status: 'Geplant' }],
'Di 27. Mai': [{ time: '14:20', title: 'Tech Update', status: 'Geplant' }],
'Mi 28. Mai': [{ time: '11:15', title: 'Poll', status: 'Review' }],
'Do 29. Mai': [{ time: '08:50', title: 'Thread', status: 'Geplant' }],
'Fr 30. Mai': [{ time: '16:00', title: 'News Flash', status: 'Freigegeben' }],
},
'YouTube Shorts': {
'Mo 26. Mai': [{ time: '17:00', title: 'Unboxing', status: 'Geplant' }],
'Di 27. Mai': [{ time: '18:00', title: 'Top 3 Features', status: 'Review' }],
'Mi 28. Mai': [{ time: '17:30', title: 'How-To', status: 'Freigegeben' }],
'Do 29. Mai': [{ time: '18:30', title: 'Comparison', status: 'Geplant' }],
'Fr 30. Mai': [{ time: '09:00', title: 'User Story', status: 'Freigegeben' }],
'Sa 31. Mai': [{ time: '12:00', title: 'Quick Hack', status: 'Veröffentlicht' }],
},
};
const APPROVAL_QUEUE = [
{ priority: 'Hoch', title: 'Automation Hack in 60 Sekunden', platform: 'TikTok', product: 'Shelly Plus 2PM', affiliate: true, scheduled: '29. Mai, 19:15', by: 'Lena M.', status: 'Review' },
{ priority: 'Hoch', title: 'Behind the Scenes Studio Tour', platform: 'Instagram', product: 'Shelly Wall Display', affiliate: true, scheduled: '27. Mai, 15:00', by: 'Tim S.', status: 'Review' },
{ priority: 'Mittel', title: 'Case Study: Energie sparen', platform: 'LinkedIn', product: 'Shelly Plug S Gen3', affiliate: false, scheduled: '28. Mai, 09:15', by: 'Anna K.', status: 'Review' },
{ priority: 'Mittel', title: 'Top 3 Features Shelly Pro 4PM', platform: 'YouTube Shorts', product: 'Shelly Pro 4PM', affiliate: true, scheduled: '27. Mai, 18:00', by: 'Mark D.', status: 'Review' },
{ priority: 'Niedrig', title: 'Quick Tip: Szene erstellen', platform: 'X (Twitter)', product: 'Shelly App', affiliate: false, scheduled: '28. Mai, 11:15', by: 'Sarah L.', status: 'Review' },
{ priority: 'Niedrig', title: 'Giveaway: Smart Home Bundle', platform: 'Instagram', product: 'Bundle Pack', affiliate: true, scheduled: '29. Mai, 16:30', by: 'Tim S.', status: 'Geplant' },
{ priority: 'Niedrig', title: 'Market Update Q2/2025', platform: 'LinkedIn', product: '—', affiliate: false, scheduled: '29. Mai, 13:00', by: 'Anna K.', status: 'Geplant' },
];
const PRIORITY_COLORS: Record<string, { bg: string; text: string }> = {
Hoch: { bg: 'oklch(0.62 0.22 25 / 0.15)', text: '#ef4444' },
Mittel: { bg: 'oklch(0.75 0.15 75 / 0.15)', text: '#f59e0b' },
Niedrig: { bg: 'oklch(0.82 0.18 210 / 0.1)', text: '#64748b' },
};
const PLATFORM_ICONS: Record<string, string> = {
TikTok: '🎵', Instagram: '📸', LinkedIn: '💼', 'X (Twitter)': '✗', 'YouTube Shorts': '▶',
};
export default function Calendar() {
const [view, setView] = useState<'Woche' | 'Monat'>('Woche');
return (
<div className="p-5 animate-fade-in-up space-y-4">
{/* Header */}
<div className="atlas-page-header-row">
<div>
<h1 className="text-xl font-bold" style={{ fontFamily: 'var(--font-display)', color: 'var(--foreground)' }}>
Kalender & Freigaben
</h1>
<p className="text-sm mt-0.5" style={{ color: 'var(--muted-foreground)' }}>
Plane, manage und genehmige Content über alle Plattformen.
</p>
</div>
<div className="atlas-header-controls">
<div className="flex items-center gap-1 px-2 py-1.5 rounded" style={{ background: 'var(--atlas-surface)', border: '1px solid var(--atlas-border)' }}>
<button className="atlas-btn-ghost p-0.5"><ChevronLeft size={13} /></button>
<span className="text-xs" style={{ color: 'var(--foreground)' }}>26. Mai 1. Jun</span>
<button className="atlas-btn-ghost p-0.5"><ChevronRight size={13} /></button>
</div>
<div className="flex rounded overflow-hidden" style={{ border: '1px solid var(--atlas-border)' }}>
{(['Woche', 'Monat'] as const).map(v => (
<button
key={v}
onClick={() => setView(v)}
className="px-3 py-1.5 text-xs transition-colors"
style={{
background: view === v ? 'oklch(0.82 0.18 210 / 0.12)' : 'var(--atlas-surface)',
color: view === v ? 'var(--atlas-cyan)' : 'var(--muted-foreground)',
}}
>
{v}
</button>
))}
</div>
<button className="atlas-btn-ghost p-1.5"><Filter size={14} /></button>
</div>
</div>
{/* KPI Bar */}
<div className="grid grid-cols-2 lg:grid-cols-4 gap-3">
{[
{ label: 'Geplante Beiträge', value: '24', trend: '↑ 14% vs. letzte Woche', color: '#60a5fa' },
{ label: 'Ausstehende Freigaben', value: '7', trend: '↑ 2 vs. letzte Woche', color: '#a78bfa' },
{ label: 'Ø Veröffentlichungszeit', value: '14:32', trend: 'Beste Zeit: 18:00', color: '#10b981' },
{ label: 'Proj. Affiliate-Umsatz', value: '€12.430', trend: '↑ 24% vs. letzte Woche', color: '#00d4ff' },
].map((kpi, i) => (
<div key={i} className="atlas-kpi-card">
<div className="text-xs mb-1" style={{ color: 'var(--muted-foreground)' }}>{kpi.label}</div>
<div className="text-xl font-bold" style={{ fontFamily: 'var(--font-mono)', color: kpi.color }}>{kpi.value}</div>
<div className="text-xs mt-1" style={{ color: 'var(--muted-foreground)' }}>{kpi.trend}</div>
</div>
))}
</div>
{/* Content Kalender */}
<div className="atlas-card overflow-hidden">
<div className="p-3 flex items-center justify-between" style={{ borderBottom: '1px solid var(--atlas-border)' }}>
<h2 className="text-sm font-semibold" style={{ fontFamily: 'var(--font-display)', color: 'var(--foreground)' }}>Content Kalender</h2>
</div>
<div className="overflow-x-auto">
<table style={{ width: '100%', borderCollapse: 'collapse', minWidth: '900px' }}>
<thead>
<tr style={{ borderBottom: '1px solid var(--atlas-border)' }}>
<th style={{ width: '140px', padding: '8px 12px', textAlign: 'left' }}>
<span className="text-xs" style={{ color: 'var(--muted-foreground)' }}>Kanal</span>
</th>
{DAYS.map(day => (
<th key={day} style={{ padding: '8px 8px', textAlign: 'left', minWidth: '120px' }}>
<span className="text-xs" style={{ color: 'var(--muted-foreground)', fontFamily: 'var(--font-mono)' }}>{day}</span>
</th>
))}
<th style={{ width: '40px' }} />
</tr>
</thead>
<tbody>
{CHANNELS.map((channel, ci) => (
<tr key={channel.name} style={{ borderBottom: ci < CHANNELS.length - 1 ? '1px solid var(--atlas-border)' : 'none' }}>
<td style={{ padding: '8px 12px', verticalAlign: 'top' }}>
<div className="flex items-center gap-2">
<span style={{ fontSize: '14px' }}>{channel.icon}</span>
<div>
<div className="text-xs font-medium" style={{ color: 'var(--foreground)' }}>{channel.name}</div>
<div className="text-xs" style={{ color: 'var(--muted-foreground)', fontSize: '0.6rem' }}>{channel.count} Beiträge</div>
</div>
</div>
</td>
{DAYS.map(day => {
const items = CALENDAR_DATA[channel.name]?.[day] || [];
return (
<td key={day} style={{ padding: '6px 8px', verticalAlign: 'top' }}>
{items.map((item, ii) => (
<div
key={ii}
className="rounded p-1.5 mb-1 cursor-pointer"
style={{
background: STATUS_COLORS[item.status]?.bg || 'var(--atlas-surface)',
border: `1px solid ${STATUS_COLORS[item.status]?.text || 'var(--atlas-border)'}30`,
}}
>
<div className="text-xs font-mono" style={{ color: 'var(--muted-foreground)', fontSize: '0.6rem' }}>{item.time}</div>
<div className="text-xs truncate" style={{ color: 'var(--foreground)', fontSize: '0.6875rem' }}>{item.title}</div>
<span
className="text-xs"
style={{ color: STATUS_COLORS[item.status]?.text, fontSize: '0.6rem' }}
>
{item.status}
</span>
</div>
))}
{items.length === 0 && (
<div className="text-center py-2">
<span style={{ color: 'var(--atlas-border)', fontSize: '16px' }}></span>
</div>
)}
</td>
);
})}
<td style={{ padding: '6px 8px', verticalAlign: 'top' }}>
<button className="p-1 rounded" style={{ color: 'var(--muted-foreground)' }}>
<Plus size={12} />
</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
{/* Freigaben Queue */}
<div className="atlas-card overflow-hidden">
<div className="p-3 flex items-center justify-between" style={{ borderBottom: '1px solid var(--atlas-border)' }}>
<div className="flex items-center gap-2">
<h2 className="text-sm font-semibold" style={{ fontFamily: 'var(--font-display)', color: 'var(--foreground)' }}>
Freigaben-Queue ({APPROVAL_QUEUE.length})
</h2>
</div>
<div className="flex items-center gap-2">
<select className="text-xs px-2 py-1 rounded" style={{ background: 'var(--atlas-navy)', border: '1px solid var(--atlas-border)', color: 'var(--foreground)' }}>
<option>Alle Plattformen</option>
</select>
<select className="text-xs px-2 py-1 rounded" style={{ background: 'var(--atlas-navy)', border: '1px solid var(--atlas-border)', color: 'var(--foreground)' }}>
<option>Sortieren: Priorität</option>
</select>
</div>
</div>
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
<thead>
<tr style={{ borderBottom: '1px solid var(--atlas-border)' }}>
{['Priorität', 'Vorschau', 'Titel', 'Plattform', 'Produkt', 'Affiliate', 'Geplant für', 'Erstellt von', 'Status', 'Aktionen'].map(h => (
<th key={h} style={{ padding: '8px 12px', textAlign: 'left' }}>
<span className="text-xs" style={{ color: 'var(--muted-foreground)', fontFamily: 'var(--font-body)' }}>{h}</span>
</th>
))}
</tr>
</thead>
<tbody>
{APPROVAL_QUEUE.map((item, i) => (
<tr key={i} style={{ borderBottom: i < APPROVAL_QUEUE.length - 1 ? '1px solid var(--atlas-border)' : 'none' }}>
<td style={{ padding: '8px 12px' }}>
<span
className="text-xs px-2 py-0.5 rounded"
style={{ background: PRIORITY_COLORS[item.priority].bg, color: PRIORITY_COLORS[item.priority].text, border: `1px solid ${PRIORITY_COLORS[item.priority].text}30` }}
>
{item.priority}
</span>
</td>
<td style={{ padding: '8px 12px' }}>
<div className="w-8 h-8 rounded" style={{ background: 'var(--atlas-border)' }} />
</td>
<td style={{ padding: '8px 12px' }}>
<span className="text-xs" style={{ color: 'var(--foreground)' }}>{item.title}</span>
</td>
<td style={{ padding: '8px 12px' }}>
<span className="text-xs">{PLATFORM_ICONS[item.platform]} {item.platform}</span>
</td>
<td style={{ padding: '8px 12px' }}>
<span className="text-xs" style={{ color: 'var(--muted-foreground)' }}>{item.product}</span>
</td>
<td style={{ padding: '8px 12px' }}>
<span className="text-xs" style={{ color: item.affiliate ? 'var(--atlas-green)' : 'var(--muted-foreground)' }}>
{item.affiliate ? 'Ja' : 'Nein'}
</span>
</td>
<td style={{ padding: '8px 12px' }}>
<span className="text-xs" style={{ color: 'var(--foreground)', fontFamily: 'var(--font-mono)' }}>{item.scheduled}</span>
</td>
<td style={{ padding: '8px 12px' }}>
<span className="text-xs" style={{ color: 'var(--muted-foreground)' }}>{item.by}</span>
</td>
<td style={{ padding: '8px 12px' }}>
<span
className="text-xs px-2 py-0.5 rounded"
style={{ background: STATUS_COLORS[item.status]?.bg, color: STATUS_COLORS[item.status]?.text }}
>
{item.status}
</span>
</td>
<td style={{ padding: '8px 12px' }}>
<div className="flex items-center gap-1">
<button className="text-xs px-2 py-0.5 rounded" style={{ background: 'oklch(0.65 0.15 165 / 0.15)', color: 'var(--atlas-green)', border: '1px solid oklch(0.65 0.15 165 / 0.25)' }}>
Freigeben
</button>
<button className="text-xs px-2 py-0.5 rounded" style={{ background: 'var(--atlas-navy)', color: 'var(--muted-foreground)', border: '1px solid var(--atlas-border)' }}>
Bearbeiten
</button>
<button className="p-1" style={{ color: 'var(--muted-foreground)' }}><MoreHorizontal size={12} /></button>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
);
}