305 lines
17 KiB
TypeScript
305 lines
17 KiB
TypeScript
/**
|
||
* 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>
|
||
);
|
||
}
|