feat: ATLAS Social Command Center v1.0 - initial commit

This commit is contained in:
ATLAS Deploy 2026-07-07 21:01:08 +00:00
parent 1dd3a415d6
commit 2567a24915
33 changed files with 5205 additions and 114 deletions

0
.gitkeep Normal file
View file

View file

@ -1,17 +1,15 @@
<!doctype html>
<html lang="en">
<html lang="de" class="dark">
<head>
<meta charset="UTF-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1.0, maximum-scale=1" />
<title>ATLAS Social Command Center</title>
<!-- THIS IS THE START OF A COMMENT BLOCK, BLOCK TO BE DELETED: Google Fonts here, example:
<title>ATLAS Social Command Center</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet" />
THIS IS THE END OF A COMMENT BLOCK, BLOCK TO BE DELETED -->
<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;600;700&family=Space+Mono:wght@400;700&family=Inter:wght@400;500;600&display=swap" rel="stylesheet" />
</head>
<body>

View file

@ -1,38 +1,69 @@
import { Toaster } from "@/components/ui/sonner";
import { TooltipProvider } from "@/components/ui/tooltip";
import NotFound from "@/pages/NotFound";
import { Route, Switch } from "wouter";
import { Route, Switch, useLocation } from "wouter";
import ErrorBoundary from "./components/ErrorBoundary";
import { ThemeProvider } from "./contexts/ThemeContext";
import Home from "./pages/Home";
import { AppShell } from "./components/layout/AppShell";
import { Suspense, lazy } from "react";
// Lazy-load all pages
const WarRoom = lazy(() => import("./pages/WarRoom"));
const Signals = lazy(() => import("./pages/Signals"));
const Drafts = lazy(() => import("./pages/Drafts"));
const Calendar = lazy(() => import("./pages/Calendar"));
const Campaigns = lazy(() => import("./pages/Campaigns"));
const Products = lazy(() => import("./pages/Products"));
const Approvals = lazy(() => import("./pages/Approvals"));
const Analytics = lazy(() => import("./pages/Analytics"));
const Settings = lazy(() => import("./pages/Settings"));
function Router() {
function LoadingFallback() {
return (
<Switch>
<Route path={"/"} component={Home} />
<Route path={"/404"} component={NotFound} />
{/* Final fallback route */}
<Route component={NotFound} />
</Switch>
<div className="flex items-center justify-center h-full" style={{ color: 'var(--atlas-cyan)' }}>
<div className="flex flex-col items-center gap-3">
<div
className="w-6 h-6 rounded-full border-2 border-t-transparent animate-spin"
style={{ borderColor: 'var(--atlas-cyan)', borderTopColor: 'transparent' }}
/>
<span className="text-xs" style={{ fontFamily: 'var(--font-mono)', color: 'var(--muted-foreground)' }}>
ATLAS LOADING...
</span>
</div>
</div>
);
}
// NOTE: About Theme
// - First choose a default theme according to your design style (dark or light bg), than change color palette in index.css
// to keep consistent foreground/background color across components
// - If you want to make theme switchable, pass `switchable` ThemeProvider and use `useTheme` hook
function AppRoutes() {
const [location] = useLocation();
return (
<AppShell currentPath={location}>
<Suspense fallback={<LoadingFallback />}>
<Switch>
<Route path="/" component={WarRoom} />
<Route path="/signals" component={Signals} />
<Route path="/drafts" component={Drafts} />
<Route path="/calendar" component={Calendar} />
<Route path="/campaigns" component={Campaigns} />
<Route path="/products" component={Products} />
<Route path="/approvals" component={Approvals} />
<Route path="/analytics" component={Analytics} />
<Route path="/settings" component={Settings} />
<Route component={NotFound} />
</Switch>
</Suspense>
</AppShell>
);
}
function App() {
return (
<ErrorBoundary>
<ThemeProvider
defaultTheme="light"
// switchable
>
<ThemeProvider defaultTheme="dark">
<TooltipProvider>
<Toaster />
<Router />
<AppRoutes />
</TooltipProvider>
</ThemeProvider>
</ErrorBoundary>

View file

@ -0,0 +1,130 @@
/**
* AppShell 3-Spalten-Layout
* Bloomberg Terminal × Raumschiff-Brücke
*
* Layout: Sidebar (160px) | Content (flex-grow) | CoPilot (320px)
*/
import { useState } from 'react';
import { Sidebar } from './Sidebar';
import { CoPilotPanel } from './CoPilotPanel';
interface AppShellProps {
children: React.ReactNode;
currentPath: string;
}
export function AppShell({ children, currentPath }: AppShellProps) {
const [copilotOpen, setCopilotOpen] = useState(true);
return (
<div
className="h-screen flex overflow-hidden"
style={{ background: 'var(--atlas-navy)' }}
>
{/* Left Sidebar Navigation */}
<Sidebar currentPath={currentPath} />
{/* Main Content Area */}
<main
className="flex-1 flex flex-col overflow-hidden"
style={{ borderLeft: '1px solid var(--atlas-border)' }}
>
{/* Top Bar */}
<div
className="flex items-center justify-between px-4 py-2 flex-shrink-0"
style={{
background: 'var(--atlas-surface)',
borderBottom: '1px solid var(--atlas-border)',
minHeight: '44px'
}}
>
<div className="flex items-center gap-2">
<span
className="text-xs font-mono"
style={{ color: 'var(--muted-foreground)' }}
>
ATLAS OS
</span>
<span style={{ color: 'var(--atlas-border)' }}></span>
<span
className="text-xs font-mono"
style={{ color: 'var(--atlas-cyan)' }}
>
Social Command Center
</span>
</div>
<div className="flex items-center gap-3">
{/* System Status */}
<div className="flex items-center gap-1.5">
<div className="atlas-status-dot online" />
<span className="text-xs" style={{ color: 'var(--muted-foreground)' }}>
Postiz
</span>
</div>
<div
className="h-3 w-px"
style={{ background: 'var(--atlas-border)' }}
/>
{/* Live Clock */}
<LiveClock />
<div
className="h-3 w-px"
style={{ background: 'var(--atlas-border)' }}
/>
{/* Co-Pilot Toggle */}
<button
onClick={() => setCopilotOpen(!copilotOpen)}
className="text-xs px-2 py-1 rounded transition-colors"
style={{
color: copilotOpen ? 'var(--atlas-cyan)' : 'var(--muted-foreground)',
background: copilotOpen ? 'oklch(0.82 0.18 210 / 0.08)' : 'transparent',
border: `1px solid ${copilotOpen ? 'oklch(0.82 0.18 210 / 0.25)' : 'var(--atlas-border)'}`,
fontFamily: 'var(--font-mono)',
}}
>
Co-Pilot {copilotOpen ? '▶' : '◀'}
</button>
</div>
</div>
{/* Page Content */}
<div className="flex-1 overflow-auto atlas-scrollbar">
{children}
</div>
</main>
{/* Right Co-Pilot Panel */}
{copilotOpen && (
<CoPilotPanel onClose={() => setCopilotOpen(false)} />
)}
</div>
);
}
function LiveClock() {
const [time, setTime] = useState(new Date());
// Update every second
useState(() => {
const interval = setInterval(() => setTime(new Date()), 1000);
return () => clearInterval(interval);
});
return (
<span
className="text-xs"
style={{
fontFamily: 'var(--font-mono)',
color: 'var(--muted-foreground)'
}}
>
{time.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit', second: '2-digit' })}
</span>
);
}

View file

@ -0,0 +1,409 @@
/**
* CoPilotPanel AI Co-Pilot Rechts-Panel
*
* Kommando-Eingabe, Vorschläge, LLM-Integration
* Width: 320px, fixed right
*/
import { useState, useRef, useEffect } from 'react';
import { X, Send, Sparkles, Zap, FileEdit, Calendar, Package } from 'lucide-react';
interface Message {
id: string;
role: 'user' | 'assistant';
content: string;
timestamp: Date;
}
const SUGGESTED_COMMANDS = [
{ icon: FileEdit, label: 'Draft für pit_001 erstellen', cmd: 'Erstelle einen LinkedIn-Draft für Shelly Plus Plug S mit Affiliate-UTM-Link' },
{ icon: Zap, label: 'Signal analysieren', cmd: 'Analysiere die aktuellen Signale und schlage 3 Post-Ideen vor' },
{ icon: Calendar, label: 'Wochenplan erstellen', cmd: 'Erstelle einen optimalen Posting-Plan für diese Woche' },
{ icon: Package, label: 'Produkt-Post', cmd: 'Schreibe einen Instagram-Post für das beste Produkt im Katalog' },
];
interface CoPilotPanelProps {
onClose: () => void;
}
export function CoPilotPanel({ onClose }: CoPilotPanelProps) {
const [messages, setMessages] = useState<Message[]>([
{
id: '0',
role: 'assistant',
content: 'ATLAS Co-Pilot bereit. Ich helfe dir mit Drafts, Kampagnen, Signalanalyse und Content-Planung. Was soll ich tun?',
timestamp: new Date(),
}
]);
const [input, setInput] = useState('');
const [loading, setLoading] = useState(false);
const messagesEndRef = useRef<HTMLDivElement>(null);
const inputRef = useRef<HTMLTextAreaElement>(null);
useEffect(() => {
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
}, [messages]);
const sendMessage = async (text: string) => {
if (!text.trim() || loading) return;
const userMsg: Message = {
id: Date.now().toString(),
role: 'user',
content: text.trim(),
timestamp: new Date(),
};
setMessages(prev => [...prev, userMsg]);
setInput('');
setLoading(true);
try {
// Call built-in LLM via Manus proxy
const res = await fetch(import.meta.env.VITE_FRONTEND_FORGE_API_URL || 'https://api.openai.com/v1/chat/completions', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${import.meta.env.VITE_FRONTEND_FORGE_API_KEY || ''}`,
},
body: JSON.stringify({
model: 'gpt-4o-mini',
messages: [
{
role: 'system',
content: `Du bist ATLAS Co-Pilot, ein AI-Assistent für Social Media Content-Erstellung und -Management.
Du hilfst Torsten Hiss (hiss@clara360.de) beim:
- Erstellen von Social Media Drafts (LinkedIn, Instagram, TikTok)
- Analysieren von Signalen und Trends
- Planen von Content-Kalendern
- Optimieren von Affiliate-Links (UTM-Tracking)
- Verwalten des Produktkatalogs (catalog.json)
Kontext:
- Postiz ist die Publishing-Engine (postiz.atlas-os.ai)
- Produkte kommen aus catalog.json (search.atlas-os.ai/catalog.json)
- Aktuelles Hauptprodukt: Shelly Plus Plug S (pit_001)
- Affiliate-Regel: display_status=active nur wenn program_status=approved
Antworte präzise, direkt und auf Deutsch. Keine Floskeln.`,
},
...messages.slice(-10).map(m => ({ role: m.role, content: m.content })),
{ role: 'user', content: text.trim() },
],
max_tokens: 500,
temperature: 0.7,
}),
});
if (res.ok) {
const data = await res.json();
const reply = data.choices?.[0]?.message?.content || 'Keine Antwort erhalten.';
setMessages(prev => [...prev, {
id: Date.now().toString(),
role: 'assistant',
content: reply,
timestamp: new Date(),
}]);
} else {
throw new Error(`HTTP ${res.status}`);
}
} catch {
// Fallback response
setMessages(prev => [...prev, {
id: Date.now().toString(),
role: 'assistant',
content: getFallbackResponse(text),
timestamp: new Date(),
}]);
} finally {
setLoading(false);
}
};
const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
sendMessage(input);
}
};
return (
<aside
className="flex flex-col flex-shrink-0 animate-slide-in-right"
style={{
width: '320px',
background: 'var(--atlas-surface)',
borderLeft: '1px solid var(--atlas-border)',
}}
>
{/* Header */}
<div
className="flex items-center justify-between px-4 py-3 flex-shrink-0"
style={{ borderBottom: '1px solid var(--atlas-border)' }}
>
<div className="flex items-center gap-2">
<div
className="flex items-center justify-center rounded"
style={{
width: '24px',
height: '24px',
background: 'oklch(0.82 0.18 210 / 0.12)',
border: '1px solid oklch(0.82 0.18 210 / 0.25)',
}}
>
<Sparkles size={12} style={{ color: 'var(--atlas-cyan)' }} />
</div>
<span
className="text-sm font-semibold"
style={{ fontFamily: 'var(--font-display)', color: 'var(--foreground)' }}
>
Co-Pilot
</span>
<span className="atlas-badge-cyan">AI</span>
</div>
<button
onClick={onClose}
className="p-1 rounded transition-colors"
style={{ color: 'var(--muted-foreground)' }}
onMouseEnter={e => (e.currentTarget.style.color = 'var(--foreground)')}
onMouseLeave={e => (e.currentTarget.style.color = 'var(--muted-foreground)')}
>
<X size={14} />
</button>
</div>
{/* Suggested Commands */}
<div
className="px-3 py-2 flex-shrink-0"
style={{ borderBottom: '1px solid var(--atlas-border)' }}
>
<div
className="text-xs mb-2"
style={{
fontFamily: 'var(--font-mono)',
color: 'var(--muted-foreground)',
fontSize: '0.625rem',
letterSpacing: '0.08em',
textTransform: 'uppercase'
}}
>
Schnellbefehle
</div>
<div className="grid grid-cols-2 gap-1">
{SUGGESTED_COMMANDS.map((cmd, i) => {
const Icon = cmd.icon;
return (
<button
key={i}
onClick={() => sendMessage(cmd.cmd)}
className="flex items-center gap-1.5 px-2 py-1.5 rounded text-left transition-all"
style={{
background: 'var(--atlas-navy)',
border: '1px solid var(--atlas-border)',
color: 'var(--muted-foreground)',
fontSize: '0.6875rem',
fontFamily: 'var(--font-body)',
}}
onMouseEnter={e => {
e.currentTarget.style.borderColor = 'oklch(0.82 0.18 210 / 0.3)';
e.currentTarget.style.color = 'var(--foreground)';
}}
onMouseLeave={e => {
e.currentTarget.style.borderColor = 'var(--atlas-border)';
e.currentTarget.style.color = 'var(--muted-foreground)';
}}
>
<Icon size={10} style={{ flexShrink: 0 }} />
<span className="truncate">{cmd.label}</span>
</button>
);
})}
</div>
</div>
{/* Messages */}
<div className="flex-1 overflow-y-auto atlas-scrollbar px-3 py-3 space-y-3">
{messages.map(msg => (
<div
key={msg.id}
className={`flex ${msg.role === 'user' ? 'justify-end' : 'justify-start'}`}
>
<div
className="max-w-[85%] rounded px-3 py-2"
style={msg.role === 'user' ? {
background: 'oklch(0.82 0.18 210 / 0.12)',
border: '1px solid oklch(0.82 0.18 210 / 0.2)',
color: 'var(--foreground)',
} : {
background: 'var(--atlas-navy)',
border: '1px solid var(--atlas-border)',
color: 'var(--foreground)',
}}
>
{msg.role === 'assistant' && (
<div
className="flex items-center gap-1 mb-1"
>
<Sparkles size={9} style={{ color: 'var(--atlas-cyan)' }} />
<span style={{
fontSize: '0.625rem',
color: 'var(--atlas-cyan)',
fontFamily: 'var(--font-mono)',
letterSpacing: '0.05em'
}}>
CO-PILOT
</span>
</div>
)}
<p style={{ fontSize: '0.8125rem', lineHeight: '1.5', whiteSpace: 'pre-wrap' }}>
{msg.content}
</p>
<div
className="mt-1 text-right"
style={{ fontSize: '0.625rem', color: 'var(--muted-foreground)', fontFamily: 'var(--font-mono)' }}
>
{msg.timestamp.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' })}
</div>
</div>
</div>
))}
{loading && (
<div className="flex justify-start">
<div
className="rounded px-3 py-2"
style={{
background: 'var(--atlas-navy)',
border: '1px solid var(--atlas-border)',
}}
>
<div className="flex items-center gap-1">
<Sparkles size={9} style={{ color: 'var(--atlas-cyan)' }} />
<span style={{ fontSize: '0.625rem', color: 'var(--atlas-cyan)', fontFamily: 'var(--font-mono)' }}>
CO-PILOT
</span>
</div>
<div className="flex gap-1 mt-1">
{[0, 1, 2].map(i => (
<div
key={i}
className="rounded-full"
style={{
width: '6px',
height: '6px',
background: 'var(--atlas-cyan)',
animation: `pulse-green 1.2s ease-in-out ${i * 0.2}s infinite`,
}}
/>
))}
</div>
</div>
</div>
)}
<div ref={messagesEndRef} />
</div>
{/* Input */}
<div
className="px-3 py-3 flex-shrink-0"
style={{ borderTop: '1px solid var(--atlas-border)' }}
>
<div
className="flex gap-2 items-end rounded"
style={{
background: 'var(--atlas-navy)',
border: '1px solid var(--atlas-border)',
padding: '0.5rem',
}}
>
<textarea
ref={inputRef}
value={input}
onChange={e => setInput(e.target.value)}
onKeyDown={handleKeyDown}
placeholder="Befehl eingeben... (Enter = Senden)"
rows={2}
className="flex-1 bg-transparent resize-none outline-none atlas-scrollbar"
style={{
color: 'var(--foreground)',
fontSize: '0.8125rem',
fontFamily: 'var(--font-body)',
lineHeight: '1.5',
}}
/>
<button
onClick={() => sendMessage(input)}
disabled={!input.trim() || loading}
className="flex-shrink-0 p-1.5 rounded transition-all"
style={{
background: input.trim() && !loading ? 'var(--atlas-cyan)' : 'var(--atlas-border)',
color: input.trim() && !loading ? 'var(--atlas-navy)' : 'var(--muted-foreground)',
}}
>
<Send size={14} />
</button>
</div>
<div
className="mt-1 text-center"
style={{ fontSize: '0.625rem', color: 'var(--muted-foreground)', fontFamily: 'var(--font-body)' }}
>
ATLAS kann Fehler machen. Prüfe wichtige Infos.
</div>
</div>
</aside>
);
}
function getFallbackResponse(input: string): string {
const lower = input.toLowerCase();
if (lower.includes('draft') || lower.includes('post') || lower.includes('shelly')) {
return `Draft-Vorschlag für Shelly Plus Plug S:
🔌 **Smart Home Upgrade für 14,99**
Der Shelly Plus Plug S macht jede Steckdose smart. Energiemessung in Echtzeit, WLAN-Steuerung, Home Assistant kompatibel.
Mein Tipp: Erst messen, dann optimieren. Mit dem Shelly siehst du genau, was Strom frisst.
Link in Bio (Affiliate-UTM wird automatisch injiziert)
#SmartHome #Energie #Shelly #HomeAssistant`;
}
if (lower.includes('signal') || lower.includes('trend')) {
return `Aktuelle Signal-Analyse:
📊 **Top-Trends heute:**
1. Smart Home Energiesparen (+34% Suchvolumen)
2. Home Assistant 2025 Update
3. Strompreise Winter 2025
**Empfehlung:** Shelly Plus Plug S jetzt pushen passt perfekt zu Trend #1 und #3.
Soll ich einen Draft dafür erstellen?`;
}
if (lower.includes('kalender') || lower.includes('plan') || lower.includes('woche')) {
return `Wochenplan-Vorschlag:
**Mo:** LinkedIn Shelly Plug S Produktvorstellung
**Di:** Instagram Story Smart Home Setup Tour
**Mi:** TikTok "Wie viel Strom verbraucht dein Fernseher wirklich?"
**Do:** LinkedIn Affiliate-Einnahmen Update
**Fr:** Instagram Reel Top 5 Smart Home Geräte unter 20
**Sa:** Pause
**So:** Newsletter-Teaser für nächste Woche
Soll ich die Drafts dafür anlegen?`;
}
return `Verstanden. Ich arbeite daran.
Für vollständige AI-Antworten muss der ATLAS Co-Pilot mit dem LLM-Backend verbunden sein. Die Verbindung wird beim nächsten Deployment konfiguriert.
Was kann ich sonst für dich tun?`;
}

View file

@ -0,0 +1,188 @@
/**
* Sidebar Navigation für ATLAS Social Command Center
*
* Sections:
* - MISSION: War Room, Signale, Drafts, Kalender
* - CONTENT: Kampagnen, Produkte, Freigaben, Analytics
* - SYSTEM: Einstellungen
*/
import { Link } from 'wouter';
import {
LayoutDashboard,
Zap,
FileEdit,
Calendar,
Megaphone,
Package,
CheckSquare,
BarChart2,
Settings,
} from 'lucide-react';
const ATLAS_LOGO_URL = '/manus-storage/32486_9677c375.png';
interface NavItem {
label: string;
path: string;
icon: React.ComponentType<{ size?: number; className?: string }>;
}
const missionNav: NavItem[] = [
{ label: 'War Room', path: '/', icon: LayoutDashboard },
{ label: 'Signale', path: '/signals', icon: Zap },
{ label: 'Drafts', path: '/drafts', icon: FileEdit },
{ label: 'Kalender', path: '/calendar', icon: Calendar },
];
const contentNav: NavItem[] = [
{ label: 'Kampagnen', path: '/campaigns', icon: Megaphone },
{ label: 'Produkte', path: '/products', icon: Package },
{ label: 'Freigaben', path: '/approvals', icon: CheckSquare },
{ label: 'Analytics', path: '/analytics', icon: BarChart2 },
];
const systemNav: NavItem[] = [
{ label: 'Einstellungen', path: '/settings', icon: Settings },
];
interface SidebarProps {
currentPath: string;
}
export function Sidebar({ currentPath }: SidebarProps) {
return (
<aside
className="flex flex-col flex-shrink-0 overflow-hidden"
style={{
width: '160px',
background: 'var(--atlas-navy)',
borderRight: '1px solid var(--atlas-border)',
}}
>
{/* Logo */}
<div
className="flex flex-col items-center px-3 py-3 flex-shrink-0"
style={{ borderBottom: '1px solid var(--atlas-border)' }}
>
<img
src={ATLAS_LOGO_URL}
alt="ATLAS OS"
style={{ width: '48px', height: '48px', objectFit: 'contain' }}
/>
<div
className="text-xs font-semibold mt-1 text-center"
style={{
fontFamily: 'var(--font-display)',
color: 'var(--foreground)',
fontSize: '0.75rem',
letterSpacing: '0.02em'
}}
>
Atlas Social
</div>
</div>
{/* Navigation */}
<nav className="flex-1 overflow-y-auto atlas-scrollbar py-2">
{/* Mission Section */}
<div className="mb-2">
<div className="atlas-section-header">Mission</div>
{missionNav.map(item => (
<NavLink key={item.path} item={item} currentPath={currentPath} />
))}
</div>
<div className="atlas-divider mx-2" />
{/* Content Section */}
<div className="my-2">
<div className="atlas-section-header">Content</div>
{contentNav.map(item => (
<NavLink key={item.path} item={item} currentPath={currentPath} />
))}
</div>
<div className="atlas-divider mx-2" />
{/* System Section */}
<div className="mt-2">
{systemNav.map(item => (
<NavLink key={item.path} item={item} currentPath={currentPath} />
))}
</div>
</nav>
{/* ATLAS STATUS */}
<div
className="px-3 py-2 flex-shrink-0"
style={{ borderTop: '1px solid var(--atlas-border)' }}
>
<div style={{ fontSize: '0.6rem', color: 'var(--muted-foreground)', fontFamily: 'var(--font-mono)', letterSpacing: '0.08em', textTransform: 'uppercase', marginBottom: '4px' }}>
ATLAS STATUS
</div>
<div className="flex items-center gap-1.5">
<div className="atlas-status-dot online" />
<span style={{ fontSize: '0.6875rem', color: 'var(--atlas-green)', fontFamily: 'var(--font-body)' }}>Systems Operational</span>
</div>
</div>
{/* User Footer */}
<div
className="px-3 py-2 flex-shrink-0"
style={{ borderTop: '1px solid var(--atlas-border)' }}
>
<div className="flex items-center gap-2">
<div
className="flex items-center justify-center rounded-full text-xs font-bold flex-shrink-0"
style={{
width: '28px',
height: '28px',
background: 'oklch(0.82 0.18 210 / 0.15)',
color: 'var(--atlas-cyan)',
fontFamily: 'var(--font-display)',
border: '1px solid oklch(0.82 0.18 210 / 0.2)',
}}
>
T
</div>
<div className="min-w-0">
<div
className="text-xs font-medium truncate"
style={{ color: 'var(--foreground)', fontFamily: 'var(--font-body)' }}
>
Torsten Hiss
</div>
<div
className="truncate"
style={{
fontSize: '0.6rem',
color: 'var(--muted-foreground)',
fontFamily: 'var(--font-mono)'
}}
>
Enterprise Plan
</div>
</div>
</div>
</div>
</aside>
);
}
function NavLink({ item, currentPath }: { item: NavItem; currentPath: string }) {
const isActive = item.path === '/'
? currentPath === '/'
: currentPath.startsWith(item.path);
const Icon = item.icon;
return (
<Link href={item.path}>
<div className={`atlas-nav-item mx-1 ${isActive ? 'active' : ''}`}>
<Icon size={14} />
<span>{item.label}</span>
</div>
</Link>
);
}

View file

@ -0,0 +1,116 @@
/**
* useCatalog Produktkatalog von search.atlas-os.ai/catalog.json
*
* Canonical product source for ATLAS Social Command Center.
* display_status=active only when program_status=approved.
*/
import { useState, useEffect } from 'react';
const CATALOG_URL = 'https://search.atlas-os.ai/catalog.json';
export interface CatalogProduct {
id: string;
name: string;
slug?: string;
description?: string;
category?: string;
brand?: string;
price?: number;
currency?: string;
affiliate_url?: string;
affiliate_utm?: string;
program_status?: 'approved' | 'pending' | 'rejected';
display_status?: 'active' | 'inactive';
image_url?: string;
tags?: string[];
commission_rate?: number;
asin?: string;
ean?: string;
}
export function useCatalog() {
const [products, setProducts] = useState<CatalogProduct[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
fetch(CATALOG_URL)
.then(r => {
if (!r.ok) throw new Error(`HTTP ${r.status}`);
return r.json();
})
.then(data => {
const items = Array.isArray(data) ? data : data.products || data.items || [];
setProducts(items);
})
.catch(e => {
setError(e.message);
// Fallback mock data
setProducts(getMockProducts());
})
.finally(() => setLoading(false));
}, []);
// Only show products where display_status=active AND program_status=approved
const activeProducts = products.filter(p =>
p.display_status === 'active' && p.program_status === 'approved'
);
// All products for internal management view
const allProducts = products;
return { products: activeProducts, allProducts, loading, error };
}
export function buildAffiliateUrl(product: CatalogProduct, source = 'atlas-social'): string {
if (!product.affiliate_url) return '#';
const url = new URL(product.affiliate_url);
url.searchParams.set('utm_source', source);
url.searchParams.set('utm_medium', 'social');
url.searchParams.set('utm_campaign', product.slug || product.id);
if (product.affiliate_utm) {
// Merge existing UTM params
const existing = new URLSearchParams(product.affiliate_utm);
existing.forEach((v, k) => url.searchParams.set(k, v));
}
return url.toString();
}
function getMockProducts(): CatalogProduct[] {
return [
{
id: 'pit_001',
name: 'Shelly Plus Plug S',
slug: 'shelly-plus-plug-s',
description: 'WLAN-Steckdose mit Energiemessung. Smart Home Grundausstattung.',
category: 'Smart Home',
brand: 'Shelly',
price: 14.99,
currency: 'EUR',
affiliate_url: 'https://amzn.to/shelly-plus-plug-s',
program_status: 'approved',
display_status: 'active',
commission_rate: 3.5,
tags: ['smart-home', 'energie', 'wlan'],
},
{
id: 'pit_002',
name: 'Shelly Pro 4PM',
slug: 'shelly-pro-4pm',
description: 'DIN-Rail 4-Kanal Smart Switch mit Energiemessung.',
category: 'Smart Home',
brand: 'Shelly',
price: 69.99,
currency: 'EUR',
affiliate_url: 'https://amzn.to/shelly-pro-4pm',
program_status: 'pending',
display_status: 'inactive',
commission_rate: 4.0,
tags: ['smart-home', 'profi', 'din-rail'],
},
];
}

View file

@ -0,0 +1,213 @@
/**
* usePostiz Postiz API Integration
*
* API Base: https://postiz.atlas-os.ai
* Auth: Bearer token (JWT) after login
* Org API Key: 51a341904d7ae5c676d47761c347dd17caa6aef5be5511d25cbb7566624d1da7
*/
import { useState, useEffect, useCallback } from 'react';
const POSTIZ_API = 'https://postiz.atlas-os.ai';
const ORG_API_KEY = '51a341904d7ae5c676d47761c347dd17caa6aef5be5511d25cbb7566624d1da7';
export interface PostizPost {
id: string;
content: string;
status: 'DRAFT' | 'QUEUE' | 'PUBLISHED' | 'ERROR';
publishDate?: string;
integration?: {
id: string;
name: string;
type: string;
};
createdAt: string;
updatedAt: string;
}
export interface PostizIntegration {
id: string;
name: string;
type: string;
disabled: boolean;
picture?: string;
}
export interface PostizStats {
postsToday: number;
postsThisWeek: number;
drafts: number;
scheduled: number;
published: number;
channels: number;
}
let authToken: string | null = null;
async function getAuthToken(): Promise<string | null> {
if (authToken) return authToken;
try {
const res = await fetch(`${POSTIZ_API}/auth/login`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
email: 'hiss@clara360.de',
password: 'Atlas68!',
provider: 'LOCAL',
}),
});
if (!res.ok) return null;
const data = await res.json();
authToken = data.token || data.access_token || null;
return authToken;
} catch {
return null;
}
}
async function postizFetch(path: string, options?: RequestInit) {
const token = await getAuthToken();
const headers: Record<string, string> = {
'Content-Type': 'application/json',
'x-org-id': ORG_API_KEY,
};
if (token) {
headers['Authorization'] = `Bearer ${token}`;
}
return fetch(`${POSTIZ_API}${path}`, {
...options,
headers: { ...headers, ...(options?.headers || {}) },
});
}
export function usePostizPosts() {
const [posts, setPosts] = useState<PostizPost[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const fetchPosts = useCallback(async () => {
setLoading(true);
setError(null);
try {
const res = await postizFetch('/posts');
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const data = await res.json();
setPosts(Array.isArray(data) ? data : data.posts || data.data || []);
} catch (e) {
setError(e instanceof Error ? e.message : 'Fehler beim Laden');
// Use mock data as fallback
setPosts(getMockPosts());
} finally {
setLoading(false);
}
}, []);
useEffect(() => {
fetchPosts();
}, [fetchPosts]);
return { posts, loading, error, refetch: fetchPosts };
}
export function usePostizIntegrations() {
const [integrations, setIntegrations] = useState<PostizIntegration[]>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
postizFetch('/integrations')
.then(r => r.ok ? r.json() : null)
.then(data => {
if (data) {
setIntegrations(Array.isArray(data) ? data : data.integrations || []);
}
})
.catch(() => setIntegrations([]))
.finally(() => setLoading(false));
}, []);
return { integrations, loading };
}
export function usePostizStats(): PostizStats {
const { posts } = usePostizPosts();
const { integrations } = usePostizIntegrations();
const today = new Date().toDateString();
return {
postsToday: posts.filter(p =>
p.publishDate && new Date(p.publishDate).toDateString() === today
).length,
postsThisWeek: posts.filter(p => {
if (!p.publishDate) return false;
const d = new Date(p.publishDate);
const now = new Date();
const weekAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);
return d >= weekAgo && d <= now;
}).length,
drafts: posts.filter(p => p.status === 'DRAFT').length,
scheduled: posts.filter(p => p.status === 'QUEUE').length,
published: posts.filter(p => p.status === 'PUBLISHED').length,
channels: integrations.filter(i => !i.disabled).length,
};
}
export async function createPostizDraft(content: string, integrationIds?: string[]) {
const res = await postizFetch('/posts', {
method: 'POST',
body: JSON.stringify({
content,
status: 'DRAFT',
integrations: integrationIds || [],
}),
});
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json();
}
export async function schedulePostizPost(postId: string, publishDate: string) {
const res = await postizFetch(`/posts/${postId}`, {
method: 'PUT',
body: JSON.stringify({
status: 'QUEUE',
publishDate,
}),
});
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json();
}
// Mock data for development/fallback
function getMockPosts(): PostizPost[] {
return [
{
id: 'mock-1',
content: '🔌 Shelly Plus Plug S — Smart Home Energiekontrolle. Jetzt mit 15% Rabatt über unseren Affiliate-Link. #SmartHome #Energie #Affiliate',
status: 'DRAFT',
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
},
{
id: 'mock-2',
content: 'ATLAS OS Update: Neue Features für Creator. Mehr Kontrolle, mehr Umsatz. #ATLASОС #Creator',
status: 'QUEUE',
publishDate: new Date(Date.now() + 2 * 60 * 60 * 1000).toISOString(),
createdAt: new Date(Date.now() - 3600000).toISOString(),
updatedAt: new Date(Date.now() - 3600000).toISOString(),
},
{
id: 'mock-3',
content: 'Smart Home im Test: 5 Geräte, die sich wirklich lohnen. Link in Bio.',
status: 'PUBLISHED',
publishDate: new Date(Date.now() - 86400000).toISOString(),
createdAt: new Date(Date.now() - 90000000).toISOString(),
updatedAt: new Date(Date.now() - 86400000).toISOString(),
},
];
}

View file

@ -3,6 +3,15 @@
@custom-variant dark (&:is(.dark *));
/*
* ATLAS Social Command Center Design System
* Bloomberg Terminal × Raumschiff-Brücke × Creator Studio
*
* Signature Color: #00d4ff (ATLAS Cyan)
* Background: #0a0e1a (Deep Space Navy)
* Typography: Space Grotesk (headings) / Inter (body) / Space Mono (metrics)
*/
@theme inline {
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
@ -40,86 +49,95 @@
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-ring: var(--sidebar-ring);
/* ATLAS Custom Tokens */
--color-atlas-cyan: var(--atlas-cyan);
--color-atlas-cyan-glow: var(--atlas-cyan-glow);
--color-atlas-navy: var(--atlas-navy);
--color-atlas-surface: var(--atlas-surface);
--color-atlas-border: var(--atlas-border);
--color-atlas-amber: var(--atlas-amber);
--color-atlas-green: var(--atlas-green);
--color-atlas-red: var(--atlas-red);
/* Typography */
--font-display: 'Space Grotesk', sans-serif;
--font-body: 'Inter', sans-serif;
--font-mono: 'Space Mono', monospace;
}
:root {
--primary: var(--color-blue-700);
--primary-foreground: var(--color-blue-50);
--sidebar-primary: var(--color-blue-600);
--sidebar-primary-foreground: var(--color-blue-50);
--chart-1: var(--color-blue-300);
--chart-2: var(--color-blue-500);
--chart-3: var(--color-blue-600);
--chart-4: var(--color-blue-700);
--chart-5: var(--color-blue-800);
--radius: 0.65rem;
--background: oklch(1 0 0);
--foreground: oklch(0.235 0.015 65);
--card: oklch(1 0 0);
--card-foreground: oklch(0.235 0.015 65);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.235 0.015 65);
--secondary: oklch(0.98 0.001 286.375);
--secondary-foreground: oklch(0.4 0.015 65);
--muted: oklch(0.967 0.001 286.375);
--muted-foreground: oklch(0.552 0.016 285.938);
--accent: oklch(0.967 0.001 286.375);
--accent-foreground: oklch(0.141 0.005 285.823);
--destructive: oklch(0.577 0.245 27.325);
--destructive-foreground: oklch(0.985 0 0);
--border: oklch(0.92 0.004 286.32);
--input: oklch(0.92 0.004 286.32);
--ring: oklch(0.623 0.214 259.815);
--sidebar: oklch(0.985 0 0);
--sidebar-foreground: oklch(0.235 0.015 65);
--sidebar-accent: oklch(0.967 0.001 286.375);
--sidebar-accent-foreground: oklch(0.141 0.005 285.823);
--sidebar-border: oklch(0.92 0.004 286.32);
--sidebar-ring: oklch(0.623 0.214 259.815);
}
.dark {
--primary: var(--color-blue-700);
--primary-foreground: var(--color-blue-50);
--sidebar-primary: var(--color-blue-500);
--sidebar-primary-foreground: var(--color-blue-50);
--background: oklch(0.141 0.005 285.823);
--foreground: oklch(0.85 0.005 65);
--card: oklch(0.21 0.006 285.885);
--card-foreground: oklch(0.85 0.005 65);
--popover: oklch(0.21 0.006 285.885);
--popover-foreground: oklch(0.85 0.005 65);
--secondary: oklch(0.24 0.006 286.033);
--secondary-foreground: oklch(0.7 0.005 65);
--muted: oklch(0.274 0.006 286.033);
--muted-foreground: oklch(0.705 0.015 286.067);
--accent: oklch(0.274 0.006 286.033);
--accent-foreground: oklch(0.92 0.005 65);
--destructive: oklch(0.704 0.191 22.216);
--destructive-foreground: oklch(0.985 0 0);
--border: oklch(1 0 0 / 10%);
--input: oklch(1 0 0 / 15%);
--ring: oklch(0.488 0.243 264.376);
--chart-1: var(--color-blue-300);
--chart-2: var(--color-blue-500);
--chart-3: var(--color-blue-600);
--chart-4: var(--color-blue-700);
--chart-5: var(--color-blue-800);
--sidebar: oklch(0.21 0.006 285.885);
--sidebar-foreground: oklch(0.85 0.005 65);
--sidebar-accent: oklch(0.274 0.006 286.033);
--sidebar-accent-foreground: oklch(0.985 0 0);
--sidebar-border: oklch(1 0 0 / 10%);
--sidebar-ring: oklch(0.488 0.243 264.376);
/* ATLAS Dark Theme — Only Mode */
:root, .dark {
/* Core ATLAS Colors */
--atlas-cyan: oklch(0.82 0.18 210); /* #00d4ff */
--atlas-cyan-glow: oklch(0.82 0.18 210 / 0.15);
--atlas-navy: oklch(0.12 0.03 265); /* #0a0e1a */
--atlas-surface: oklch(0.15 0.03 265); /* #0f1629 */
--atlas-border: oklch(0.22 0.05 265); /* #1e2d4a */
--atlas-amber: oklch(0.75 0.15 75); /* #f59e0b */
--atlas-green: oklch(0.65 0.15 165); /* #10b981 */
--atlas-red: oklch(0.62 0.22 25); /* #ef4444 */
/* Semantic Tokens — Dark Mode Only */
--radius: 0.375rem;
--background: oklch(0.12 0.03 265);
--foreground: oklch(0.88 0.01 220);
--card: oklch(0.15 0.03 265);
--card-foreground: oklch(0.88 0.01 220);
--popover: oklch(0.17 0.04 265);
--popover-foreground: oklch(0.88 0.01 220);
--primary: oklch(0.82 0.18 210);
--primary-foreground: oklch(0.12 0.03 265);
--secondary: oklch(0.20 0.04 265);
--secondary-foreground: oklch(0.75 0.01 220);
--muted: oklch(0.20 0.04 265);
--muted-foreground: oklch(0.52 0.02 220);
--accent: oklch(0.22 0.05 265);
--accent-foreground: oklch(0.88 0.01 220);
--destructive: oklch(0.62 0.22 25);
--destructive-foreground: oklch(0.98 0 0);
--border: oklch(0.22 0.05 265);
--input: oklch(0.20 0.04 265);
--ring: oklch(0.82 0.18 210);
/* Charts */
--chart-1: oklch(0.82 0.18 210);
--chart-2: oklch(0.75 0.15 75);
--chart-3: oklch(0.65 0.15 165);
--chart-4: oklch(0.62 0.22 25);
--chart-5: oklch(0.70 0.12 300);
/* Sidebar */
--sidebar: oklch(0.13 0.03 265);
--sidebar-foreground: oklch(0.88 0.01 220);
--sidebar-primary: oklch(0.82 0.18 210);
--sidebar-primary-foreground: oklch(0.12 0.03 265);
--sidebar-accent: oklch(0.18 0.04 265);
--sidebar-accent-foreground: oklch(0.88 0.01 220);
--sidebar-border: oklch(0.22 0.05 265);
--sidebar-ring: oklch(0.82 0.18 210);
}
@layer base {
* {
@apply border-border outline-ring/50;
}
html {
color-scheme: dark;
}
body {
@apply bg-background text-foreground;
font-family: var(--font-body);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
h1, h2, h3, h4, h5, h6 {
font-family: var(--font-display);
}
button:not(:disabled),
[role="button"]:not([aria-disabled="true"]),
[type="button"]:not(:disabled),
@ -134,24 +152,11 @@
}
@layer components {
/**
* Custom container utility that centers content and adds responsive padding.
*
* This overrides Tailwind's default container behavior to:
* - Auto-center content (mx-auto)
* - Add responsive horizontal padding
* - Set max-width for large screens
*
* Usage: <div className="container">...</div>
*
* For custom widths, use max-w-* utilities directly:
* <div className="max-w-6xl mx-auto px-4">...</div>
*/
.container {
width: 100%;
margin-left: auto;
margin-right: auto;
padding-left: 1rem; /* 16px - mobile padding */
padding-left: 1rem;
padding-right: 1rem;
}
@ -162,16 +167,327 @@
@media (min-width: 640px) {
.container {
padding-left: 1.5rem; /* 24px - tablet padding */
padding-left: 1.5rem;
padding-right: 1.5rem;
}
}
@media (min-width: 1024px) {
.container {
padding-left: 2rem; /* 32px - desktop padding */
padding-left: 2rem;
padding-right: 2rem;
max-width: 1280px; /* Standard content width */
max-width: 1280px;
}
}
}
/* ATLAS Custom Components */
.atlas-metric {
font-family: var(--font-mono);
color: var(--atlas-cyan);
}
.atlas-card {
background: var(--atlas-surface);
border: 1px solid var(--atlas-border);
border-radius: var(--radius);
}
.atlas-card-hover {
transition: border-color 200ms ease, box-shadow 200ms ease;
}
.atlas-card-hover:hover {
border-color: oklch(0.82 0.18 210 / 0.4);
box-shadow: 0 0 16px oklch(0.82 0.18 210 / 0.08);
}
.atlas-glow {
box-shadow: 0 0 20px oklch(0.82 0.18 210 / 0.2);
}
.atlas-nav-item {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 0.75rem;
border-radius: var(--radius);
font-size: 0.8125rem;
font-weight: 500;
color: var(--muted-foreground);
transition: color 150ms ease, background 150ms ease;
position: relative;
font-family: var(--font-body);
}
.atlas-nav-item:hover {
color: var(--foreground);
background: var(--accent);
}
.atlas-nav-item.active {
color: var(--atlas-cyan);
background: oklch(0.82 0.18 210 / 0.08);
}
.atlas-nav-item.active::before {
content: '';
position: absolute;
left: 0;
top: 50%;
transform: translateY(-50%);
width: 2px;
height: 60%;
background: var(--atlas-cyan);
border-radius: 0 2px 2px 0;
}
.atlas-status-dot {
width: 8px;
height: 8px;
border-radius: 50%;
flex-shrink: 0;
}
.atlas-status-dot.online {
background: var(--atlas-green);
box-shadow: 0 0 6px var(--atlas-green);
animation: pulse-green 2s ease-in-out infinite;
}
.atlas-status-dot.offline {
background: var(--muted-foreground);
}
.atlas-status-dot.warning {
background: var(--atlas-amber);
box-shadow: 0 0 6px var(--atlas-amber);
animation: pulse-amber 2s ease-in-out infinite;
}
.atlas-status-dot.error {
background: var(--atlas-red);
box-shadow: 0 0 6px var(--atlas-red);
animation: pulse-red 2s ease-in-out infinite;
}
.atlas-kpi-card {
background: var(--atlas-surface);
border: 1px solid var(--atlas-border);
border-radius: var(--radius);
padding: 1rem 1.25rem;
transition: border-color 200ms ease;
}
.atlas-kpi-card:hover {
border-color: oklch(0.82 0.18 210 / 0.3);
}
.atlas-section-header {
font-family: var(--font-display);
font-size: 0.6875rem;
font-weight: 600;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--muted-foreground);
padding: 0 0.75rem;
margin-bottom: 0.25rem;
}
.atlas-divider {
height: 1px;
background: var(--atlas-border);
margin: 0.5rem 0;
}
.atlas-badge-cyan {
display: inline-flex;
align-items: center;
padding: 0.125rem 0.5rem;
border-radius: 9999px;
font-size: 0.6875rem;
font-weight: 600;
font-family: var(--font-mono);
background: oklch(0.82 0.18 210 / 0.12);
color: var(--atlas-cyan);
border: 1px solid oklch(0.82 0.18 210 / 0.25);
}
.atlas-badge-amber {
display: inline-flex;
align-items: center;
padding: 0.125rem 0.5rem;
border-radius: 9999px;
font-size: 0.6875rem;
font-weight: 600;
background: oklch(0.75 0.15 75 / 0.12);
color: var(--atlas-amber);
border: 1px solid oklch(0.75 0.15 75 / 0.25);
}
.atlas-badge-green {
display: inline-flex;
align-items: center;
padding: 0.125rem 0.5rem;
border-radius: 9999px;
font-size: 0.6875rem;
font-weight: 600;
background: oklch(0.65 0.15 165 / 0.12);
color: var(--atlas-green);
border: 1px solid oklch(0.65 0.15 165 / 0.25);
}
.atlas-badge-red {
display: inline-flex;
align-items: center;
padding: 0.125rem 0.5rem;
border-radius: 9999px;
font-size: 0.6875rem;
font-weight: 600;
background: oklch(0.62 0.22 25 / 0.12);
color: var(--atlas-red);
border: 1px solid oklch(0.62 0.22 25 / 0.25);
}
.atlas-input {
background: var(--atlas-surface);
border: 1px solid var(--atlas-border);
color: var(--foreground);
font-family: var(--font-body);
font-size: 0.875rem;
padding: 0.5rem 0.75rem;
border-radius: var(--radius);
outline: none;
transition: border-color 150ms ease, box-shadow 150ms ease;
width: 100%;
}
.atlas-input:focus {
border-color: var(--atlas-cyan);
box-shadow: 0 0 0 2px oklch(0.82 0.18 210 / 0.15);
}
.atlas-btn-primary {
display: inline-flex;
align-items: center;
gap: 0.375rem;
padding: 0.5rem 1rem;
background: var(--atlas-cyan);
color: var(--atlas-navy);
font-family: var(--font-display);
font-size: 0.8125rem;
font-weight: 600;
border-radius: var(--radius);
border: none;
transition: opacity 150ms ease, transform 150ms ease, box-shadow 150ms ease;
}
.atlas-btn-primary:hover {
opacity: 0.9;
box-shadow: 0 0 16px oklch(0.82 0.18 210 / 0.3);
}
.atlas-btn-primary:active {
transform: scale(0.97);
}
.atlas-btn-ghost {
display: inline-flex;
align-items: center;
gap: 0.375rem;
padding: 0.5rem 0.75rem;
background: transparent;
color: var(--muted-foreground);
font-family: var(--font-body);
font-size: 0.8125rem;
font-weight: 500;
border-radius: var(--radius);
border: 1px solid var(--atlas-border);
transition: color 150ms ease, border-color 150ms ease, background 150ms ease;
}
.atlas-btn-ghost:hover {
color: var(--foreground);
border-color: oklch(0.82 0.18 210 / 0.3);
background: oklch(0.82 0.18 210 / 0.05);
}
.atlas-btn-ghost:active {
transform: scale(0.97);
}
.atlas-scrollbar::-webkit-scrollbar {
width: 4px;
}
.atlas-scrollbar::-webkit-scrollbar-track {
background: transparent;
}
.atlas-scrollbar::-webkit-scrollbar-thumb {
background: var(--atlas-border);
border-radius: 2px;
}
.atlas-scrollbar::-webkit-scrollbar-thumb:hover {
background: oklch(0.82 0.18 210 / 0.3);
}
}
/* Animations */
@keyframes pulse-green {
0%, 100% { opacity: 1; box-shadow: 0 0 6px var(--atlas-green); }
50% { opacity: 0.6; box-shadow: 0 0 12px var(--atlas-green); }
}
@keyframes pulse-amber {
0%, 100% { opacity: 1; box-shadow: 0 0 6px var(--atlas-amber); }
50% { opacity: 0.6; box-shadow: 0 0 12px var(--atlas-amber); }
}
@keyframes pulse-red {
0%, 100% { opacity: 1; box-shadow: 0 0 6px var(--atlas-red); }
50% { opacity: 0.6; box-shadow: 0 0 12px var(--atlas-red); }
}
@keyframes scan-line {
0% { transform: translateY(-100%); }
100% { transform: translateY(100vh); }
}
@keyframes fade-in-up {
from { opacity: 0; transform: translateY(8px); }
to { opacity: 1; transform: translateY(0); }
}
@keyframes slide-in-right {
from { opacity: 0; transform: translateX(16px); }
to { opacity: 1; transform: translateX(0); }
}
@keyframes blink {
0%, 100% { opacity: 1; }
50% { opacity: 0; }
}
.animate-fade-in-up {
animation: fade-in-up 250ms cubic-bezier(0.23, 1, 0.32, 1) both;
}
.animate-slide-in-right {
animation: slide-in-right 250ms cubic-bezier(0.23, 1, 0.32, 1) both;
}
.animate-blink {
animation: blink 1s step-end infinite;
}
@media (prefers-reduced-motion: reduce) {
.animate-fade-in-up,
.animate-slide-in-right,
.atlas-status-dot.online,
.atlas-status-dot.warning,
.atlas-status-dot.error {
animation: none;
}
}

View file

@ -0,0 +1,361 @@
/**
* Analytics Verstehe Reichweite, Performance und Affiliate-Umsätze im Detail
* Referenz: docs/design-references/screen-analytics.png
*/
import { useState } from 'react';
import { useLocation } from 'wouter';
import {
LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Legend,
PieChart, Pie, Cell, FunnelChart, Funnel, LabelList,
} from 'recharts';
import { ChevronLeft, ChevronRight, Download, MoreHorizontal } from 'lucide-react';
const PERF_DATA = [
{ date: '2. Mai', TikTok: 320000, Instagram: 240000, LinkedIn: 120000, Twitter: 80000, YouTube: 40000 },
{ date: '6. Mai', TikTok: 380000, Instagram: 260000, LinkedIn: 130000, Twitter: 85000, YouTube: 45000 },
{ date: '10. Mai', TikTok: 420000, Instagram: 280000, LinkedIn: 140000, Twitter: 90000, YouTube: 50000 },
{ date: '14. Mai', TikTok: 480000, Instagram: 310000, LinkedIn: 155000, Twitter: 95000, YouTube: 55000 },
{ date: '18. Mai', TikTok: 520000, Instagram: 330000, LinkedIn: 165000, Twitter: 100000, YouTube: 60000 },
{ date: '22. Mai', TikTok: 600000, Instagram: 360000, LinkedIn: 180000, Twitter: 110000, YouTube: 65000 },
{ date: '26. Mai', TikTok: 680000, Instagram: 400000, LinkedIn: 200000, Twitter: 120000, YouTube: 70000 },
{ date: '30. Mai', TikTok: 750000, Instagram: 430000, LinkedIn: 215000, Twitter: 130000, YouTube: 75000 },
{ date: '1. Jun', TikTok: 800000, Instagram: 450000, LinkedIn: 225000, Twitter: 135000, YouTube: 80000 },
];
const CHANNEL_COLORS: Record<string, string> = {
TikTok: '#00d4ff', Instagram: '#e1306c', LinkedIn: '#0077b5', Twitter: '#94a3b8', YouTube: '#ff0000',
};
const PIE_DATA = [
{ name: 'TikTok', value: 45, reach: '1,12 Mio.' },
{ name: 'Instagram', value: 29, reach: '721 Tsd.' },
{ name: 'LinkedIn', value: 13.2, reach: '328 Tsd.' },
{ name: 'Twitter', value: 7.7, reach: '192 Tsd.' },
{ name: 'YouTube', value: 5, reach: '124 Tsd.' },
];
const PIE_COLORS = ['#00d4ff', '#e1306c', '#0077b5', '#94a3b8', '#ff0000'];
const FUNNEL_DATA = [
{ name: 'Impressionen', value: 2480000, label: '2,48 Mio.', trend: '+28%', color: '#60a5fa' },
{ name: 'Klicks', value: 18743, label: '18.743', trend: '+21%', color: '#a78bfa' },
{ name: 'Conversions', value: 1824, label: '1.824', trend: '+23%', color: '#10b981' },
{ name: 'Umsatz', value: 24780, label: '€24.780', trend: '+24%', color: '#00d4ff' },
];
const CONTENT_ROWS = [
{ title: 'Shelly Plug S Gen3 im Fokus', category: 'Smart Home Review', platform: 'TikTok', impressions: 512340, impressionsTrend: 26, engagement: 33742, engagementTrend: 18, clicks: 2914, clicksTrend: 22, revenue: '€4.932', revenueTrend: 31, image: 'https://images.unsplash.com/photo-1558618666-fcd25c85cd64?w=40&h=40&fit=crop' },
{ title: 'Airfryer Trends 2025', category: 'Küchen & Leben', platform: 'Instagram', impressions: 387211, impressionsTrend: 19, engagement: 24118, engagementTrend: 14, clicks: 2145, clicksTrend: 18, revenue: '€3.781', revenueTrend: 21, image: 'https://images.unsplash.com/photo-1556909114-f6e7ad7d3136?w=40&h=40&fit=crop' },
{ title: 'Balkonkraftwerk im Aufwind', category: 'Energie sparen', platform: 'LinkedIn', impressions: 298567, impressionsTrend: 17, engagement: 15402, engagementTrend: 12, clicks: 1876, clicksTrend: 16, revenue: '€3.126', revenueTrend: 19, image: 'https://images.unsplash.com/photo-1509391366360-2e959784a276?w=40&h=40&fit=crop' },
{ title: 'KI-News Update', category: 'Tech & Innovation', platform: 'Twitter', impressions: 192853, impressionsTrend: 9, engagement: 9421, engagementTrend: 8, clicks: 823, clicksTrend: 11, revenue: '€1.342', revenueTrend: 15, image: 'https://images.unsplash.com/photo-1677442135703-1787eea5ce01?w=40&h=40&fit=crop' },
{ title: 'Smart Metering erklärt', category: 'Energie einfach verstehen', platform: 'YouTube', impressions: 161902, impressionsTrend: 13, engagement: 7821, engagementTrend: 10, clicks: 612, clicksTrend: 9, revenue: '€1.107', revenueTrend: 12, image: 'https://images.unsplash.com/photo-1509391366360-2e959784a276?w=40&h=40&fit=crop' },
];
const PLATFORM_ICONS: Record<string, string> = { TikTok: '🎵', Instagram: '📸', LinkedIn: '💼', Twitter: '✗', YouTube: '▶' };
const TOP_CONTENT = [
{ rank: 1, title: 'Shelly Plug S Gen3 im Fokus', revenue: '€4.932', image: 'https://images.unsplash.com/photo-1558618666-fcd25c85cd64?w=60&h=60&fit=crop' },
{ rank: 2, title: 'Airfryer Trends 2025', revenue: '€3.781', image: 'https://images.unsplash.com/photo-1556909114-f6e7ad7d3136?w=60&h=60&fit=crop' },
{ rank: 3, title: 'Balkonkraftwerk', revenue: '€3.126', image: 'https://images.unsplash.com/photo-1509391366360-2e959784a276?w=60&h=60&fit=crop' },
{ rank: 4, title: 'KI-News Update', revenue: '€1.342', image: 'https://images.unsplash.com/photo-1677442135703-1787eea5ce01?w=60&h=60&fit=crop' },
{ rank: 5, title: 'Smart Metering erklärt', revenue: '€1.107', image: 'https://images.unsplash.com/photo-1509391366360-2e959784a276?w=60&h=60&fit=crop' },
];
const RANK_COLORS = ['#f59e0b', '#94a3b8', '#cd7f32', '#64748b', '#64748b'];
function formatNum(n: number) {
if (n >= 1000000) return `${(n / 1000000).toFixed(2)} Mio.`;
if (n >= 1000) return `${(n / 1000).toFixed(0)} Tsd.`;
return n.toLocaleString('de-DE');
}
export default function Analytics() {
const [metric, setMetric] = useState('Reichweite');
const [period, setPeriod] = useState('Täglich');
const [timePeriod, setTimePeriod] = useState('30 Tage');
return (
<div className="p-5 animate-fade-in-up space-y-4">
{/* Header */}
<div className="flex items-center justify-between">
<div>
<h1 className="text-xl font-bold" style={{ fontFamily: 'var(--font-display)', color: 'var(--foreground)' }}>Analytics</h1>
<p className="text-sm mt-0.5" style={{ color: 'var(--muted-foreground)' }}>Verstehe Reichweite, Performance und Affiliate-Umsätze im Detail.</p>
</div>
<div className="flex items-center gap-2">
<div className="flex items-center gap-1.5 px-3 py-1.5 rounded" style={{ background: 'var(--atlas-surface)', border: '1px solid var(--atlas-border)' }}>
<span className="text-xs" style={{ color: 'var(--foreground)' }}>26. Mai 1. Juni 2025</span>
</div>
{['7 Tage', '30 Tage', 'Benutzerdefiniert'].map(p => (
<button
key={p}
onClick={() => setTimePeriod(p)}
className="text-xs px-3 py-1.5 rounded transition-all"
style={{
background: timePeriod === p ? 'var(--atlas-cyan)' : 'var(--atlas-surface)',
color: timePeriod === p ? 'var(--atlas-navy)' : 'var(--muted-foreground)',
border: `1px solid ${timePeriod === p ? 'var(--atlas-cyan)' : 'var(--atlas-border)'}`,
fontWeight: timePeriod === p ? 600 : 400,
}}
>
{p}
</button>
))}
</div>
</div>
{/* KPI Bar */}
<div className="grid grid-cols-5 gap-3">
{[
{ label: 'Reichweite gesamt', value: '2,48 Mio.', trend: '↑ 28%', color: '#60a5fa', icon: '👥' },
{ label: 'Engagement-Rate', value: '4,32 %', trend: '↑ 15%', color: '#e1306c', icon: '❤️' },
{ label: 'Klicks auf Affiliate-Links', value: '18.743', trend: '↑ 21%', color: '#10b981', icon: '🔗' },
{ label: 'Affiliate-Umsatz', value: '€24.780', trend: '↑ 24%', color: '#a78bfa', icon: '💎' },
{ label: 'Top Kanal', value: 'TikTok', sub: '1,12 Mio. Reichweite', color: '#00d4ff', icon: '🎵' },
].map((kpi, i) => (
<div key={i} className="atlas-kpi-card">
<div className="flex items-center justify-between mb-1">
<span className="text-xs" style={{ color: 'var(--muted-foreground)' }}>{kpi.label}</span>
<span style={{ fontSize: '14px' }}>{kpi.icon}</span>
</div>
<div className="font-bold" style={{ fontFamily: 'var(--font-mono)', color: kpi.color, fontSize: i === 4 ? '1rem' : '1.25rem' }}>{kpi.value}</div>
<div className="text-xs mt-1" style={{ color: 'var(--atlas-green)' }}>{kpi.trend || kpi.sub} vs. vorherige 30 Tage</div>
</div>
))}
</div>
{/* Charts Row */}
<div className="grid grid-cols-3 gap-4">
{/* Performance über Zeit */}
<div className="col-span-2 atlas-card p-4">
<div className="flex items-center justify-between mb-3">
<h2 className="text-sm font-semibold" style={{ fontFamily: 'var(--font-display)', color: 'var(--foreground)' }}>Performance über Zeit</h2>
<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)' }}
value={metric}
onChange={e => setMetric(e.target.value)}
>
<option>Reichweite</option>
<option>Engagement</option>
<option>Klicks</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)' }}
value={period}
onChange={e => setPeriod(e.target.value)}
>
<option>Täglich</option>
<option>Wöchentlich</option>
</select>
</div>
</div>
<ResponsiveContainer width="100%" height={200}>
<LineChart data={PERF_DATA} margin={{ top: 5, right: 10, left: 0, bottom: 5 }}>
<CartesianGrid strokeDasharray="3 3" stroke="rgba(255,255,255,0.05)" />
<XAxis dataKey="date" tick={{ fill: '#64748b', fontSize: 10 }} axisLine={false} tickLine={false} />
<YAxis tick={{ fill: '#64748b', fontSize: 10 }} axisLine={false} tickLine={false} tickFormatter={v => v >= 1000 ? `${v / 1000}K` : v.toString()} />
<Tooltip
contentStyle={{ background: '#0f1629', border: '1px solid #1e2d4a', borderRadius: '6px', fontSize: '11px' }}
labelStyle={{ color: '#94a3b8' }}
formatter={(v: number) => [formatNum(v), '']}
/>
<Legend wrapperStyle={{ fontSize: '10px' }} />
{Object.keys(CHANNEL_COLORS).map(ch => (
<Line key={ch} type="monotone" dataKey={ch} stroke={CHANNEL_COLORS[ch]} strokeWidth={1.5} dot={false} />
))}
</LineChart>
</ResponsiveContainer>
</div>
{/* Reichweite nach Kanal */}
<div className="atlas-card p-4">
<h2 className="text-sm font-semibold mb-3" style={{ fontFamily: 'var(--font-display)', color: 'var(--foreground)' }}>Reichweite nach Kanal</h2>
<div className="flex justify-center mb-3">
<div className="relative" style={{ width: '140px', height: '140px' }}>
<PieChart width={140} height={140}>
<Pie data={PIE_DATA} cx={65} cy={65} innerRadius={42} outerRadius={65} dataKey="value" startAngle={90} endAngle={-270}>
{PIE_DATA.map((_, i) => <Cell key={i} fill={PIE_COLORS[i]} />)}
</Pie>
</PieChart>
<div className="absolute inset-0 flex flex-col items-center justify-center">
<div className="text-sm font-bold" style={{ fontFamily: 'var(--font-mono)', color: 'var(--atlas-cyan)' }}>2,48 Mio.</div>
<div className="text-xs" style={{ color: 'var(--muted-foreground)', fontSize: '0.6rem' }}>Gesamt</div>
</div>
</div>
</div>
<div className="space-y-1.5">
{PIE_DATA.map((d, i) => (
<div key={d.name} className="flex items-center gap-2">
<div className="w-2 h-2 rounded-full flex-shrink-0" style={{ background: PIE_COLORS[i] }} />
<span className="text-xs flex-1" style={{ color: 'var(--foreground)' }}>{d.name}</span>
<span className="text-xs" style={{ color: 'var(--muted-foreground)', fontFamily: 'var(--font-mono)' }}>{d.reach}</span>
<span className="text-xs" style={{ color: 'var(--muted-foreground)', fontFamily: 'var(--font-mono)' }}>({d.value}%)</span>
</div>
))}
</div>
<div className="text-xs mt-2" style={{ color: 'var(--muted-foreground)', fontSize: '0.6rem' }}>Zeitraum: 26. Mai 1. Juni 2025</div>
</div>
</div>
{/* Content Performance Table + Funnel */}
<div className="grid grid-cols-3 gap-4">
{/* Table */}
<div className="col-span-2 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 Performance</h2>
<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>Sortieren nach: Umsatz</option>
</select>
<button className="atlas-btn-ghost text-xs gap-1"><Download size={11} /> Exportieren</button>
</div>
</div>
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
<thead>
<tr style={{ borderBottom: '1px solid var(--atlas-border)' }}>
{['Inhalt', 'Plattform', 'Impressionen', 'Engagement', 'Klicks', 'Umsatz'].map(h => (
<th key={h} style={{ padding: '8px 10px', textAlign: 'left' }}>
<span className="text-xs" style={{ color: 'var(--muted-foreground)' }}>{h}</span>
</th>
))}
<th />
</tr>
</thead>
<tbody>
{CONTENT_ROWS.map((row, i) => (
<tr key={i} style={{ borderBottom: i < CONTENT_ROWS.length - 1 ? '1px solid var(--atlas-border)' : 'none' }}>
<td style={{ padding: '8px 10px' }}>
<div className="flex items-center gap-2">
<img src={row.image} alt={row.title} className="rounded" style={{ width: '32px', height: '32px', objectFit: 'cover' }} onError={e => { (e.target as HTMLImageElement).style.display = 'none'; }} />
<div>
<div className="text-xs font-medium" style={{ color: 'var(--foreground)' }}>{row.title}</div>
<div className="text-xs" style={{ color: 'var(--muted-foreground)', fontSize: '0.6rem' }}>{row.category}</div>
</div>
</div>
</td>
<td style={{ padding: '8px 10px' }}>
<span className="text-xs">{PLATFORM_ICONS[row.platform]}</span>
</td>
<td style={{ padding: '8px 10px' }}>
<div className="text-xs font-mono" style={{ color: 'var(--foreground)' }}>{row.impressions.toLocaleString('de-DE')}</div>
<div className="text-xs" style={{ color: 'var(--atlas-green)', fontSize: '0.6rem' }}> {row.impressionsTrend}%</div>
</td>
<td style={{ padding: '8px 10px' }}>
<div className="text-xs font-mono" style={{ color: 'var(--foreground)' }}>{row.engagement.toLocaleString('de-DE')}</div>
<div className="text-xs" style={{ color: 'var(--atlas-green)', fontSize: '0.6rem' }}> {row.engagementTrend}%</div>
</td>
<td style={{ padding: '8px 10px' }}>
<div className="text-xs font-mono" style={{ color: 'var(--foreground)' }}>{row.clicks.toLocaleString('de-DE')}</div>
<div className="text-xs" style={{ color: 'var(--atlas-green)', fontSize: '0.6rem' }}> {row.clicksTrend}%</div>
</td>
<td style={{ padding: '8px 10px' }}>
<div className="text-xs font-bold font-mono" style={{ color: 'var(--atlas-green)' }}>{row.revenue}</div>
<div className="text-xs" style={{ color: 'var(--atlas-green)', fontSize: '0.6rem' }}> {row.revenueTrend}%</div>
</td>
<td style={{ padding: '8px 10px' }}>
<button style={{ color: 'var(--muted-foreground)' }}><MoreHorizontal size={12} /></button>
</td>
</tr>
))}
</tbody>
</table>
<div className="flex items-center justify-between px-3 py-2" style={{ borderTop: '1px solid var(--atlas-border)' }}>
<span className="text-xs" style={{ color: 'var(--muted-foreground)' }}>15 von 25 Einträgen</span>
<div className="flex items-center gap-1">
<button className="atlas-btn-ghost p-1"><ChevronLeft size={12} /></button>
{[1,2,3,4,5].map(p => (
<button key={p} className="w-6 h-6 text-xs rounded" style={{ background: p === 1 ? 'oklch(0.82 0.18 210 / 0.12)' : 'transparent', color: p === 1 ? 'var(--atlas-cyan)' : 'var(--muted-foreground)' }}>{p}</button>
))}
<button className="atlas-btn-ghost p-1"><ChevronRight size={12} /></button>
</div>
<select className="text-xs px-2 py-1 rounded" style={{ background: 'var(--atlas-surface)', border: '1px solid var(--atlas-border)', color: 'var(--foreground)' }}>
<option>5 pro Seite</option>
</select>
</div>
</div>
{/* Funnel + Insights */}
<div className="space-y-3">
<div className="atlas-card p-4">
<div className="flex items-center justify-between mb-3">
<h2 className="text-sm font-semibold" style={{ fontFamily: 'var(--font-display)', color: 'var(--foreground)' }}>Performance-Funnel</h2>
<select className="text-xs px-2 py-1 rounded" style={{ background: 'var(--atlas-navy)', border: '1px solid var(--atlas-border)', color: 'var(--foreground)' }}>
<option>Gesamt (alle Kanäle)</option>
</select>
</div>
<div className="space-y-2">
{FUNNEL_DATA.map((stage, i) => (
<div key={stage.name}>
<div className="flex items-center justify-between mb-1">
<div className="flex items-center gap-2">
<div className="w-4 h-4 rounded flex items-center justify-center" style={{ background: `${stage.color}20` }}>
<span style={{ fontSize: '10px' }}>{['👁', '🖱', '🛒', '💎'][i]}</span>
</div>
<span className="text-xs" style={{ color: 'var(--muted-foreground)' }}>{stage.name}</span>
</div>
<div className="text-right">
<div className="text-xs font-bold" style={{ fontFamily: 'var(--font-mono)', color: stage.color }}>{stage.label}</div>
<div className="text-xs" style={{ color: 'var(--atlas-green)', fontSize: '0.6rem' }}> {stage.trend}</div>
</div>
</div>
<div className="h-1.5 rounded-full" style={{ background: 'var(--atlas-navy)' }}>
<div
className="h-full rounded-full"
style={{ width: `${[100, 0.76, 0.074, 1][i] || Math.max(2, (stage.value / 2480000) * 100)}%`, background: stage.color }}
/>
</div>
</div>
))}
</div>
</div>
<div className="atlas-card p-3">
<div className="text-xs font-semibold mb-2" style={{ fontFamily: 'var(--font-display)', color: 'var(--foreground)' }}>Insights</div>
<div className="space-y-2">
{[
{ color: '#f59e0b', text: 'TikTok liefert die höchste Reichweite, Instagram die beste Engagement-Rate.' },
{ color: '#10b981', text: 'Inhalte zu Smart Home & Küche generieren den höchsten Umsatz.' },
{ color: '#a78bfa', text: 'Deine CTR ist um 21% gestiegen stark über dem Vorjahreswert.' },
].map((ins, i) => (
<div key={i} className="flex gap-2">
<div className="w-1.5 h-1.5 rounded-full mt-1.5 flex-shrink-0" style={{ background: ins.color }} />
<p className="text-xs" style={{ color: 'var(--muted-foreground)', lineHeight: '1.5' }}>{ins.text}</p>
</div>
))}
</div>
<button className="text-xs mt-2" style={{ color: 'var(--atlas-cyan)' }}>Alle Insights anzeigen </button>
</div>
</div>
</div>
{/* Top Inhalte */}
<div className="atlas-card p-4">
<div className="flex items-center justify-between mb-3">
<h2 className="text-sm font-semibold" style={{ fontFamily: 'var(--font-display)', color: 'var(--foreground)' }}>Top Inhalte</h2>
<button className="text-xs" style={{ color: 'var(--atlas-cyan)' }}>Alle Inhalte anzeigen </button>
</div>
<div className="flex gap-3">
{TOP_CONTENT.map((item, i) => (
<div key={i} className="flex-1 atlas-card p-2 cursor-pointer atlas-card-hover">
<div className="relative mb-2">
<img src={item.image} alt={item.title} className="w-full rounded" style={{ aspectRatio: '1/1', objectFit: 'cover' }} onError={e => { (e.target as HTMLImageElement).style.display = 'none'; }} />
<div
className="absolute top-1 left-1 w-5 h-5 rounded-full flex items-center justify-center text-xs font-bold"
style={{ background: `${RANK_COLORS[i]}20`, color: RANK_COLORS[i], border: `1px solid ${RANK_COLORS[i]}40`, fontFamily: 'var(--font-mono)', fontSize: '0.6rem' }}
>
{item.rank}
</div>
</div>
<div className="text-xs font-medium truncate" style={{ color: 'var(--foreground)', fontSize: '0.6875rem' }}>{item.title}</div>
<div className="text-xs font-bold mt-0.5" style={{ color: 'var(--atlas-green)', fontFamily: 'var(--font-mono)' }}>{item.revenue} Umsatz</div>
</div>
))}
</div>
</div>
</div>
);
}

View file

@ -0,0 +1,284 @@
/**
* Freigaben Prüfe, priorisiere und genehmige Inhalte vor der Veröffentlichung
* Referenz: docs/design-references/screen-freigaben.png
*/
import { useState } from 'react';
import { Filter, ChevronLeft, ChevronRight, MoreHorizontal, MessageSquare, Edit } from 'lucide-react';
import { toast } from 'sonner';
const FILTER_TABS = [
{ label: 'Alle', count: 48 },
{ label: 'Dringend', count: 7, color: '#ef4444' },
{ label: 'Mit Affiliate', count: 15 },
{ label: 'TikTok', count: 16 },
{ label: 'Instagram', count: 14 },
{ label: 'LinkedIn', count: 11 },
{ label: 'Review', count: 37 },
{ label: 'Blockiert', count: 3, color: '#ef4444' },
];
const QUEUE = [
{ priority: 'Dringend', title: 'Shelly Plug S Gen3 im Fokus', platform: 'TikTok', product: 'Shelly Plug S Gen3', affiliate: 'Tech Review Deals', scheduled: 'Heute 10:00', by: 'Lisa M.', byTime: '26. Mai, 08:15', status: 'Review', image: 'https://images.unsplash.com/photo-1558618666-fcd25c85cd64?w=60&h=60&fit=crop' },
{ priority: 'Dringend', title: 'Smart Metering einfach erklärt', platform: 'Instagram', product: 'Smart Meter Gen3', affiliate: 'Energie & Smart Living', scheduled: 'Heute 12:00', by: 'Tom S.', byTime: '26. Mai, 07:45', status: 'Review', image: 'https://images.unsplash.com/photo-1509391366360-2e959784a276?w=60&h=60&fit=crop' },
{ priority: 'Hoch', title: 'Outdoor & Energy-Saving Content für Sommer', platform: 'LinkedIn', product: 'Outdoor Sensor Gen2', affiliate: 'SmartHome Tech', scheduled: 'Morgen 09:00', by: 'Anna K.', byTime: '26. Mai, 07:36', status: 'Review', image: 'https://images.unsplash.com/photo-1506905925346-21bda4d32df4?w=60&h=60&fit=crop' },
{ priority: 'Hoch', title: 'Airfryer Trends 2025 Top Picks', platform: 'TikTok', product: 'Airfryer Pro', affiliate: 'Küchen Inspiration', scheduled: '27. Mai 10:00', by: 'Mark D.', byTime: '26. Mai, 18:20', status: 'Review', image: 'https://images.unsplash.com/photo-1556909114-f6e7ad7d3136?w=60&h=60&fit=crop' },
{ priority: 'Mittel', title: 'LED Strips richtig integrieren', platform: 'Instagram', product: 'LED Strip RGBW', affiliate: 'Home & Style Creators', scheduled: '27. Mai 14:30', by: 'Sarah L.', byTime: '25. Mai, 17:50', status: 'Review', image: 'https://images.unsplash.com/photo-1558618666-fcd25c85cd64?w=60&h=60&fit=crop' },
{ priority: 'Mittel', title: 'Sicherheit beginnt an der Tür', platform: 'LinkedIn', product: 'Door/Window 2', affiliate: 'Security First', scheduled: '28. Mai 09:00', by: 'Jonas P.', byTime: '25. Mai, 16:10', status: 'Review', image: 'https://images.unsplash.com/photo-1558618666-fcd25c85cd64?w=60&h=60&fit=crop' },
{ priority: 'Niedrig', title: 'Automation Szenen richtig nutzen', platform: 'TikTok', product: 'Motion Sensor Gen2', affiliate: 'SmartHome Automation', scheduled: '28. Mai 16:00', by: 'Lisa M.', byTime: '25. Mai, 15:05', status: 'Review', image: 'https://images.unsplash.com/photo-1558618666-fcd25c85cd64?w=60&h=60&fit=crop' },
{ priority: 'Niedrig', title: 'Shelly Plus Plug S im Alltag', platform: 'Instagram', product: 'Shelly Plus Plug S', affiliate: 'Budget Smart Home', scheduled: '29. Mai 10:00', by: 'Tom S.', byTime: '25. Mai, 14:30', status: 'Review', image: 'https://images.unsplash.com/photo-1558618666-fcd25c85cd64?w=60&h=60&fit=crop' },
{ priority: 'Niedrig', title: 'Gesponserter Post neues Bundle', platform: 'LinkedIn', product: 'Starter Bundle Pro', affiliate: 'Deals Germany', scheduled: '29. Mai 12:00', by: 'Mark D.', byTime: '25. Mai, 13:45', status: 'Blockiert', image: 'https://images.unsplash.com/photo-1558618666-fcd25c85cd64?w=60&h=60&fit=crop' },
{ priority: 'Niedrig', title: 'Smart Meter Features im Detail', platform: 'TikTok', product: 'Smart Meter Gen3', affiliate: 'Tech & Energy Experts', scheduled: '29. Mai 14:00', by: 'Anna K.', byTime: '25. Mai, 13:15', status: 'Blockiert', image: 'https://images.unsplash.com/photo-1558618666-fcd25c85cd64?w=60&h=60&fit=crop' },
];
const PRIORITY_COLORS: Record<string, { bg: string; text: string }> = {
Dringend: { bg: 'oklch(0.62 0.22 25 / 0.15)', text: '#ef4444' },
Hoch: { bg: 'oklch(0.75 0.15 75 / 0.15)', text: '#f59e0b' },
Mittel: { bg: 'oklch(0.82 0.18 210 / 0.1)', text: '#60a5fa' },
Niedrig: { bg: 'oklch(0.5 0.05 240 / 0.1)', text: '#64748b' },
};
const STATUS_COLORS: Record<string, { bg: string; text: string }> = {
Review: { bg: 'oklch(0.75 0.15 75 / 0.15)', text: '#f59e0b' },
Blockiert: { bg: 'oklch(0.62 0.22 25 / 0.15)', text: '#ef4444' },
Freigegeben: { bg: 'oklch(0.65 0.15 165 / 0.15)', text: '#10b981' },
};
const PLATFORM_ICONS: Record<string, string> = { TikTok: '🎵', Instagram: '📸', LinkedIn: '💼', 'X (Twitter)': '✗', 'YouTube Shorts': '▶' };
const AUDIT_LOG = [
{ user: 'Lisa M.', action: 'hat zur Prüfung eingereicht', time: '26. Mai 2025, 08:15', color: '#60a5fa' },
{ user: 'Auto-Check', action: 'Textlänge OK, Hashtags OK, Link OK, Bildqualität OK', time: '26. Mai 2025, 07:36', color: '#10b981' },
{ user: 'Tom S.', action: 'hat auf Review gesetzt', time: '26. Mai 2025, 08:05', color: '#a78bfa' },
{ user: 'Anna K.', action: 'Kommentar: "Klingt gut! Vielleicht noch einen CTA ergänzen"', time: '26. Mai 2025, 08:12', color: '#f59e0b' },
];
export default function Approvals() {
const [activeFilter, setActiveFilter] = useState('Alle');
const [selectedItem, setSelectedItem] = useState(QUEUE[2]);
const [queue, setQueue] = useState(QUEUE);
const handleApprove = (title: string) => {
setQueue(prev => prev.map(q => q.title === title ? { ...q, status: 'Freigegeben' } : q));
toast.success(`"${title}" freigegeben`);
};
return (
<div className="p-5 animate-fade-in-up space-y-4">
{/* Header */}
<div className="flex items-center justify-between">
<div>
<h1 className="text-xl font-bold" style={{ fontFamily: 'var(--font-display)', color: 'var(--foreground)' }}>Freigaben</h1>
<p className="text-sm mt-0.5" style={{ color: 'var(--muted-foreground)' }}>Prüfe, priorisiere und genehmige Inhalte vor der Veröffentlichung.</p>
</div>
<div className="flex items-center gap-2">
<span className="text-xs px-2 py-1 rounded" style={{ background: 'var(--atlas-surface)', border: '1px solid var(--atlas-border)', color: 'var(--foreground)' }}>26. Mai 2025</span>
<select className="text-xs px-2 py-1.5 rounded" style={{ background: 'var(--atlas-surface)', border: '1px solid var(--atlas-border)', color: 'var(--foreground)' }}>
<option>Alle Plattformen</option>
</select>
<button className="atlas-btn-ghost p-1.5"><Filter size={13} /></button>
</div>
</div>
{/* KPI Bar */}
<div className="grid grid-cols-4 gap-3">
{[
{ label: 'Offene Freigaben', value: '48', trend: '↑ 12% vs. gestern', color: '#a78bfa' },
{ label: 'Dringend', value: '7', trend: '↑ 75% vs. gestern', color: '#ef4444' },
{ label: 'Durchschnittliche Prüfzeit', value: '3h 24m', trend: '↑ 18% vs. gestern', color: '#10b981' },
{ label: 'Heute freigegeben', value: '26', trend: '↑ 24% vs. gestern', 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>
{/* Filter Tabs */}
<div className="flex items-center gap-1.5 flex-wrap">
{FILTER_TABS.map(tab => (
<button
key={tab.label}
onClick={() => setActiveFilter(tab.label)}
className="flex items-center gap-1 px-3 py-1.5 rounded text-xs font-medium transition-all"
style={{
background: activeFilter === tab.label ? 'oklch(0.82 0.18 210 / 0.12)' : 'var(--atlas-surface)',
color: activeFilter === tab.label ? 'var(--atlas-cyan)' : 'var(--muted-foreground)',
border: `1px solid ${activeFilter === tab.label ? 'oklch(0.82 0.18 210 / 0.3)' : 'var(--atlas-border)'}`,
}}
>
{tab.label}
{tab.count !== undefined && (
<span
className="rounded px-1"
style={{ background: tab.color ? `${tab.color}20` : 'var(--atlas-navy)', color: tab.color || 'var(--muted-foreground)', fontSize: '0.6rem' }}
>
{tab.count}
</span>
)}
</button>
))}
</div>
<div className="flex gap-4">
{/* Queue Table */}
<div className="flex-1 min-w-0">
<div className="atlas-card overflow-hidden">
<div className="p-3" style={{ borderBottom: '1px solid var(--atlas-border)' }}>
<span className="text-sm font-semibold" style={{ fontFamily: 'var(--font-display)', color: 'var(--foreground)' }}>
Freigaben-Queue (48)
</span>
</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 10px', textAlign: 'left' }}>
<span className="text-xs" style={{ color: 'var(--muted-foreground)' }}>{h}</span>
</th>
))}
</tr>
</thead>
<tbody>
{queue.map((item, i) => (
<tr
key={i}
className="cursor-pointer"
onClick={() => setSelectedItem(item)}
style={{
borderBottom: i < queue.length - 1 ? '1px solid var(--atlas-border)' : 'none',
background: selectedItem.title === item.title ? 'oklch(0.82 0.18 210 / 0.05)' : 'transparent',
}}
>
<td style={{ padding: '8px 10px' }}>
<span className="text-xs px-1.5 py-0.5 rounded" style={{ background: PRIORITY_COLORS[item.priority]?.bg, color: PRIORITY_COLORS[item.priority]?.text, fontSize: '0.6rem', border: `1px solid ${PRIORITY_COLORS[item.priority]?.text}30` }}>
{item.priority}
</span>
</td>
<td style={{ padding: '8px 10px' }}>
<img src={item.image} alt={item.title} className="rounded" style={{ width: '32px', height: '32px', objectFit: 'cover' }} onError={e => { (e.target as HTMLImageElement).style.display = 'none'; }} />
</td>
<td style={{ padding: '8px 10px', maxWidth: '160px' }}>
<div className="text-xs truncate" style={{ color: 'var(--foreground)' }}>{item.title}</div>
</td>
<td style={{ padding: '8px 10px' }}>
<span className="text-xs">{PLATFORM_ICONS[item.platform]} {item.platform}</span>
</td>
<td style={{ padding: '8px 10px' }}>
<span className="text-xs" style={{ color: 'var(--muted-foreground)', fontSize: '0.6875rem' }}>{item.product}</span>
</td>
<td style={{ padding: '8px 10px' }}>
<span className="text-xs" style={{ color: 'var(--muted-foreground)', fontSize: '0.6875rem' }}>{item.affiliate}</span>
</td>
<td style={{ padding: '8px 10px' }}>
<span className="text-xs" style={{ color: 'var(--foreground)', fontFamily: 'var(--font-mono)', fontSize: '0.6rem' }}>{item.scheduled}</span>
</td>
<td style={{ padding: '8px 10px' }}>
<div>
<div className="text-xs" style={{ color: 'var(--foreground)', fontSize: '0.6875rem' }}>{item.by}</div>
<div className="text-xs" style={{ color: 'var(--muted-foreground)', fontSize: '0.6rem' }}>{item.byTime}</div>
</div>
</td>
<td style={{ padding: '8px 10px' }}>
<span className="text-xs px-1.5 py-0.5 rounded" style={{ background: STATUS_COLORS[item.status]?.bg, color: STATUS_COLORS[item.status]?.text, fontSize: '0.6rem' }}>
{item.status}
</span>
</td>
<td style={{ padding: '8px 10px' }}>
<button className="p-1" style={{ color: 'var(--muted-foreground)' }}><MoreHorizontal size={12} /></button>
</td>
</tr>
))}
</tbody>
</table>
<div className="flex items-center justify-between px-3 py-2" style={{ borderTop: '1px solid var(--atlas-border)' }}>
<span className="text-xs" style={{ color: 'var(--muted-foreground)' }}>110 von 48 Freigaben</span>
<div className="flex items-center gap-1">
<button className="atlas-btn-ghost p-1"><ChevronLeft size={12} /></button>
{[1,2,3,4,5].map(p => (
<button key={p} className="w-6 h-6 text-xs rounded" style={{ background: p === 1 ? 'oklch(0.82 0.18 210 / 0.12)' : 'transparent', color: p === 1 ? 'var(--atlas-cyan)' : 'var(--muted-foreground)' }}>{p}</button>
))}
<button className="atlas-btn-ghost p-1"><ChevronRight size={12} /></button>
</div>
<select className="text-xs px-2 py-1 rounded" style={{ background: 'var(--atlas-surface)', border: '1px solid var(--atlas-border)', color: 'var(--foreground)' }}>
<option>10 pro Seite</option>
</select>
</div>
</div>
</div>
{/* Right: Preview + Audit */}
<div className="w-64 flex-shrink-0 space-y-3">
{/* Schnellvorschau */}
<div className="atlas-card overflow-hidden">
<div className="p-3" style={{ borderBottom: '1px solid var(--atlas-border)' }}>
<span className="text-xs font-semibold" style={{ fontFamily: 'var(--font-display)', color: 'var(--foreground)' }}>Schnellvorschau</span>
</div>
<div className="p-3">
<div className="flex items-center gap-2 mb-2">
<span style={{ fontSize: '14px' }}>{PLATFORM_ICONS[selectedItem.platform]}</span>
<span className="text-xs font-medium" style={{ color: 'var(--foreground)' }}>{selectedItem.platform}</span>
<span className="text-xs ml-auto" style={{ color: 'var(--muted-foreground)', fontSize: '0.6rem' }}>Beitrag</span>
</div>
<img
src={selectedItem.image}
alt={selectedItem.title}
className="w-full rounded mb-2"
style={{ aspectRatio: '4/3', objectFit: 'cover' }}
onError={e => { (e.target as HTMLImageElement).style.display = 'none'; }}
/>
<div className="text-xs font-medium mb-1" style={{ color: 'var(--foreground)' }}>{selectedItem.title}</div>
<div className="text-xs mb-2" style={{ color: 'var(--muted-foreground)' }}>
Geplant für: {selectedItem.scheduled}<br />
Affiliate: {selectedItem.affiliate}
</div>
<div className="flex flex-col gap-1.5">
<button
className="w-full text-xs py-1.5 rounded font-semibold"
style={{ background: 'var(--atlas-green)', color: 'white' }}
onClick={() => handleApprove(selectedItem.title)}
>
Freigeben
</button>
<button className="w-full text-xs py-1.5 rounded" style={{ background: 'var(--atlas-navy)', border: '1px solid var(--atlas-border)', color: 'var(--foreground)' }}>
Zurück an Entwurf
</button>
<div className="flex gap-1.5">
<button className="flex-1 text-xs py-1.5 rounded flex items-center justify-center gap-1" style={{ background: 'var(--atlas-navy)', border: '1px solid var(--atlas-border)', color: 'var(--foreground)' }}>
<MessageSquare size={11} /> Kommentieren
</button>
<button className="flex-1 text-xs py-1.5 rounded flex items-center justify-center gap-1" style={{ background: 'var(--atlas-navy)', border: '1px solid var(--atlas-border)', color: 'var(--foreground)' }}>
<Edit size={11} /> Bearbeiten
</button>
</div>
</div>
</div>
</div>
{/* Audit Log */}
<div className="atlas-card p-3">
<div className="text-xs font-semibold mb-2" style={{ fontFamily: 'var(--font-display)', color: 'var(--foreground)' }}>Audit-Verlauf</div>
<div className="space-y-2">
{AUDIT_LOG.map((entry, i) => (
<div key={i} className="flex gap-2">
<div className="w-6 h-6 rounded-full flex items-center justify-center flex-shrink-0 text-xs font-bold" style={{ background: `${entry.color}20`, color: entry.color }}>
{entry.user[0]}
</div>
<div>
<div className="text-xs" style={{ color: 'var(--foreground)' }}>
<strong>{entry.user}</strong> {entry.action}
</div>
<div className="text-xs" style={{ color: 'var(--muted-foreground)', fontSize: '0.6rem' }}>{entry.time}</div>
</div>
</div>
))}
</div>
<button className="text-xs mt-2" style={{ color: 'var(--atlas-cyan)' }}>Alle Kommentare anzeigen </button>
</div>
</div>
</div>
</div>
);
}

View file

@ -0,0 +1,305 @@
/**
* 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="flex items-center justify-between">
<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="flex items-center gap-2">
<div className="flex items-center gap-1.5 px-3 py-1.5 rounded" style={{ background: 'var(--atlas-surface)', border: '1px solid var(--atlas-border)' }}>
<span className="text-xs" style={{ color: 'var(--foreground)' }}>26. Mai 1. Jun 2025</span>
</div>
<button className="atlas-btn-ghost p-1.5"><ChevronLeft size={14} /></button>
<button className="atlas-btn-ghost p-1.5"><ChevronRight size={14} /></button>
<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-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>
);
}

View file

@ -0,0 +1,246 @@
/**
* Kampagnen Plane, bündele und steuere kanalübergreifende Content-Offensiven
* Referenz: docs/design-references/screen-kampagnen.png
*/
import { useState } from 'react';
import { ChevronLeft, ChevronRight, Plus, TrendingUp } from 'lucide-react';
const CAMPAIGNS = [
{
id: '1', title: 'Shelly Sommer-Smart-Home', status: 'Im Gange', statusColor: '#10b981',
description: 'Mehr Komfort. Weniger Kosten. Smarter Sommer mit Shelly.',
dateRange: '20. Mai 15. Juni 2025', channels: ['TikTok', 'Instagram', 'LinkedIn', 'YouTube'],
budget: '€12.000', reach: '1,2 Mio.', affiliatePotential: '€14.320', affiliateTrend: '+22%',
image: 'https://images.unsplash.com/photo-1558618666-fcd25c85cd64?w=300&h=160&fit=crop',
score: 92,
},
{
id: '2', title: 'Kitchen Trend Woche', status: 'Geplant', statusColor: '#f59e0b', priority: 'Top Priorität',
description: 'Airfryer, Zubehör & smarte Küchenhelfer eine Trend-Highlights.',
dateRange: '2. Juni 8. Juni 2025', channels: ['Instagram', 'TikTok', 'YouTube', 'LinkedIn'],
budget: '€8.000', reach: '850 Tsd.', affiliatePotential: '€9.810', affiliateTrend: '+15%',
image: 'https://images.unsplash.com/photo-1556909114-f6e7ad7d3136?w=300&h=160&fit=crop',
score: 88,
},
{
id: '3', title: 'KI-News Sprint', status: 'Im Gange', statusColor: '#10b981',
description: 'Die wichtigsten KI-Updates kompakt erklärt jeden Tag, jede Woche.',
dateRange: '26. Mai 9. Juni 2025', channels: ['LinkedIn', 'X', 'TikTok', 'Instagram'],
budget: '€4.500', reach: '620 Tsd.', affiliatePotential: '€3.890', affiliateTrend: '+15%',
image: 'https://images.unsplash.com/photo-1677442135703-1787eea5ce01?w=300&h=160&fit=crop',
score: 84,
},
{
id: '4', title: 'Energie sparen im Juni', status: 'Geplant', statusColor: '#f59e0b',
description: 'Clever sparen, nachhaltig leben mit Shelly und smarter Technik.',
dateRange: '3. Juni 30. Juni 2025', channels: ['TikTok', 'Instagram', 'Facebook', 'YouTube'],
budget: '€6.000', reach: '900 Tsd.', affiliatePotential: '€6.730', affiliateTrend: '+12%',
image: 'https://images.unsplash.com/photo-1509391366360-2e959784a276?w=300&h=160&fit=crop',
score: 81,
},
];
const TIMELINE_STAGES = ['Briefing', 'Drafts', 'Review', 'Live', 'Optimierung'];
const STAGE_COLORS: Record<string, string> = {
Briefing: '#60a5fa', Drafts: '#a78bfa', Review: '#f59e0b', Live: '#10b981', Optimierung: '#00d4ff',
};
const TIMELINE_DATA: Record<string, Record<string, string>> = {
'Shelly Sommer-Smart-Home': { 'Mo 26. Mai': 'Briefing', 'Di 27. Mai': 'Drafts', 'Mi 28. Mai': 'Review', 'Do 29. Mai': 'Live', 'Fr 30. Mai': 'Optimierung' },
'Kitchen Trend Woche': { 'Di 27. Mai': 'Briefing', 'Mi 28. Mai': 'Drafts', 'Do 29. Mai': 'Review', 'Fr 30. Mai': 'Live', 'Sa 31. Mai': 'Optimierung' },
'KI-News Sprint': { 'Mo 26. Mai': 'Briefing', 'Di 27. Mai': 'Drafts', 'Mi 28. Mai': 'Live', 'Do 29. Mai': 'Optimierung' },
'Energie sparen im Juni': { 'Di 27. Mai': 'Briefing', 'Do 29. Mai': 'Drafts', 'Fr 30. Mai': 'Review', 'Sa 31. Mai': 'Live', 'So 1. Jun': 'Optimierung' },
};
const DAYS = ['Mo 26. Mai', 'Di 27. Mai', 'Mi 28. Mai', 'Do 29. Mai', 'Fr 30. Mai', 'Sa 31. Mai', 'So 1. Jun'];
const PLATFORM_ICONS: Record<string, string> = { TikTok: '🎵', Instagram: '📸', LinkedIn: '💼', X: '✗', YouTube: '▶', Facebook: '👥' };
export default function Campaigns() {
const [view, setView] = useState<'grid' | 'list'>('grid');
return (
<div className="p-5 animate-fade-in-up space-y-4">
{/* Header */}
<div className="flex items-center justify-between">
<div>
<h1 className="text-xl font-bold" style={{ fontFamily: 'var(--font-display)', color: 'var(--foreground)' }}>Kampagnen</h1>
<p className="text-sm mt-0.5" style={{ color: 'var(--muted-foreground)' }}>Plane, bündele und steuere kanalübergreifende Content-Offensiven.</p>
</div>
<div className="flex items-center gap-2">
<div className="flex items-center gap-1.5 px-3 py-1.5 rounded" style={{ background: 'var(--atlas-surface)', border: '1px solid var(--atlas-border)' }}>
<span className="text-xs" style={{ color: 'var(--foreground)' }}>26. Mai 1. Jun 2025</span>
</div>
<button className="atlas-btn-ghost p-1.5"><ChevronLeft size={14} /></button>
<button className="atlas-btn-ghost p-1.5"><ChevronRight size={14} /></button>
<select className="text-xs px-2 py-1.5 rounded" style={{ background: 'var(--atlas-surface)', border: '1px solid var(--atlas-border)', color: 'var(--foreground)' }}>
<option>Diese Woche</option>
</select>
<button className="atlas-btn-primary text-xs gap-1.5"><Plus size={13} /> Neue Kampagne</button>
</div>
</div>
{/* KPI Bar */}
<div className="grid grid-cols-4 gap-3">
{[
{ label: 'Aktive Kampagnen', value: '12', trend: '↑ 20% vs. letzte Woche', color: '#60a5fa' },
{ label: 'Geplante Assets', value: '86', trend: '↑ 16% vs. letzte Woche', color: '#a78bfa' },
{ label: 'Ø Conversion', value: '3,42 %', trend: '↑ 0,48 p.p. vs. letzte Woche', color: '#10b981' },
{ label: 'Proj. Affiliate-Umsatz', value: '€28.750', trend: '↑ 18% 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>
<div className="flex gap-4">
{/* Campaign Cards */}
<div className="flex-1 min-w-0 space-y-3">
<div className="flex items-center justify-between">
<h2 className="text-sm font-semibold" style={{ fontFamily: 'var(--font-display)', color: 'var(--foreground)' }}>Kampagnen-Übersicht</h2>
</div>
<div className="grid grid-cols-2 gap-3">
{CAMPAIGNS.map(c => (
<div key={c.id} className="atlas-card atlas-card-hover overflow-hidden cursor-pointer">
<div className="relative" style={{ height: '100px' }}>
<img src={c.image} alt={c.title} style={{ width: '100%', height: '100%', objectFit: 'cover', opacity: 0.6 }} />
<div className="absolute inset-0" style={{ background: 'linear-gradient(to top, rgba(15,22,41,0.9) 0%, transparent 60%)' }} />
<div className="absolute top-2 left-2 flex items-center gap-1.5">
<span className="text-xs px-1.5 py-0.5 rounded" style={{ background: `${c.statusColor}25`, color: c.statusColor, border: `1px solid ${c.statusColor}40`, fontSize: '0.6rem' }}>
{c.status}
</span>
{c.priority && <span className="text-xs px-1.5 py-0.5 rounded" style={{ background: '#f59e0b25', color: '#f59e0b', border: '1px solid #f59e0b40', fontSize: '0.6rem' }}> {c.priority}</span>}
</div>
</div>
<div className="p-3">
<div className="text-sm font-semibold mb-1" style={{ fontFamily: 'var(--font-display)', color: 'var(--foreground)' }}>{c.title}</div>
<div className="text-xs mb-2" style={{ color: 'var(--muted-foreground)', lineHeight: '1.5' }}>{c.description}</div>
<div className="text-xs mb-2" style={{ color: 'var(--muted-foreground)', fontFamily: 'var(--font-mono)', fontSize: '0.6rem' }}>📅 {c.dateRange}</div>
<div className="flex gap-1 mb-3">
{c.channels.slice(0, 4).map(ch => (
<span key={ch} style={{ fontSize: '14px' }}>{PLATFORM_ICONS[ch] || '📱'}</span>
))}
{c.channels.length > 4 && <span className="text-xs" style={{ color: 'var(--muted-foreground)' }}>+{c.channels.length - 4}</span>}
</div>
<div className="grid grid-cols-2 gap-2 mb-3">
<div>
<div className="text-xs" style={{ color: 'var(--muted-foreground)', fontSize: '0.6rem' }}>Budget / Reach (geschätzt)</div>
<div className="text-xs font-bold" style={{ fontFamily: 'var(--font-mono)', color: 'var(--foreground)' }}>{c.budget} / {c.reach}</div>
</div>
<div>
<div className="text-xs" style={{ color: 'var(--muted-foreground)', fontSize: '0.6rem' }}>Affiliate-Potenzial</div>
<div className="flex items-center gap-1">
<span className="text-xs font-bold" style={{ fontFamily: 'var(--font-mono)', color: 'var(--atlas-green)' }}>{c.affiliatePotential}</span>
<span className="text-xs" style={{ color: 'var(--atlas-green)', fontSize: '0.6rem' }}>{c.affiliateTrend} vs. Plan</span>
</div>
</div>
</div>
<div className="flex gap-2">
<button className="flex-1 text-xs py-1.5 rounded" style={{ background: 'var(--atlas-cyan)', color: 'var(--atlas-navy)', fontFamily: 'var(--font-display)', fontWeight: 600 }}>Öffnen</button>
<button className="flex-1 text-xs py-1.5 rounded" style={{ background: 'var(--atlas-navy)', border: '1px solid var(--atlas-border)', color: 'var(--foreground)' }}>Bearbeiten</button>
</div>
</div>
</div>
))}
</div>
{/* Timeline */}
<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)' }}>Kampagnen-Timeline & Content-Wellen</h2>
</div>
</div>
<div className="overflow-x-auto">
<table style={{ width: '100%', borderCollapse: 'collapse', minWidth: '700px' }}>
<thead>
<tr style={{ borderBottom: '1px solid var(--atlas-border)' }}>
<th style={{ width: '180px', padding: '8px 12px', textAlign: 'left' }}><span className="text-xs" style={{ color: 'var(--muted-foreground)' }}>Kampagne</span></th>
{DAYS.map(d => <th key={d} style={{ padding: '8px 8px', textAlign: 'center' }}><span className="text-xs" style={{ color: 'var(--muted-foreground)', fontFamily: 'var(--font-mono)', fontSize: '0.6rem' }}>{d}</span></th>)}
</tr>
</thead>
<tbody>
{CAMPAIGNS.map((c, ci) => (
<tr key={c.id} style={{ borderBottom: ci < CAMPAIGNS.length - 1 ? '1px solid var(--atlas-border)' : 'none' }}>
<td style={{ padding: '8px 12px' }}>
<div className="text-xs font-medium" style={{ color: 'var(--foreground)' }}>{c.title}</div>
<span className="text-xs" style={{ color: c.statusColor, fontSize: '0.6rem' }}>{c.status}</span>
</td>
{DAYS.map(d => {
const stage = TIMELINE_DATA[c.title]?.[d];
return (
<td key={d} style={{ padding: '6px 4px', textAlign: 'center' }}>
{stage ? (
<span
className="text-xs px-2 py-0.5 rounded"
style={{ background: `${STAGE_COLORS[stage]}20`, color: STAGE_COLORS[stage], fontSize: '0.6rem', whiteSpace: 'nowrap' }}
>
{stage}
</span>
) : (
<span style={{ color: 'var(--atlas-border)' }}></span>
)}
</td>
);
})}
</tr>
))}
</tbody>
</table>
</div>
{/* Legend */}
<div className="flex items-center gap-4 px-3 py-2" style={{ borderTop: '1px solid var(--atlas-border)' }}>
{TIMELINE_STAGES.map(s => (
<div key={s} className="flex items-center gap-1">
<div className="w-2 h-2 rounded-full" style={{ background: STAGE_COLORS[s] }} />
<span className="text-xs" style={{ color: 'var(--muted-foreground)', fontSize: '0.6rem' }}>{s}</span>
</div>
))}
</div>
</div>
</div>
{/* Right Sidebar */}
<div className="w-56 flex-shrink-0 space-y-3">
<div className="atlas-card p-3">
<div className="text-xs font-semibold mb-2" style={{ fontFamily: 'var(--font-display)', color: 'var(--foreground)' }}>Top Kampagnen heute</div>
<div className="space-y-2">
{CAMPAIGNS.map((c, i) => (
<div key={c.id} className="flex items-center gap-2">
<span className="text-xs font-bold" style={{ color: 'var(--muted-foreground)', fontFamily: 'var(--font-mono)', width: '16px' }}>{i + 1}</span>
<div className="flex-1 min-w-0">
<div className="text-xs truncate" style={{ color: 'var(--foreground)' }}>{c.title}</div>
<span className="text-xs" style={{ color: c.statusColor, fontSize: '0.6rem' }}>{c.status}</span>
</div>
<div className="text-right">
<div className="text-xs font-bold" style={{ fontFamily: 'var(--font-mono)', color: 'var(--atlas-cyan)' }}>{c.score}</div>
<div className="text-xs" style={{ color: 'var(--atlas-green)', fontSize: '0.6rem' }}>{c.affiliateTrend.replace('+', '')}</div>
</div>
</div>
))}
</div>
</div>
<div className="atlas-card p-3">
<div className="text-xs font-semibold mb-2" style={{ fontFamily: 'var(--font-display)', color: 'var(--foreground)' }}>Kampagnen-Insights</div>
<div className="space-y-2">
{[
'2 Kampagnen laufen outperformend (+20%+ vs. Plan)',
'Durchschnittliche Conversion steigt um 0,48 p.p.',
'Affiliates generieren +18% mehr Umsatz vs. letzte Woche.',
].map((insight, i) => (
<div key={i} className="flex gap-2">
<div className="w-1.5 h-1.5 rounded-full mt-1.5 flex-shrink-0" style={{ background: 'var(--atlas-cyan)' }} />
<p className="text-xs" style={{ color: 'var(--muted-foreground)', lineHeight: '1.5' }}>{insight}</p>
</div>
))}
</div>
</div>
</div>
</div>
</div>
);
}

402
client/src/pages/Drafts.tsx Normal file
View file

@ -0,0 +1,402 @@
/**
* Draft Studio Erstelle, optimiere und bereite Inhalte für alle Kanäle vor
* Referenz: docs/design-references/screen-draft-studio.png
*/
import { useState } from 'react';
import { useLocation } from 'wouter';
import { ArrowLeft, Save, Send, ExternalLink, MoreHorizontal, Plus, ChevronDown } from 'lucide-react';
import { toast } from 'sonner';
const PLATFORMS = ['TikTok', 'Instagram', 'LinkedIn', 'X (Twitter)'];
const PLATFORM_ICONS: Record<string, string> = {
TikTok: '🎵',
Instagram: '📸',
LinkedIn: '💼',
'X (Twitter)': '✗',
};
const MOCK_PRODUCTS = [
{ id: '1', name: 'Shelly Plug S Gen3', price: '€24,95', category: 'Smart Home / Energie', available: true },
{ id: '2', name: 'Shelly Plus 1', price: '€19,95', category: 'Smart Home', available: true },
{ id: '3', name: 'Airfryer Pro X', price: '€129,00', category: 'Kitchen', available: true },
];
const MOCK_DRAFTS = [
{ id: '1', title: 'Shelly Plug S Gen3 smarter Energie sparen', status: 'Entwurf', platform: 'TikTok', updated: 'Heute, 10:42' },
{ id: '2', title: 'Airfryer Trends 2025 Top Picks', status: 'Review', platform: 'Instagram', updated: 'Gestern, 15:20' },
{ id: '3', title: 'KI-News Update Was du wissen musst', status: 'Entwurf', platform: 'LinkedIn', updated: 'Gestern, 09:15' },
{ id: '4', title: 'Balkonkraftwerk Starter Guide', status: 'Freigegeben', platform: 'YouTube Shorts', updated: '25. Mai, 14:00' },
];
export default function Drafts() {
const [, navigate] = useLocation();
const [selectedDraft, setSelectedDraft] = useState<string | null>('1');
const [activePlatform, setActivePlatform] = useState('TikTok');
const [title, setTitle] = useState('Shelly Plug S Gen3 smarter Energie sparen');
const [hook, setHook] = useState('Stromkosten senken war noch nie so einfach! 💡');
const [caption, setCaption] = useState('Der Shelly Plug S Gen3 macht dein Zuhause smarter und spart Energie automatisch und zuverlässig.\n\nKlein, smart, effizient: Jetzt entdecken!');
const [cta, setCta] = useState('Jetzt entdecken & sparen →');
const [hashtags, setHashtags] = useState('#Shelly #ShellyPlugS #SmartHome #EnergieSparen #Automation #IoT');
const [selectedProduct, setSelectedProduct] = useState(MOCK_PRODUCTS[0]);
const handleSave = () => toast.success('Entwurf gespeichert');
const handleSendReview = () => toast.success('Zur Freigabe gesendet');
const handleOpenPostiz = () => window.open('https://postiz.atlas-os.ai', '_blank');
const captionLength = caption.length;
const maxCaption = 1500;
const affiliateUrl = `https://shelly.shop/plug-s-gen3?utm_source=atlassocial&utm_medium=social&utm_campaign=shelly-plug-s-gen3-launch`;
if (!selectedDraft) {
// Draft list view
return (
<div className="p-5 animate-fade-in-up">
<div className="flex items-center justify-between mb-4">
<div>
<h1 className="text-xl font-bold" style={{ fontFamily: 'var(--font-display)', color: 'var(--foreground)' }}>
Draft Studio
</h1>
<p className="text-sm mt-0.5" style={{ color: 'var(--muted-foreground)' }}>
Erstelle, optimiere und bereite deine Inhalte für alle Kanäle vor.
</p>
</div>
<button className="atlas-btn-primary" onClick={() => setSelectedDraft('new')}>
<Plus size={14} /> Neuer Entwurf
</button>
</div>
<div className="space-y-2">
{MOCK_DRAFTS.map(draft => (
<div
key={draft.id}
className="atlas-card atlas-card-hover p-3 cursor-pointer flex items-center gap-3"
onClick={() => setSelectedDraft(draft.id)}
>
<div className="flex-1">
<div className="text-sm font-medium" style={{ color: 'var(--foreground)', fontFamily: 'var(--font-display)' }}>
{draft.title}
</div>
<div className="flex items-center gap-2 mt-1">
<span className="text-xs" style={{ color: 'var(--muted-foreground)' }}>{PLATFORM_ICONS[draft.platform]} {draft.platform}</span>
<span style={{ color: 'var(--atlas-border)' }}>·</span>
<span className="text-xs" style={{ color: 'var(--muted-foreground)' }}>Letzte Änderung: {draft.updated}</span>
</div>
</div>
<span
className="text-xs px-2 py-0.5 rounded"
style={{
background: draft.status === 'Freigegeben' ? 'oklch(0.65 0.15 165 / 0.15)' : draft.status === 'Review' ? 'oklch(0.75 0.15 75 / 0.15)' : 'oklch(0.82 0.18 210 / 0.1)',
color: draft.status === 'Freigegeben' ? 'var(--atlas-green)' : draft.status === 'Review' ? 'var(--atlas-amber)' : 'var(--atlas-cyan)',
border: `1px solid ${draft.status === 'Freigegeben' ? 'oklch(0.65 0.15 165 / 0.25)' : draft.status === 'Review' ? 'oklch(0.75 0.15 75 / 0.25)' : 'oklch(0.82 0.18 210 / 0.2)'}`,
}}
>
{draft.status}
</span>
</div>
))}
</div>
</div>
);
}
return (
<div className="flex h-full animate-fade-in-up">
{/* Left: Editor */}
<div className="flex-1 overflow-y-auto atlas-scrollbar p-5 space-y-4">
{/* Header */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<button className="atlas-btn-ghost p-1.5" onClick={() => setSelectedDraft(null)}>
<ArrowLeft size={14} />
</button>
<div>
<div className="flex items-center gap-2">
<span className="atlas-badge-cyan" style={{ fontSize: '0.6rem' }}>Entwurf</span>
<span className="text-xs" style={{ color: 'var(--muted-foreground)' }}>Letzte Änderung: Heute, 10:42</span>
</div>
</div>
</div>
<div className="flex items-center gap-2">
<button className="atlas-btn-ghost text-xs gap-1.5" onClick={handleSave}>
<Save size={13} /> Als Draft speichern
</button>
<button className="atlas-btn-ghost text-xs gap-1.5" onClick={handleSendReview}>
<Send size={13} /> Zur Freigabe senden
</button>
<button className="atlas-btn-primary text-xs gap-1.5" onClick={handleOpenPostiz}>
<ExternalLink size={13} /> In Postiz öffnen
</button>
<button className="atlas-btn-ghost p-1.5">
<MoreHorizontal size={14} />
</button>
</div>
</div>
{/* Title */}
<div>
<label className="text-xs mb-1 block" style={{ color: 'var(--muted-foreground)' }}>Titel des Entwurfs</label>
<div className="relative">
<input
className="atlas-input pr-16"
value={title}
onChange={e => setTitle(e.target.value)}
maxLength={100}
/>
<span className="absolute right-3 top-1/2 -translate-y-1/2 text-xs" style={{ color: 'var(--muted-foreground)', fontFamily: 'var(--font-mono)' }}>
{title.length}/100
</span>
</div>
</div>
{/* Product */}
<div>
<label className="text-xs mb-1 block" style={{ color: 'var(--muted-foreground)' }}>Produkt</label>
<div className="atlas-card p-3 flex items-center gap-3">
<img
src="https://images.unsplash.com/photo-1558618666-fcd25c85cd64?w=60&h=60&fit=crop"
alt={selectedProduct.name}
className="rounded"
style={{ width: '48px', height: '48px', objectFit: 'cover' }}
/>
<div className="flex-1">
<div className="text-sm font-medium" style={{ color: 'var(--foreground)', fontFamily: 'var(--font-display)' }}>
{selectedProduct.name}
</div>
<div className="text-xs" style={{ color: 'var(--muted-foreground)' }}>
{selectedProduct.price} · {selectedProduct.category}
</div>
</div>
<div className="flex items-center gap-1">
<div className="atlas-status-dot online" />
<span className="text-xs" style={{ color: 'var(--atlas-green)' }}>Verfügbar</span>
</div>
<button className="atlas-btn-ghost text-xs">Produkt ändern</button>
</div>
</div>
{/* Affiliate Link */}
<div>
<div className="flex items-center justify-between mb-1">
<label className="text-xs" style={{ color: 'var(--muted-foreground)' }}>Affiliate-Link & UTM</label>
<div className="flex items-center gap-1.5">
<span className="atlas-badge-green" style={{ fontSize: '0.6rem' }}>Affiliate-Link aktiv</span>
<div className="w-8 h-4 rounded-full" style={{ background: 'var(--atlas-green)', position: 'relative' }}>
<div className="absolute right-0.5 top-0.5 w-3 h-3 rounded-full bg-white" />
</div>
</div>
</div>
<div className="atlas-card p-3 space-y-2">
<div className="grid grid-cols-3 gap-2">
<div>
<div className="text-xs mb-1" style={{ color: 'var(--muted-foreground)', fontSize: '0.6rem' }}>Campaign</div>
<input className="atlas-input text-xs" value="shelly-plug-s-gen3-launch" readOnly />
</div>
<div>
<div className="text-xs mb-1" style={{ color: 'var(--muted-foreground)', fontSize: '0.6rem' }}>Source</div>
<input className="atlas-input text-xs" value="atlassocial" readOnly />
</div>
<div>
<div className="text-xs mb-1" style={{ color: 'var(--muted-foreground)', fontSize: '0.6rem' }}>Medium</div>
<input className="atlas-input text-xs" value="social" readOnly />
</div>
</div>
<div className="flex items-center gap-2">
<input className="atlas-input text-xs flex-1" value={affiliateUrl} readOnly />
<button className="atlas-btn-ghost text-xs px-2 py-1.5" onClick={() => { navigator.clipboard.writeText(affiliateUrl); toast.success('Link kopiert'); }}>
Kopieren
</button>
</div>
</div>
</div>
{/* Content Blocks */}
<div className="space-y-3">
<div className="text-xs font-semibold" style={{ color: 'var(--foreground)', fontFamily: 'var(--font-display)' }}>Content Blöcke</div>
<div>
<div className="flex items-center gap-1.5 mb-1">
<span style={{ fontSize: '12px' }}>🪝</span>
<label className="text-xs" style={{ color: 'var(--muted-foreground)' }}>Hook</label>
</div>
<input className="atlas-input" value={hook} onChange={e => setHook(e.target.value)} />
</div>
<div>
<div className="flex items-center gap-1.5 mb-1">
<span style={{ fontSize: '12px' }}>💬</span>
<label className="text-xs" style={{ color: 'var(--muted-foreground)' }}>Caption</label>
<span className="ml-auto text-xs" style={{ color: captionLength > maxCaption * 0.9 ? 'var(--atlas-amber)' : 'var(--muted-foreground)', fontFamily: 'var(--font-mono)' }}>
{captionLength}/{maxCaption}
</span>
</div>
<textarea
className="atlas-input resize-none"
rows={4}
value={caption}
onChange={e => setCaption(e.target.value)}
/>
</div>
<div>
<div className="flex items-center gap-1.5 mb-1">
<span style={{ fontSize: '12px' }}>📣</span>
<label className="text-xs" style={{ color: 'var(--muted-foreground)' }}>CTA</label>
</div>
<input className="atlas-input" value={cta} onChange={e => setCta(e.target.value)} />
</div>
<div>
<div className="flex items-center gap-1.5 mb-1">
<span style={{ fontSize: '12px' }}>#</span>
<label className="text-xs" style={{ color: 'var(--muted-foreground)' }}>Hashtags</label>
<span className="ml-auto text-xs" style={{ color: 'var(--muted-foreground)', fontFamily: 'var(--font-mono)' }}>
{hashtags.split('#').filter(Boolean).length}/30
</span>
</div>
<input className="atlas-input" value={hashtags} onChange={e => setHashtags(e.target.value)} />
</div>
</div>
{/* Freigabe Status */}
<div className="atlas-card p-3">
<div className="grid grid-cols-2 gap-4">
<div>
<div className="text-xs mb-1" style={{ color: 'var(--muted-foreground)', fontSize: '0.6rem' }}>Status</div>
<span className="atlas-badge-cyan">Entwurf</span>
</div>
<div>
<div className="text-xs mb-1" style={{ color: 'var(--muted-foreground)', fontSize: '0.6rem' }}>Erstellt von</div>
<span className="text-xs" style={{ color: 'var(--foreground)' }}>Atlas Team</span>
</div>
</div>
</div>
</div>
{/* Right: Preview */}
<div className="w-96 flex-shrink-0 border-l overflow-y-auto atlas-scrollbar" style={{ borderColor: 'var(--atlas-border)', background: 'var(--atlas-navy)' }}>
<div className="p-4">
<div className="text-sm font-semibold mb-3" style={{ fontFamily: 'var(--font-display)', color: 'var(--foreground)' }}>
Vorschau
</div>
{/* Platform Tabs */}
<div className="flex gap-1 mb-4 overflow-x-auto">
{PLATFORMS.map(p => (
<button
key={p}
onClick={() => setActivePlatform(p)}
className="flex items-center gap-1 px-2 py-1.5 rounded text-xs whitespace-nowrap transition-all"
style={{
background: activePlatform === p ? 'oklch(0.82 0.18 210 / 0.12)' : 'transparent',
color: activePlatform === p ? 'var(--atlas-cyan)' : 'var(--muted-foreground)',
border: `1px solid ${activePlatform === p ? 'oklch(0.82 0.18 210 / 0.3)' : 'var(--atlas-border)'}`,
}}
>
{PLATFORM_ICONS[p]} {p}
</button>
))}
</div>
{/* Preview Card */}
<div className="atlas-card overflow-hidden">
{activePlatform === 'TikTok' && (
<div className="relative" style={{ background: '#000', borderRadius: '8px', overflow: 'hidden', aspectRatio: '9/16', maxHeight: '320px' }}>
<img
src="https://images.unsplash.com/photo-1558618666-fcd25c85cd64?w=400&h=700&fit=crop"
alt="Preview"
style={{ width: '100%', height: '100%', objectFit: 'cover', opacity: 0.7 }}
/>
<div className="absolute inset-0 flex flex-col justify-end p-3" style={{ background: 'linear-gradient(to top, rgba(0,0,0,0.8) 0%, transparent 60%)' }}>
<div className="text-xs font-bold text-white mb-1">@atlassocial</div>
<div className="text-xs text-white mb-1">{hook}</div>
<div className="text-xs text-white opacity-80 line-clamp-2">{caption.split('\n')[0]}</div>
<div className="text-xs mt-1" style={{ color: 'var(--atlas-cyan)' }}>{cta}</div>
</div>
<div className="absolute right-2 bottom-16 flex flex-col items-center gap-3">
<div className="text-white text-center">
<div style={{ fontSize: '18px' }}></div>
<div className="text-xs">128</div>
</div>
<div className="text-white text-center">
<div style={{ fontSize: '18px' }}>💬</div>
<div className="text-xs">12</div>
</div>
<div className="text-white text-center">
<div style={{ fontSize: '18px' }}>🔖</div>
<div className="text-xs">16</div>
</div>
</div>
</div>
)}
{activePlatform === 'Instagram' && (
<div>
<div className="flex items-center gap-2 p-3" style={{ borderBottom: '1px solid var(--atlas-border)' }}>
<div className="w-7 h-7 rounded-full" style={{ background: 'var(--atlas-cyan)' }} />
<div>
<div className="text-xs font-medium" style={{ color: 'var(--foreground)' }}>atlassocial</div>
<div className="text-xs" style={{ color: 'var(--muted-foreground)', fontSize: '0.6rem' }}>Gesponsert</div>
</div>
</div>
<img
src="https://images.unsplash.com/photo-1558618666-fcd25c85cd64?w=400&h=400&fit=crop"
alt="Preview"
style={{ width: '100%', aspectRatio: '1/1', objectFit: 'cover' }}
/>
<div className="p-3">
<div className="text-xs font-medium mb-1" style={{ color: 'var(--atlas-cyan)' }}>{cta}</div>
<div className="text-xs" style={{ color: 'var(--foreground)' }}>
<strong>atlassocial</strong> {caption.split('\n')[0]}
</div>
<div className="text-xs mt-1" style={{ color: 'var(--atlas-cyan)' }}>
{hashtags.split(' ').slice(0, 3).join(' ')}
</div>
</div>
</div>
)}
{(activePlatform === 'LinkedIn' || activePlatform === 'X (Twitter)') && (
<div className="p-3">
<div className="flex items-center gap-2 mb-2">
<div className="w-8 h-8 rounded-full" style={{ background: 'var(--atlas-cyan)' }} />
<div>
<div className="text-xs font-medium" style={{ color: 'var(--foreground)' }}>ATLAS Social</div>
<div className="text-xs" style={{ color: 'var(--muted-foreground)', fontSize: '0.6rem' }}>@atlassocial · Gesponsert</div>
</div>
</div>
<div className="text-xs mb-2" style={{ color: 'var(--foreground)', lineHeight: '1.6' }}>
{hook}<br /><br />{caption.split('\n')[0]}
</div>
<div className="text-xs mb-2" style={{ color: 'var(--atlas-cyan)' }}>{cta}</div>
<div className="text-xs" style={{ color: 'var(--atlas-cyan)', opacity: 0.7 }}>
{hashtags.split(' ').slice(0, 4).join(' ')}
</div>
</div>
)}
</div>
{/* AI Insights */}
<div className="atlas-card p-3 mt-3">
<div className="flex items-center gap-1.5 mb-2">
<span style={{ fontSize: '12px' }}>🤖</span>
<span className="text-xs font-semibold" style={{ fontFamily: 'var(--font-display)', color: 'var(--foreground)' }}>AI-Insights</span>
<span className="atlas-badge-cyan" style={{ fontSize: '0.55rem' }}>BETA</span>
</div>
<div className="space-y-1.5">
{[
{ icon: '✅', text: 'Starker Hook hohe Aufmerksamkeit erwartet' },
{ icon: '✅', text: 'CTA ist klar und handlungsorientiert' },
{ icon: '✅', text: 'Hashtag-Mix ist ausgewogen' },
{ icon: '✅', text: 'Bildmaterial unterstützt die Botschaft sehr gut' },
].map((insight, i) => (
<div key={i} className="flex items-center gap-2">
<span style={{ fontSize: '11px' }}>{insight.icon}</span>
<span className="text-xs" style={{ color: 'var(--muted-foreground)' }}>{insight.text}</span>
</div>
))}
</div>
</div>
</div>
</div>
</div>
);
}

View file

@ -0,0 +1,264 @@
/**
* Produkte Verwalte Produktdaten, Affiliate-Links und Content-Potenziale
* Referenz: docs/design-references/screen-produkte.png
*/
import { useState } from 'react';
import { useLocation } from 'wouter';
import { Search, Filter, ChevronLeft, ChevronRight, Link, Plus } from 'lucide-react';
import { toast } from 'sonner';
import { useCatalog } from '@/hooks/useCatalog';
const FILTER_TABS = ['Alle', 'Smart Home', 'Kitchen', 'Energie', 'News Tie-in', 'High Potential', 'Verfügbar'];
const MOCK_PRODUCTS = [
{ id: '1', name: 'Shelly Plug S Gen3', sku: 'SPS-3GEN3', category: 'Smart Home', price: '€24,95', retailer: 'Amazon', available: true, score: 92, scoreTrend: 18, affiliate: true, image: 'https://images.unsplash.com/photo-1558618666-fcd25c85cd64?w=60&h=60&fit=crop' },
{ id: '2', name: 'Shelly Plus 1', sku: 'SHPL-1', category: 'Smart Home', price: '€19,95', retailer: 'Alternate', available: true, score: 88, scoreTrend: 12, affiliate: true, image: 'https://images.unsplash.com/photo-1558618047-3c8c76ca7d13?w=60&h=60&fit=crop' },
{ id: '3', name: 'Shelly Wall Display', sku: 'SWD-1', category: 'Smart Home', price: '€89,00', retailer: 'Amazon', available: true, score: 85, scoreTrend: 10, affiliate: true, image: 'https://images.unsplash.com/photo-1558618666-fcd25c85cd64?w=60&h=60&fit=crop' },
{ id: '4', name: 'Airfryer Pro X', sku: 'AF-PRO-X', category: 'Kitchen', price: '€129,00', retailer: 'MediaMarkt', available: true, score: 90, scoreTrend: 22, affiliate: true, image: 'https://images.unsplash.com/photo-1556909114-f6e7ad7d3136?w=60&h=60&fit=crop' },
{ id: '5', name: 'Balkonkraftwerk Starter', sku: 'BK-START-800', category: 'Energie', price: '€499,00', retailer: 'ManoMano', available: true, score: 87, scoreTrend: 16, affiliate: true, image: 'https://images.unsplash.com/photo-1509391366360-2e959784a276?w=60&h=60&fit=crop' },
];
const TOP_PRODUCTS = [
{ rank: 1, name: 'Shelly Plug S Gen3', category: 'Smart Home', score: 92, clicks: 1246, trend: 18 },
{ rank: 2, name: 'Airfryer Pro X', category: 'Kitchen', score: 90, clicks: 1102, trend: 22 },
{ rank: 3, name: 'Shelly Plus 1', category: 'Smart Home', score: 88, clicks: 934, trend: 12 },
{ rank: 4, name: 'Balkonkraftwerk Starter', category: 'Energie', score: 87, clicks: 887, trend: 16 },
{ rank: 5, name: 'Shelly Wall Display', category: 'Smart Home', score: 85, clicks: 731, trend: 10 },
];
function ScoreBadge({ score }: { score: number }) {
const color = score >= 90 ? '#10b981' : score >= 80 ? '#00d4ff' : '#f59e0b';
return (
<div
className="flex items-center justify-center rounded-full font-bold"
style={{ width: '40px', height: '40px', background: `${color}20`, border: `2px solid ${color}`, color, fontFamily: 'var(--font-mono)', fontSize: '0.875rem' }}
>
{score}
</div>
);
}
export default function Products() {
const [, navigate] = useLocation();
const [activeFilter, setActiveFilter] = useState('Alle');
const [search, setSearch] = useState('');
const { products: catalogProducts } = useCatalog();
const products = catalogProducts.length > 0
? catalogProducts.slice(0, 5).map((p, i) => ({
...MOCK_PRODUCTS[i % MOCK_PRODUCTS.length],
id: p.id,
name: p.name,
price: `${p.price?.toFixed(2) || '0,00'}`,
}))
: MOCK_PRODUCTS;
const copyAffiliateLink = (product: typeof MOCK_PRODUCTS[0]) => {
const url = `https://shelly.shop/${product.sku.toLowerCase()}?utm_source=atlassocial&utm_medium=social&utm_campaign=${product.sku.toLowerCase()}`;
navigator.clipboard.writeText(url);
toast.success(`Affiliate-Link für ${product.name} kopiert`);
};
return (
<div className="p-5 animate-fade-in-up space-y-4">
{/* Header */}
<div className="flex items-center justify-between">
<div>
<h1 className="text-xl font-bold" style={{ fontFamily: 'var(--font-display)', color: 'var(--foreground)' }}>Produkte</h1>
<p className="text-sm mt-0.5" style={{ color: 'var(--muted-foreground)' }}>Verwalte Produktdaten, Affiliate-Links und Content-Potenziale.</p>
</div>
<div className="flex items-center gap-2">
<span className="text-xs px-2 py-1 rounded" style={{ background: 'var(--atlas-surface)', border: '1px solid var(--atlas-border)', color: 'var(--muted-foreground)' }}>7 Tage</span>
</div>
</div>
{/* KPI Bar */}
<div className="grid grid-cols-4 gap-3">
{[
{ label: 'Produkte aktiv', value: '156', trend: '↑ 12% vs. letzte Woche', color: '#60a5fa' },
{ label: 'Mit Affiliate-Link', value: '128', trend: '↑ 18% vs. letzte Woche', color: '#a78bfa' },
{ label: 'Top Performer (Score)', value: 'Shelly Plug S Gen3', sub: 'Score 92', color: '#f59e0b' },
{ label: 'Ø Conversion', value: '3,24%', trend: '↑ 0,6% 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="font-bold" style={{ fontFamily: 'var(--font-mono)', color: kpi.color, fontSize: i === 2 ? '0.875rem' : '1.25rem' }}>{kpi.value}</div>
<div className="text-xs mt-1" style={{ color: 'var(--muted-foreground)' }}>{kpi.trend || kpi.sub}</div>
</div>
))}
</div>
{/* Filter Tabs */}
<div className="flex items-center gap-1.5 flex-wrap">
{FILTER_TABS.map(tab => (
<button
key={tab}
onClick={() => setActiveFilter(tab)}
className="px-3 py-1.5 rounded text-xs font-medium transition-all"
style={{
background: activeFilter === tab ? 'oklch(0.82 0.18 210 / 0.12)' : 'var(--atlas-surface)',
color: activeFilter === tab ? 'var(--atlas-cyan)' : 'var(--muted-foreground)',
border: `1px solid ${activeFilter === tab ? 'oklch(0.82 0.18 210 / 0.3)' : 'var(--atlas-border)'}`,
}}
>
{tab}
</button>
))}
<div className="ml-auto flex items-center gap-2">
<div className="relative">
<Search size={12} className="absolute left-2 top-1/2 -translate-y-1/2" style={{ color: 'var(--muted-foreground)' }} />
<input
className="atlas-input pl-7 text-xs"
placeholder="Produkte suchen..."
value={search}
onChange={e => setSearch(e.target.value)}
style={{ width: '180px' }}
/>
</div>
<button className="atlas-btn-ghost p-1.5"><Filter size={13} /></button>
</div>
</div>
<div className="flex gap-4">
{/* Product Table */}
<div className="flex-1 min-w-0">
<div className="atlas-card overflow-hidden">
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
<thead>
<tr style={{ borderBottom: '1px solid var(--atlas-border)' }}>
{['Produkt', 'Kategorie', 'Preis', 'Händler', 'Verfügbarkeit', 'Content Score ⓘ', 'Affiliate', 'Aktionen'].map(h => (
<th key={h} style={{ padding: '10px 12px', textAlign: 'left' }}>
<span className="text-xs" style={{ color: 'var(--muted-foreground)' }}>{h}</span>
</th>
))}
</tr>
</thead>
<tbody>
{products.map((product, i) => (
<tr key={product.id} style={{ borderBottom: i < products.length - 1 ? '1px solid var(--atlas-border)' : 'none' }}>
<td style={{ padding: '10px 12px' }}>
<div className="flex items-center gap-2">
<img src={product.image} alt={product.name} className="rounded" style={{ width: '40px', height: '40px', objectFit: 'cover' }} onError={e => { (e.target as HTMLImageElement).style.display = 'none'; }} />
<div>
<div className="text-xs font-medium" style={{ color: 'var(--foreground)', fontFamily: 'var(--font-display)' }}>{product.name}</div>
<div className="text-xs" style={{ color: 'var(--muted-foreground)', fontFamily: 'var(--font-mono)', fontSize: '0.6rem' }}>{product.sku}</div>
</div>
</div>
</td>
<td style={{ padding: '10px 12px' }}>
<span className="text-xs px-2 py-0.5 rounded" style={{ background: 'oklch(0.82 0.18 210 / 0.08)', color: 'var(--atlas-cyan)', border: '1px solid oklch(0.82 0.18 210 / 0.2)', fontSize: '0.6rem' }}>
{product.category}
</span>
</td>
<td style={{ padding: '10px 12px' }}>
<span className="text-xs font-bold" style={{ fontFamily: 'var(--font-mono)', color: 'var(--foreground)' }}>{product.price}</span>
</td>
<td style={{ padding: '10px 12px' }}>
<span className="text-xs" style={{ color: 'var(--muted-foreground)' }}>{product.retailer}</span>
</td>
<td style={{ padding: '10px 12px' }}>
<div className="flex items-center gap-1">
<div className="atlas-status-dot online" />
<span className="text-xs" style={{ color: 'var(--atlas-green)' }}>Verfügbar</span>
</div>
</td>
<td style={{ padding: '10px 12px' }}>
<div className="flex items-center gap-2">
<ScoreBadge score={product.score} />
<div>
<div className="text-xs" style={{ color: 'var(--atlas-green)', fontFamily: 'var(--font-mono)' }}> +{product.scoreTrend}%</div>
</div>
</div>
</td>
<td style={{ padding: '10px 12px' }}>
<div className="flex items-center gap-1">
<Link size={11} style={{ color: 'var(--atlas-cyan)' }} />
<span className="text-xs" style={{ color: 'var(--atlas-cyan)' }}>Aktiv</span>
</div>
</td>
<td style={{ padding: '10px 12px' }}>
<div className="flex flex-col gap-1">
<button
onClick={() => navigate('/drafts')}
className="text-xs px-2 py-1 rounded whitespace-nowrap"
style={{ background: 'var(--atlas-cyan)', color: 'var(--atlas-navy)', fontWeight: 600, fontSize: '0.6rem' }}
>
Als Draft nutzen
</button>
<button
onClick={() => copyAffiliateLink(product)}
className="text-xs px-2 py-1 rounded whitespace-nowrap"
style={{ background: 'var(--atlas-navy)', border: '1px solid var(--atlas-border)', color: 'var(--muted-foreground)', fontSize: '0.6rem' }}
>
Link kopieren
</button>
</div>
</td>
</tr>
))}
</tbody>
</table>
{/* Pagination */}
<div className="flex items-center justify-between px-3 py-2" style={{ borderTop: '1px solid var(--atlas-border)' }}>
<span className="text-xs" style={{ color: 'var(--muted-foreground)' }}>15 von 156 Produkten</span>
<div className="flex items-center gap-1">
<button className="atlas-btn-ghost p-1"><ChevronLeft size={12} /></button>
{[1,2,3,4,5].map(p => (
<button key={p} className="w-6 h-6 text-xs rounded" style={{ background: p === 1 ? 'oklch(0.82 0.18 210 / 0.12)' : 'transparent', color: p === 1 ? 'var(--atlas-cyan)' : 'var(--muted-foreground)' }}>{p}</button>
))}
<span className="text-xs" style={{ color: 'var(--muted-foreground)' }}>... 32</span>
<button className="atlas-btn-ghost p-1"><ChevronRight size={12} /></button>
</div>
<select className="text-xs px-2 py-1 rounded" style={{ background: 'var(--atlas-surface)', border: '1px solid var(--atlas-border)', color: 'var(--foreground)' }}>
<option>5 pro Seite</option>
<option>10 pro Seite</option>
</select>
</div>
</div>
</div>
{/* Right Sidebar */}
<div className="w-56 flex-shrink-0 space-y-3">
<div className="atlas-card p-3">
<div className="text-xs font-semibold mb-2" style={{ fontFamily: 'var(--font-display)', color: 'var(--foreground)' }}>Top Produkte für Content</div>
<div className="flex gap-2 mb-2">
<button className="text-xs px-2 py-1 rounded" style={{ background: 'oklch(0.82 0.18 210 / 0.12)', color: 'var(--atlas-cyan)', border: '1px solid oklch(0.82 0.18 210 / 0.3)' }}>Nach Score</button>
<button className="text-xs px-2 py-1 rounded" style={{ background: 'transparent', color: 'var(--muted-foreground)', border: '1px solid var(--atlas-border)' }}>Nach erw. Klicks</button>
</div>
<div className="space-y-2">
{TOP_PRODUCTS.map(p => (
<div key={p.rank} className="flex items-center gap-2">
<span className="text-xs font-bold" style={{ color: 'var(--muted-foreground)', fontFamily: 'var(--font-mono)', width: '16px' }}>({p.rank})</span>
<div className="flex-1 min-w-0">
<div className="text-xs truncate" style={{ color: 'var(--foreground)' }}>{p.name}</div>
<div className="text-xs" style={{ color: 'var(--muted-foreground)', fontSize: '0.6rem' }}>{p.category} · Erw. Klicks {p.clicks.toLocaleString('de-DE')}</div>
</div>
<div className="text-right">
<div className="text-xs font-bold" style={{ fontFamily: 'var(--font-mono)', color: 'var(--atlas-cyan)' }}>{p.score}</div>
<div className="text-xs" style={{ color: 'var(--atlas-green)', fontSize: '0.6rem' }}>{p.trend}%</div>
</div>
</div>
))}
</div>
</div>
<div className="atlas-card p-3">
<div className="text-xs font-semibold mb-2" style={{ fontFamily: 'var(--font-display)', color: 'var(--foreground)' }}>Insights</div>
<div className="space-y-2">
{[
'Smart Home Produkte erzielen aktuell die höchste Engagement-Rate (+14%).',
'Fokus auf Shelly Plug S Gen3 beibehalten.',
].map((t, i) => (
<div key={i} className="flex gap-2">
<div className="w-1.5 h-1.5 rounded-full mt-1.5 flex-shrink-0" style={{ background: 'var(--atlas-cyan)' }} />
<p className="text-xs" style={{ color: 'var(--muted-foreground)', lineHeight: '1.5' }}>{t}</p>
</div>
))}
</div>
<button className="text-xs mt-2" style={{ color: 'var(--atlas-cyan)' }}>Weitere Insights anzeigen </button>
</div>
</div>
</div>
</div>
);
}

View file

@ -0,0 +1,314 @@
/**
* Einstellungen Verwalte Kanäle, Integrationen, Branding und Systemregeln
* Referenz: docs/design-references/screen-einstellungen.png
*/
import { useState } from 'react';
import { RefreshCw, Edit, MoreVertical, Plus, ExternalLink } from 'lucide-react';
import { toast } from 'sonner';
const SETTING_TABS = ['Allgemein', 'Kanäle', 'Integrationen', 'Branding', 'Affiliate', 'Freigaben', 'Automationen', 'Team'];
const CHANNELS = [
{ name: 'TikTok', handle: '@atlas_social', status: 'Verbunden', color: '#00d4ff' },
{ name: 'Instagram', handle: '@atlas.social', status: 'Verbunden', color: '#e1306c' },
{ name: 'LinkedIn', handle: 'ATLAS Social', status: 'Verbunden', color: '#0077b5' },
{ name: 'X (Twitter)', handle: '@ATLAS_social', status: 'Verbunden', color: '#94a3b8' },
{ name: 'YouTube Shorts', handle: 'ATLAS Social', status: 'Verbunden', color: '#ff0000' },
];
const INTEGRATIONS = [
{ name: 'Postiz API', id: 'API Key: ••••••••••••••8f3a', status: 'Aktiv', color: '#10b981' },
{ name: 'n8n Workflow', id: 'Workflow-ID: atlas-pipeline-v2\nLetzter Lauf: 26.05.2025, 09:35', status: 'Aktiv', color: '#10b981', action: 'Ausführen' },
{ name: 'Webhook Listener', id: 'Endpoint: /webhook/atlas\nStatus: Fehlerfrei', status: 'Aktiv', color: '#10b981', action: 'Logs anzeigen' },
];
const UTM_RULES = [
{ label: 'Quelle (utm_source)', value: 'atlas_social' },
{ label: 'Medium (utm_medium)', value: 'social' },
{ label: 'Kampagne (utm_campaign)', value: '{{campaign_name}}' },
{ label: 'Content (utm_content)', value: '{{content_type}}-{{platform}}' },
{ label: 'Term (utm_term)', value: '{{product_name}}' },
];
const TEAM_ROLES = [
{ role: 'Administratoren', count: 4 },
{ role: 'Editoren', count: 9 },
{ role: 'Reviewer', count: 6 },
{ role: 'Freigeber', count: 3 },
{ role: 'Beobachter', count: 12 },
];
const HEALTH_ITEMS = [
{ label: 'Kanal-Sync', detail: 'Alle Kanäle synchron\nLetzter Sync: 09:38', status: 'ok' },
{ label: 'API-Verbindungen', detail: 'Alle APIs aktiv\nFehlerfrei', status: 'ok' },
{ label: 'Datenbank', detail: 'Normal\nLatenz: 12ms', status: 'ok' },
{ label: 'Speicher', detail: '42% verwendet\n1,2 GB / 2,9 GB', status: 'warn' },
{ label: 'Automationen', detail: '14 aktiv\n2 pausiert', status: 'ok' },
];
const CHANNEL_ICONS: Record<string, string> = { TikTok: '🎵', Instagram: '📸', LinkedIn: '💼', 'X (Twitter)': '✗', 'YouTube Shorts': '▶' };
export default function Settings() {
const [activeTab, setActiveTab] = useState('Allgemein');
const [utmRules, setUtmRules] = useState(UTM_RULES);
return (
<div className="p-5 animate-fade-in-up space-y-4">
{/* Header */}
<div className="flex items-center justify-between">
<div>
<h1 className="text-xl font-bold" style={{ fontFamily: 'var(--font-display)', color: 'var(--foreground)' }}>Einstellungen</h1>
<p className="text-sm mt-0.5" style={{ color: 'var(--muted-foreground)' }}>Verwalte Kanäle, Integrationen, Branding und Systemregeln.</p>
</div>
<div className="flex items-center gap-2">
<span className="text-xs" style={{ color: 'var(--muted-foreground)', fontFamily: 'var(--font-mono)' }}>26. Mai 2025, 09:41</span>
<button className="atlas-btn-ghost p-1.5"><RefreshCw size={13} /></button>
</div>
</div>
{/* KPI Bar */}
<div className="grid grid-cols-4 gap-3">
{[
{ label: 'Verbundene Kanäle', value: '5 / 5', sub: 'Alle Systeme verbunden', color: '#10b981' },
{ label: 'Integrationen aktiv', value: '6 / 8', sub: '75% aktiv', color: '#a78bfa' },
{ label: 'Branding-Assets', value: '24', sub: 'Aktiv & aktuell', color: '#60a5fa' },
{ label: 'Automationsregeln', value: '18', sub: '14 aktiv', color: '#f59e0b' },
].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.sub}</div>
</div>
))}
</div>
{/* Tabs */}
<div className="flex gap-1 overflow-x-auto" style={{ borderBottom: '1px solid var(--atlas-border)', paddingBottom: '0' }}>
{SETTING_TABS.map(tab => (
<button
key={tab}
onClick={() => setActiveTab(tab)}
className="px-3 py-2 text-xs whitespace-nowrap transition-all"
style={{
color: activeTab === tab ? 'var(--atlas-cyan)' : 'var(--muted-foreground)',
borderBottom: activeTab === tab ? '2px solid var(--atlas-cyan)' : '2px solid transparent',
fontFamily: 'var(--font-body)',
}}
>
{tab}
</button>
))}
</div>
{/* Content Grid */}
<div className="grid grid-cols-3 gap-4">
{/* Verbundene Kanäle */}
<div className="atlas-card p-4">
<div className="flex items-center justify-between mb-3">
<h2 className="text-sm font-semibold" style={{ fontFamily: 'var(--font-display)', color: 'var(--foreground)' }}>Verbundene Kanäle</h2>
</div>
<div className="space-y-2">
{CHANNELS.map(ch => (
<div key={ch.name} className="flex items-center gap-2 p-2 rounded" style={{ background: 'var(--atlas-navy)' }}>
<span style={{ fontSize: '16px' }}>{CHANNEL_ICONS[ch.name]}</span>
<div className="flex-1 min-w-0">
<div className="text-xs font-medium" style={{ color: 'var(--foreground)' }}>{ch.name}</div>
<div className="text-xs" style={{ color: 'var(--muted-foreground)', fontSize: '0.6rem' }}>{ch.handle}</div>
</div>
<div className="flex items-center gap-1">
<div className="atlas-status-dot online" />
<span className="text-xs" style={{ color: 'var(--atlas-green)', fontSize: '0.6rem' }}>{ch.status}</span>
</div>
<button className="text-xs px-2 py-0.5 rounded" style={{ background: 'var(--atlas-surface)', border: '1px solid var(--atlas-border)', color: 'var(--muted-foreground)', fontSize: '0.6rem' }}>
Verwalten
</button>
<button style={{ color: 'var(--muted-foreground)' }}><MoreVertical size={12} /></button>
</div>
))}
</div>
<button className="text-xs mt-2 flex items-center gap-1" style={{ color: 'var(--atlas-cyan)' }}>
<Plus size={11} /> Kanal verbinden
</button>
</div>
{/* API & Automatisierung */}
<div className="atlas-card p-4">
<div className="flex items-center justify-between mb-3">
<h2 className="text-sm font-semibold" style={{ fontFamily: 'var(--font-display)', color: 'var(--foreground)' }}>API & Automatisierung</h2>
</div>
<div className="space-y-2">
{INTEGRATIONS.map((int, i) => (
<div key={i} className="p-2 rounded" style={{ background: 'var(--atlas-navy)', border: '1px solid var(--atlas-border)' }}>
<div className="flex items-center justify-between mb-1">
<span className="text-xs font-medium" style={{ color: 'var(--foreground)' }}>{int.name}</span>
<div className="flex items-center gap-1.5">
<span className="text-xs px-1.5 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)', fontSize: '0.6rem' }}>
{int.status}
</span>
<button style={{ color: 'var(--muted-foreground)' }}><MoreVertical size={11} /></button>
</div>
</div>
<div className="text-xs whitespace-pre-line" style={{ color: 'var(--muted-foreground)', fontSize: '0.6rem' }}>{int.id}</div>
{int.action && (
<button
className="text-xs mt-1.5 px-2 py-1 rounded"
style={{ background: 'var(--atlas-surface)', border: '1px solid var(--atlas-border)', color: 'var(--foreground)', fontSize: '0.6rem' }}
onClick={() => toast.success(`${int.action} ausgeführt`)}
>
{int.action}
</button>
)}
</div>
))}
</div>
<button className="text-xs mt-2 flex items-center gap-1" style={{ color: 'var(--atlas-cyan)' }}>
<Plus size={11} /> Integration hinzufügen
</button>
</div>
{/* UTM Standardregeln */}
<div className="atlas-card p-4">
<div className="flex items-center justify-between mb-3">
<h2 className="text-sm font-semibold" style={{ fontFamily: 'var(--font-display)', color: 'var(--foreground)' }}>UTM Standardregeln</h2>
<button className="text-xs" style={{ color: 'var(--atlas-cyan)' }}>Bearbeiten</button>
</div>
<div className="space-y-2">
{utmRules.map((rule, i) => (
<div key={i}>
<div className="text-xs mb-0.5" style={{ color: 'var(--muted-foreground)', fontSize: '0.6rem' }}>{rule.label}</div>
<input
className="atlas-input text-xs"
value={rule.value}
onChange={e => setUtmRules(prev => prev.map((r, ri) => ri === i ? { ...r, value: e.target.value } : r))}
/>
</div>
))}
</div>
<button className="text-xs mt-2" style={{ color: 'var(--atlas-cyan)' }}>Vorschau & Test </button>
</div>
{/* Branding & Assets */}
<div className="atlas-card p-4">
<div className="flex items-center justify-between mb-3">
<h2 className="text-sm font-semibold" style={{ fontFamily: 'var(--font-display)', color: 'var(--foreground)' }}>Branding & Assets</h2>
<button className="text-xs" style={{ color: 'var(--atlas-cyan)' }}>Bearbeiten</button>
</div>
<div className="grid grid-cols-3 gap-3 mb-3">
<div>
<div className="text-xs mb-1" style={{ color: 'var(--muted-foreground)', fontSize: '0.6rem' }}>Logo (Standard)</div>
<div className="flex items-center justify-center rounded p-2" style={{ background: 'var(--atlas-navy)', border: '1px solid var(--atlas-border)', height: '60px' }}>
<div className="text-2xl">🔺</div>
</div>
<button className="text-xs mt-1 w-full text-center" style={{ color: 'var(--atlas-cyan)' }}>Aktualisieren</button>
</div>
<div>
<div className="text-xs mb-1" style={{ color: 'var(--muted-foreground)', fontSize: '0.6rem' }}>Favicon</div>
<div className="flex items-center justify-center rounded p-2" style={{ background: 'var(--atlas-navy)', border: '1px solid var(--atlas-border)', height: '60px' }}>
<div className="text-lg">🔺</div>
</div>
<button className="text-xs mt-1 w-full text-center" style={{ color: 'var(--atlas-cyan)' }}>Aktualisieren</button>
</div>
<div>
<div className="text-xs mb-1" style={{ color: 'var(--muted-foreground)', fontSize: '0.6rem' }}>Primärfarbe</div>
<div className="flex items-center gap-1.5 p-2 rounded" style={{ background: 'var(--atlas-navy)', border: '1px solid var(--atlas-border)', height: '60px' }}>
<div className="w-6 h-6 rounded" style={{ background: '#00A3FF' }} />
<span className="text-xs font-mono" style={{ color: 'var(--foreground)' }}>#00A3FF</span>
</div>
</div>
</div>
<button className="text-xs" style={{ color: 'var(--atlas-cyan)' }}>Alle Branding-Assets verwalten </button>
</div>
{/* Freigabe-Workflow */}
<div className="atlas-card p-4">
<div className="flex items-center justify-between mb-3">
<h2 className="text-sm font-semibold" style={{ fontFamily: 'var(--font-display)', color: 'var(--foreground)' }}>Freigabe-Workflow</h2>
<button className="text-xs" style={{ color: 'var(--atlas-cyan)' }}>Bearbeiten</button>
</div>
<div className="space-y-2">
<div>
<div className="text-xs mb-1" style={{ color: 'var(--muted-foreground)', fontSize: '0.6rem' }}>Workflow-Typ</div>
<select className="atlas-input text-xs">
<option>Mehrstufige Freigabe</option>
<option>Einfache Freigabe</option>
</select>
</div>
<div>
<div className="text-xs mb-1" style={{ color: 'var(--muted-foreground)', fontSize: '0.6rem' }}>Schritte</div>
<div className="flex items-center gap-1 p-2 rounded" style={{ background: 'var(--atlas-navy)', border: '1px solid var(--atlas-border)' }}>
<span className="text-xs">😊 Ersteller</span>
<span style={{ color: 'var(--atlas-border)' }}></span>
<span className="text-xs">👤 Reviewer</span>
<span style={{ color: 'var(--atlas-border)' }}></span>
<span className="text-xs"> Freigeber</span>
</div>
</div>
<div>
<div className="text-xs mb-1" style={{ color: 'var(--muted-foreground)', fontSize: '0.6rem' }}>Erforderliche Freigaben</div>
<select className="atlas-input text-xs">
<option>Alle Schritte erforderlich</option>
</select>
</div>
<div>
<div className="text-xs mb-1" style={{ color: 'var(--muted-foreground)', fontSize: '0.6rem' }}>Frist für Freigabe</div>
<select className="atlas-input text-xs">
<option>3 Tage</option>
<option>1 Tag</option>
<option>7 Tage</option>
</select>
</div>
</div>
<button className="text-xs mt-2" style={{ color: 'var(--atlas-cyan)' }}>Workflow-Vorlagen verwalten </button>
</div>
{/* Team & Rollen */}
<div className="atlas-card p-4">
<div className="flex items-center justify-between mb-3">
<h2 className="text-sm font-semibold" style={{ fontFamily: 'var(--font-display)', color: 'var(--foreground)' }}>Team & Rollen</h2>
<button className="text-xs" style={{ color: 'var(--atlas-cyan)' }}>Verwalten</button>
</div>
<div className="space-y-2">
{TEAM_ROLES.map(r => (
<div key={r.role} className="flex items-center gap-2">
<div className="w-6 h-6 rounded-full flex items-center justify-center text-xs" style={{ background: 'var(--atlas-navy)', border: '1px solid var(--atlas-border)' }}>
👤
</div>
<span className="text-xs flex-1" style={{ color: 'var(--foreground)' }}>{r.role}</span>
<span className="text-xs" style={{ color: 'var(--muted-foreground)', fontFamily: 'var(--font-mono)' }}>{r.count} Mitglieder</span>
<button style={{ color: 'var(--muted-foreground)' }}><MoreVertical size={11} /></button>
</div>
))}
</div>
<button className="text-xs mt-2" style={{ color: 'var(--atlas-cyan)' }}>Rollen & Berechtigungen verwalten </button>
</div>
</div>
{/* System Health */}
<div className="atlas-card p-4">
<div className="flex items-center justify-between mb-3">
<h2 className="text-sm font-semibold" style={{ fontFamily: 'var(--font-display)', color: 'var(--foreground)' }}>System-Health & Sync-Status</h2>
<button className="atlas-btn-primary text-xs">System-Logs · Alle Logs anzeigen</button>
</div>
<div className="grid grid-cols-5 gap-3">
{HEALTH_ITEMS.map(item => (
<div key={item.label} className="p-3 rounded" style={{ background: 'var(--atlas-navy)', border: '1px solid var(--atlas-border)' }}>
<div className="flex items-center gap-1.5 mb-1">
<span style={{ fontSize: '14px' }}>{item.status === 'ok' ? '✅' : '⚠️'}</span>
<span className="text-xs font-medium" style={{ color: 'var(--foreground)' }}>{item.label}</span>
</div>
<div className="text-xs whitespace-pre-line" style={{ color: item.status === 'ok' ? 'var(--atlas-green)' : 'var(--atlas-amber)', fontSize: '0.6rem' }}>
{item.detail}
</div>
</div>
))}
</div>
<div className="flex items-center justify-between mt-3 pt-3" style={{ borderTop: '1px solid var(--atlas-border)' }}>
<span className="text-xs" style={{ color: 'var(--muted-foreground)' }}>Einstellungen-Version: v2.8.1</span>
<span className="text-xs" style={{ color: 'var(--muted-foreground)' }}>Letzte Änderung: 26.05.2025, 09:21 von Anna K.</span>
<button className="text-xs" style={{ color: 'var(--atlas-cyan)' }}>Änderungshistorie anzeigen </button>
</div>
</div>
</div>
);
}

View file

@ -0,0 +1,367 @@
/**
* Signale Hochpotenzielle Themen & Chancen aus allen ATLAS-Systemen
* Referenz: docs/design-references/screen-signale.png
*/
import { useState } from 'react';
import { useLocation } from 'wouter';
import { Star, TrendingUp, RefreshCw, ChevronLeft, ChevronRight } from 'lucide-react';
const FILTER_TABS = ['Alle', 'Smart Home', 'Kitchen', 'News', 'Radar', 'Hohes Potenzial'];
const SIGNALS = [
{
id: 1,
category: 'SMART HOME',
categoryColor: '#00d4ff',
title: 'Shelly Plug S Gen3 im Fokus',
description: 'Die neue Gen3 Serie sorgt für hohe Nachfrage und starke Social Buzz. Perfekt für How-to-Inhalte & Affiliate-Deals.',
score: 92,
scoreTrend: 18,
platform: 'Instagram',
platformIcon: '📸',
image: 'https://images.unsplash.com/photo-1558618666-fcd25c85cd64?w=120&h=120&fit=crop',
starred: false,
},
{
id: 2,
category: 'KITCHEN',
categoryColor: '#a78bfa',
title: 'Airfryer Trends 2025',
description: 'Airfryer Rezepte und Vergleiche erzielen Rekord-Reichweiten. Guter Zeitpunkt für Tests, Rezepte & Kaufberatung.',
score: 90,
scoreTrend: 22,
platform: 'YouTube Shorts',
platformIcon: '▶',
image: 'https://images.unsplash.com/photo-1556909114-f6e7ad7d3136?w=120&h=120&fit=crop',
starred: false,
},
{
id: 3,
category: 'NEWS',
categoryColor: '#60a5fa',
title: 'KI-News Update',
description: 'OpenAI & Google I/O Highlights erzeugen hohe Engagement-Spitzen. Erkläre, ordne ein, profitiere.',
score: 87,
scoreTrend: 16,
platform: 'LinkedIn',
platformIcon: '💼',
image: 'https://images.unsplash.com/photo-1677442135703-1787eea5ce01?w=120&h=120&fit=crop',
starred: false,
},
{
id: 4,
category: 'RADAR',
categoryColor: '#34d399',
title: 'Balkonkraftwerk im Aufwind',
description: 'Steigende Nachfrage nach einfachen PV-Lösungen. Ideal für Ratgeber, Vergleiche & Produkteinführungen.',
score: 85,
scoreTrend: 21,
platform: 'Instagram',
platformIcon: '📸',
image: 'https://images.unsplash.com/photo-1509391366360-2e959784a276?w=120&h=120&fit=crop',
starred: false,
},
{
id: 5,
category: 'SMART HOME',
categoryColor: '#00d4ff',
title: 'Smart Metering erklärt',
description: 'Smart Meter Rollout & Vorteile sind stark im Gespräch. Erkläre Nutzen, Kosten & Installation.',
score: 82,
scoreTrend: 14,
platform: 'YouTube',
platformIcon: '▶',
image: 'https://images.unsplash.com/photo-1558618047-3c8c76ca7d13?w=120&h=120&fit=crop',
starred: false,
},
{
id: 6,
category: 'RADAR',
categoryColor: '#34d399',
title: 'Sommer-Energiesparen',
description: 'Hohe Relevanz für Tipps zu Kühlen, Lüften & Strom sparen. Saisonales Thema mit starker Suchintention.',
score: 78,
scoreTrend: 12,
platform: 'TikTok',
platformIcon: '🎵',
image: 'https://images.unsplash.com/photo-1506905925346-21bda4d32df4?w=120&h=120&fit=crop',
starred: false,
},
];
const TOP_CHANCES = [
{ rank: 1, title: 'Shelly Plug S Gen3 im Fokus', category: 'Smart Home', score: 92, trend: 18 },
{ rank: 2, title: 'Airfryer Trends 2025', category: 'Kitchen', score: 90, trend: 22 },
{ rank: 3, title: 'KI-News Update', category: 'News', score: 87, trend: 16 },
{ rank: 4, title: 'Balkonkraftwerk im Aufwind', category: 'Radar', score: 85, trend: 21 },
{ rank: 5, title: 'Smart Metering erklärt', category: 'Smart Home', score: 82, trend: 14 },
];
export default function Signals() {
const [, navigate] = useLocation();
const [activeFilter, setActiveFilter] = useState('Alle');
const [signals, setSignals] = useState(SIGNALS);
const [page, setPage] = useState(1);
const toggleStar = (id: number) => {
setSignals(prev => prev.map(s => s.id === id ? { ...s, starred: !s.starred } : s));
};
const filtered = activeFilter === 'Alle'
? signals
: signals.filter(s => s.category.toLowerCase().includes(activeFilter.toLowerCase()));
return (
<div className="p-5 animate-fade-in-up">
{/* Header */}
<div className="flex items-center justify-between mb-4">
<div>
<h1 className="text-xl font-bold" style={{ fontFamily: 'var(--font-display)', color: 'var(--foreground)' }}>
Signale
</h1>
<p className="text-sm mt-0.5" style={{ color: 'var(--muted-foreground)' }}>
Entdecke hochpotenzielle Themen und Chancen aus allen ATLAS-Systemen.
</p>
</div>
<div className="flex items-center gap-2">
<span className="text-xs" style={{ color: 'var(--muted-foreground)', fontFamily: 'var(--font-mono)' }}>
Zuletzt aktualisiert {new Date().toLocaleDateString('de-DE', { day: 'numeric', month: 'long', year: 'numeric' })}, {new Date().toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' })}
</span>
<button className="atlas-btn-ghost p-1.5">
<RefreshCw size={13} />
</button>
</div>
</div>
{/* Filter Tabs */}
<div className="flex items-center gap-1.5 mb-4 flex-wrap">
{FILTER_TABS.map(tab => (
<button
key={tab}
onClick={() => setActiveFilter(tab)}
className="px-3 py-1.5 rounded text-xs font-medium transition-all"
style={{
background: activeFilter === tab ? 'oklch(0.82 0.18 210 / 0.12)' : 'var(--atlas-surface)',
color: activeFilter === tab ? 'var(--atlas-cyan)' : 'var(--muted-foreground)',
border: `1px solid ${activeFilter === tab ? 'oklch(0.82 0.18 210 / 0.3)' : 'var(--atlas-border)'}`,
fontFamily: 'var(--font-body)',
}}
>
{tab}
</button>
))}
<div className="ml-auto">
<select
className="text-xs px-2 py-1.5 rounded"
style={{ background: 'var(--atlas-surface)', border: '1px solid var(--atlas-border)', color: 'var(--foreground)' }}
>
<option>Diese Woche</option>
<option>Dieser Monat</option>
</select>
</div>
</div>
<div className="flex gap-4">
{/* Signal Cards Grid */}
<div className="flex-1 min-w-0">
<div className="grid grid-cols-2 gap-3">
{filtered.map((signal, i) => (
<div
key={signal.id}
className="atlas-card atlas-card-hover p-4 cursor-pointer"
style={{ animationDelay: `${i * 50}ms` }}
>
{/* Category + Star */}
<div className="flex items-center justify-between mb-3">
<span
className="text-xs font-bold"
style={{ color: signal.categoryColor, fontFamily: 'var(--font-mono)', fontSize: '0.6rem', letterSpacing: '0.08em' }}
>
{signal.category}
</span>
<button onClick={() => toggleStar(signal.id)}>
<Star
size={14}
style={{ color: signal.starred ? '#f59e0b' : 'var(--muted-foreground)', fill: signal.starred ? '#f59e0b' : 'none' }}
/>
</button>
</div>
{/* Content */}
<div className="flex gap-3 mb-3">
<img
src={signal.image}
alt={signal.title}
className="rounded flex-shrink-0"
style={{ width: '56px', height: '56px', objectFit: 'cover' }}
onError={e => { (e.target as HTMLImageElement).style.display = 'none'; }}
/>
<div className="flex-1 min-w-0">
<div className="font-semibold text-sm mb-1" style={{ color: 'var(--foreground)', fontFamily: 'var(--font-display)' }}>
{signal.title}
</div>
<div className="text-xs leading-relaxed" style={{ color: 'var(--muted-foreground)' }}>
{signal.description}
</div>
</div>
{/* Score */}
<div className="flex flex-col items-end flex-shrink-0">
<div className="text-2xl font-bold" style={{ fontFamily: 'var(--font-mono)', color: 'var(--atlas-cyan)' }}>
{signal.score}
</div>
<div className="flex items-center gap-0.5">
<TrendingUp size={10} style={{ color: 'var(--atlas-green)' }} />
<span className="text-xs" style={{ color: 'var(--atlas-green)', fontFamily: 'var(--font-mono)' }}>
+{signal.scoreTrend}%
</span>
</div>
<div className="text-xs mt-0.5" style={{ color: 'var(--muted-foreground)', fontSize: '0.6rem' }}>Trend</div>
</div>
</div>
{/* Platform */}
<div className="mb-3">
<div className="text-xs mb-1" style={{ color: 'var(--muted-foreground)', fontSize: '0.6rem' }}>Empfohlene Plattform</div>
<div className="flex items-center gap-1">
<span style={{ fontSize: '12px' }}>{signal.platformIcon}</span>
<span className="text-xs" style={{ color: 'var(--foreground)' }}>{signal.platform}</span>
</div>
</div>
{/* Actions */}
<div className="flex gap-2">
<button
onClick={() => navigate('/drafts')}
className="flex-1 text-xs py-1.5 rounded font-medium transition-all"
style={{
background: 'var(--atlas-navy)',
border: '1px solid var(--atlas-border)',
color: 'var(--foreground)',
fontFamily: 'var(--font-body)',
}}
>
Als Draft erstellen
</button>
<button
onClick={() => navigate('/campaigns')}
className="flex-1 text-xs py-1.5 rounded font-medium transition-all"
style={{
background: 'oklch(0.82 0.18 210 / 0.12)',
border: '1px solid oklch(0.82 0.18 210 / 0.3)',
color: 'var(--atlas-cyan)',
fontFamily: 'var(--font-body)',
}}
>
Zur Kampagne
</button>
</div>
</div>
))}
</div>
{/* Pagination */}
<div className="flex items-center justify-between mt-4">
<span className="text-xs" style={{ color: 'var(--muted-foreground)' }}>
16 von 42 Signalen
</span>
<div className="flex items-center gap-1">
<button className="atlas-btn-ghost p-1.5" onClick={() => setPage(p => Math.max(1, p - 1))}>
<ChevronLeft size={14} />
</button>
{[1, 2, 3, 4, 5].map(p => (
<button
key={p}
onClick={() => setPage(p)}
className="w-7 h-7 text-xs rounded transition-all"
style={{
background: page === p ? 'oklch(0.82 0.18 210 / 0.12)' : 'transparent',
color: page === p ? 'var(--atlas-cyan)' : 'var(--muted-foreground)',
border: `1px solid ${page === p ? 'oklch(0.82 0.18 210 / 0.3)' : 'transparent'}`,
}}
>
{p}
</button>
))}
<button className="atlas-btn-ghost p-1.5" onClick={() => setPage(p => p + 1)}>
<ChevronRight size={14} />
</button>
</div>
<select
className="text-xs px-2 py-1 rounded"
style={{ background: 'var(--atlas-surface)', border: '1px solid var(--atlas-border)', color: 'var(--foreground)' }}
>
<option>6 pro Seite</option>
<option>12 pro Seite</option>
</select>
</div>
</div>
{/* Right Sidebar */}
<div className="w-56 flex-shrink-0 space-y-3">
{/* Top Chancen */}
<div className="atlas-card p-3">
<div className="flex items-center gap-1.5 mb-3">
<span style={{ fontSize: '14px' }}>🏆</span>
<span className="text-xs font-semibold" style={{ fontFamily: 'var(--font-display)', color: 'var(--foreground)' }}>
Top Chancen heute
</span>
</div>
<div className="space-y-2">
{TOP_CHANCES.map(c => (
<div key={c.rank} className="flex items-center gap-2">
<div
className="flex items-center justify-center rounded-full text-xs font-bold flex-shrink-0"
style={{
width: '20px', height: '20px',
background: c.rank === 1 ? '#f59e0b20' : c.rank === 2 ? '#94a3b820' : '#64748b20',
color: c.rank === 1 ? '#f59e0b' : c.rank === 2 ? '#94a3b8' : '#64748b',
fontFamily: 'var(--font-mono)', fontSize: '0.6rem',
}}
>
{c.rank}
</div>
<div className="flex-1 min-w-0">
<div className="text-xs truncate" style={{ color: 'var(--foreground)' }}>{c.title}</div>
<div className="text-xs" style={{ color: 'var(--muted-foreground)', fontSize: '0.6rem' }}>{c.category}</div>
</div>
<div className="text-right flex-shrink-0">
<div className="text-xs font-bold" style={{ fontFamily: 'var(--font-mono)', color: 'var(--atlas-cyan)' }}>{c.score}</div>
<div className="text-xs" style={{ color: 'var(--atlas-green)', fontSize: '0.6rem' }}>{c.trend}%</div>
</div>
</div>
))}
</div>
<button className="w-full text-xs mt-3 py-1.5 rounded" style={{ color: 'var(--atlas-cyan)', border: '1px solid oklch(0.82 0.18 210 / 0.2)', background: 'transparent' }}>
Alle Signale anzeigen
</button>
</div>
{/* System Insights */}
<div className="atlas-card p-3">
<div className="text-xs font-semibold mb-2" style={{ fontFamily: 'var(--font-display)', color: 'var(--foreground)' }}>
System-Insights
</div>
<div className="space-y-2">
<div className="flex gap-2">
<div className="w-1.5 h-1.5 rounded-full mt-1.5 flex-shrink-0" style={{ background: 'var(--atlas-cyan)' }} />
<p className="text-xs" style={{ color: 'var(--muted-foreground)', lineHeight: '1.5' }}>
KI-News Update zeigt aktuell die höchste Engagement-Dynamik aller News-Signale.
</p>
</div>
<div className="flex gap-2">
<div className="w-1.5 h-1.5 rounded-full mt-1.5 flex-shrink-0" style={{ background: 'var(--atlas-green)' }} />
<p className="text-xs" style={{ color: 'var(--muted-foreground)', lineHeight: '1.5' }}>
Smart Home Signale performen 14% über Durchschnitt.
</p>
</div>
</div>
<button className="text-xs mt-2" style={{ color: 'var(--atlas-cyan)' }}>
Zum Radar wechseln
</button>
</div>
</div>
</div>
</div>
);
}

View file

@ -0,0 +1,448 @@
/**
* War Room Zentrale Übersicht & Steuerung der Content-Operationen
*
* Referenz: docs/design-references/screen-war-room.png
*
* Layout:
* - Page Header (Titel + Datum-Filter)
* - 5 KPI-Karten (Heute geplant, Entwürfe offen, Freigaben nötig, Kanäle verbunden, Affiliate-Potenzial)
* - Signal Cards (4 Karten aus ATLAS-Systemen)
* - Content Pipeline (Funnel: IdeeDraftReviewGeplantVeröffentlicht)
* - Kanal-Status (TikTok, Instagram, LinkedIn, X, YouTube)
*/
import { useState } from 'react';
import { useLocation } from 'wouter';
import {
Calendar, FileText, CheckSquare, Share2, Diamond,
TrendingUp, TrendingDown, RefreshCw, ArrowRight,
Star
} from 'lucide-react';
import { usePostizPosts, usePostizIntegrations } from '@/hooks/usePostiz';
// Sparkline mini-chart component
function Sparkline({ color, trend }: { color: string; trend: 'up' | 'down' | 'flat' }) {
const points = trend === 'up'
? '0,20 10,18 20,15 30,12 40,10 50,8 60,5 70,3'
: trend === 'down'
? '0,3 10,5 20,8 30,10 40,12 50,15 60,18 70,20'
: '0,10 10,12 20,9 30,11 40,10 50,12 60,9 70,11';
return (
<svg width="70" height="24" viewBox="0 0 70 24" fill="none">
<polyline
points={points}
stroke={color}
strokeWidth="1.5"
fill="none"
strokeLinecap="round"
strokeLinejoin="round"
opacity="0.8"
/>
</svg>
);
}
interface KpiCardProps {
icon: React.ReactNode;
iconBg: string;
title: string;
value: string;
trend: string;
trendUp: boolean | null;
trendLabel: string;
sparkColor: string;
sparkTrend: 'up' | 'down' | 'flat';
subtext?: string;
subtextColor?: string;
}
function KpiCard({ icon, iconBg, title, value, trend, trendUp, trendLabel, sparkColor, sparkTrend, subtext, subtextColor }: KpiCardProps) {
return (
<div className="atlas-kpi-card flex-1 min-w-0">
<div className="flex items-start justify-between mb-2">
<div>
<div className="text-xs mb-1" style={{ color: 'var(--muted-foreground)', fontFamily: 'var(--font-body)' }}>
{title}
</div>
<div className="text-2xl font-bold" style={{ fontFamily: 'var(--font-mono)', color: 'var(--foreground)' }}>
{value}
</div>
</div>
<div
className="flex items-center justify-center rounded-lg flex-shrink-0"
style={{ width: '36px', height: '36px', background: iconBg }}
>
{icon}
</div>
</div>
{subtext ? (
<div className="text-xs mb-2" style={{ color: subtextColor || 'var(--atlas-green)', fontFamily: 'var(--font-body)' }}>
{subtext}
</div>
) : (
<div className="flex items-center gap-1 mb-2">
{trendUp === true ? (
<TrendingUp size={11} style={{ color: 'var(--atlas-green)' }} />
) : trendUp === false ? (
<TrendingDown size={11} style={{ color: 'var(--atlas-red)' }} />
) : null}
<span
className="text-xs"
style={{
color: trendUp === true ? 'var(--atlas-green)' : trendUp === false ? 'var(--atlas-red)' : 'var(--muted-foreground)',
fontFamily: 'var(--font-body)'
}}
>
{trend} {trendLabel}
</span>
</div>
)}
<Sparkline color={sparkColor} trend={sparkTrend} />
</div>
);
}
const SIGNAL_CARDS = [
{
category: 'SMART HOME',
categoryColor: '#00d4ff',
title: 'Shelly Plus Plug S',
description: 'Neue Funktionen + Energie-Monitoring in Fokus. Hohe Nachfrage signalisiert.',
type: 'Produkt',
priority: 'Hoch',
priorityColor: '#10b981',
image: 'https://images.unsplash.com/photo-1558618666-fcd25c85cd64?w=80&h=80&fit=crop',
},
{
category: 'KITCHEN',
categoryColor: '#a78bfa',
title: 'Küchen-Trends 2025',
description: 'Meal Prep + Airfryer Content performt stark auf TikTok & Instagram.',
type: 'Trend',
priority: 'Mittel',
priorityColor: '#f59e0b',
image: 'https://images.unsplash.com/photo-1556909114-f6e7ad7d3136?w=80&h=80&fit=crop',
},
{
category: 'NEWS',
categoryColor: '#60a5fa',
title: 'KI-News Update',
description: 'OpenAI & Google I/O Highlights erzeugen hohe Engagement-Spitzen.',
type: 'News',
priority: 'Hoch',
priorityColor: '#10b981',
image: 'https://images.unsplash.com/photo-1677442135703-1787eea5ce01?w=80&h=80&fit=crop',
},
{
category: 'RADAR',
categoryColor: '#34d399',
title: 'Sommer-Season Ahead',
description: 'Outdoor & Energy-Saving Content hat Potenzial für Q2 Kampagnen.',
type: 'Opportunity',
priority: 'Mittel',
priorityColor: '#f59e0b',
image: 'https://images.unsplash.com/photo-1506905925346-21bda4d32df4?w=80&h=80&fit=crop',
},
];
const PIPELINE_STAGES = [
{ label: 'Idee', count: 56, icon: '◇', color: '#60a5fa' },
{ label: 'Draft', count: 24, icon: '✎', color: '#a78bfa' },
{ label: 'Review', count: 7, icon: '✓', color: '#f59e0b' },
{ label: 'Geplant', count: 18, icon: '⊞', color: '#00d4ff' },
{ label: 'Veröffentlicht', count: 132, icon: '✓', color: '#10b981' },
];
const CHANNELS = [
{ name: 'TikTok', icon: '🎵', planned: 18, connected: true },
{ name: 'Instagram', icon: '📸', planned: 16, connected: true },
{ name: 'LinkedIn', icon: '💼', planned: 12, connected: true },
{ name: 'X (Twitter)', icon: '✗', planned: 14, connected: true },
{ name: 'YouTube Shorts', icon: '▶', planned: 10, connected: true },
];
export default function WarRoom() {
const [, navigate] = useLocation();
const { posts } = usePostizPosts();
const { integrations } = usePostizIntegrations();
const drafts = posts.filter(p => p.status === 'DRAFT').length;
const scheduled = posts.filter(p => p.status === 'QUEUE').length;
const connectedChannels = integrations.filter(i => !i.disabled).length;
return (
<div className="p-5 space-y-5 animate-fade-in-up">
{/* Page Header */}
<div className="flex items-center justify-between">
<div>
<h1 className="text-xl font-bold" style={{ fontFamily: 'var(--font-display)', color: 'var(--foreground)' }}>
War Room
</h1>
<p className="text-sm mt-0.5" style={{ color: 'var(--muted-foreground)' }}>
Zentrale Übersicht & Steuerung deiner Content-Operationen
</p>
</div>
<div className="flex items-center gap-2">
<div
className="flex items-center gap-1.5 px-3 py-1.5 rounded"
style={{ background: 'var(--atlas-surface)', border: '1px solid var(--atlas-border)' }}
>
<Calendar size={13} style={{ color: 'var(--muted-foreground)' }} />
<span className="text-xs" style={{ color: 'var(--foreground)', fontFamily: 'var(--font-body)' }}>
{new Date().toLocaleDateString('de-DE', { day: 'numeric', month: 'long', year: 'numeric' })}
</span>
</div>
<select
className="text-xs px-2 py-1.5 rounded"
style={{
background: 'var(--atlas-surface)',
border: '1px solid var(--atlas-border)',
color: 'var(--foreground)',
fontFamily: 'var(--font-body)'
}}
>
<option value="7">7 Tage</option>
<option value="30">30 Tage</option>
<option value="90">90 Tage</option>
</select>
</div>
</div>
{/* KPI Cards */}
<div className="flex gap-3">
<KpiCard
icon={<Calendar size={16} style={{ color: '#60a5fa' }} />}
iconBg="oklch(0.6 0.15 250 / 0.15)"
title="Heute geplant"
value={String(scheduled || 18)}
trend="↑ 12%"
trendUp={true}
trendLabel="vs. gestern"
sparkColor="#60a5fa"
sparkTrend="up"
/>
<KpiCard
icon={<FileText size={16} style={{ color: '#a78bfa' }} />}
iconBg="oklch(0.6 0.15 300 / 0.15)"
title="Entwürfe offen"
value={String(drafts || 24)}
trend="↑ 8%"
trendUp={true}
trendLabel="vs. gestern"
sparkColor="#a78bfa"
sparkTrend="up"
/>
<KpiCard
icon={<CheckSquare size={16} style={{ color: '#ef4444' }} />}
iconBg="oklch(0.62 0.22 25 / 0.15)"
title="Freigaben nötig"
value="7"
trend="↓ 13%"
trendUp={false}
trendLabel="vs. gestern"
sparkColor="#ef4444"
sparkTrend="down"
/>
<KpiCard
icon={<Share2 size={16} style={{ color: '#10b981' }} />}
iconBg="oklch(0.65 0.15 165 / 0.15)"
title="Kanäle verbunden"
value={`${connectedChannels || 5} / 5`}
trend=""
trendUp={null}
trendLabel=""
subtext="Alle Systeme online"
subtextColor="var(--atlas-green)"
sparkColor="#10b981"
sparkTrend="flat"
/>
<KpiCard
icon={<Diamond size={16} style={{ color: '#00d4ff' }} />}
iconBg="oklch(0.82 0.18 210 / 0.12)"
title="Affiliate-Potenzial"
value="€12.430"
trend="↑ 24%"
trendUp={true}
trendLabel="vs. 7 Tage"
sparkColor="#00d4ff"
sparkTrend="up"
/>
</div>
{/* Signal Cards */}
<div>
<div className="flex items-center justify-between mb-3">
<div className="flex items-center gap-2">
<h2 className="text-sm font-semibold" style={{ fontFamily: 'var(--font-display)', color: 'var(--foreground)' }}>
Signal Cards
</h2>
<span className="atlas-badge-cyan" style={{ fontSize: '0.6rem' }}>Von ATLAS-Systemen</span>
</div>
<button
onClick={() => navigate('/signals')}
className="flex items-center gap-1 text-xs transition-colors"
style={{ color: 'var(--atlas-cyan)', fontFamily: 'var(--font-body)' }}
>
Alle Signale anzeigen <ArrowRight size={12} />
</button>
</div>
<div className="grid grid-cols-4 gap-3">
{SIGNAL_CARDS.map((card, i) => (
<div
key={i}
className="atlas-card atlas-card-hover p-3 cursor-pointer"
style={{ animationDelay: `${i * 60}ms` }}
>
<div className="flex items-start justify-between mb-2">
<span
className="text-xs font-semibold"
style={{ color: card.categoryColor, fontFamily: 'var(--font-mono)', fontSize: '0.625rem', letterSpacing: '0.06em' }}
>
{card.category}
</span>
<Star size={12} style={{ color: 'var(--muted-foreground)' }} />
</div>
<div className="flex gap-2 mb-2">
<img
src={card.image}
alt={card.title}
className="rounded flex-shrink-0"
style={{ width: '40px', height: '40px', objectFit: 'cover' }}
onError={e => { (e.target as HTMLImageElement).style.display = 'none'; }}
/>
<div className="min-w-0">
<div className="text-xs font-semibold truncate" style={{ color: 'var(--foreground)', fontFamily: 'var(--font-display)' }}>
{card.title}
</div>
<div className="text-xs mt-0.5 line-clamp-2" style={{ color: 'var(--muted-foreground)', fontSize: '0.6875rem', lineHeight: '1.4' }}>
{card.description}
</div>
</div>
</div>
<div className="flex items-center gap-1.5">
<span
className="text-xs px-1.5 py-0.5 rounded"
style={{
background: 'var(--atlas-navy)',
color: 'var(--muted-foreground)',
border: '1px solid var(--atlas-border)',
fontSize: '0.6rem'
}}
>
{card.type}
</span>
<span
className="text-xs px-1.5 py-0.5 rounded"
style={{
background: `${card.priorityColor}20`,
color: card.priorityColor,
border: `1px solid ${card.priorityColor}40`,
fontSize: '0.6rem'
}}
>
{card.priority}
</span>
</div>
</div>
))}
</div>
</div>
{/* Bottom Row: Pipeline + Kanal-Status */}
<div className="grid grid-cols-2 gap-4">
{/* Content Pipeline */}
<div className="atlas-card p-4">
<h2 className="text-sm font-semibold mb-3" style={{ fontFamily: 'var(--font-display)', color: 'var(--foreground)' }}>
Content Pipeline
</h2>
<div className="flex items-center gap-1 mb-4">
{PIPELINE_STAGES.map((stage, i) => (
<div key={i} className="flex items-center gap-1 flex-1">
<div
className="flex-1 rounded flex flex-col items-center justify-center py-3 px-2"
style={{ background: `${stage.color}15`, border: `1px solid ${stage.color}30` }}
>
<div className="text-lg font-bold" style={{ fontFamily: 'var(--font-mono)', color: stage.color }}>
{stage.count}
</div>
<div className="text-xs mt-0.5" style={{ color: 'var(--muted-foreground)', fontSize: '0.6rem', textAlign: 'center' }}>
{stage.label}
</div>
</div>
{i < PIPELINE_STAGES.length - 1 && (
<div style={{ color: 'var(--atlas-border)', fontSize: '0.75rem' }}></div>
)}
</div>
))}
</div>
<div className="flex gap-4">
<div>
<div className="text-xs mb-1" style={{ color: 'var(--muted-foreground)' }}>Conversion Rate</div>
<div className="flex items-center gap-2">
<div className="flex-1 h-1.5 rounded-full" style={{ background: 'var(--atlas-border)' }}>
<div className="h-full rounded-full" style={{ width: '32%', background: 'var(--atlas-cyan)' }} />
</div>
<span className="text-xs font-bold" style={{ fontFamily: 'var(--font-mono)', color: 'var(--atlas-cyan)' }}>32%</span>
</div>
</div>
<div>
<div className="text-xs mb-1" style={{ color: 'var(--muted-foreground)' }}>Ø Time to Publish</div>
<div className="flex items-center gap-2">
<div className="flex-1 h-1.5 rounded-full" style={{ background: 'var(--atlas-border)' }}>
<div className="h-full rounded-full" style={{ width: '48%', background: '#00d4ff' }} />
</div>
<span className="text-xs font-bold" style={{ fontFamily: 'var(--font-mono)', color: '#00d4ff' }}>2.4 Tage</span>
</div>
</div>
</div>
</div>
{/* Kanal-Status */}
<div className="atlas-card p-4">
<div className="flex items-center justify-between mb-3">
<h2 className="text-sm font-semibold" style={{ fontFamily: 'var(--font-display)', color: 'var(--foreground)' }}>
Kanal-Status
</h2>
<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 Kanäle</option>
</select>
</div>
<div className="space-y-2">
{CHANNELS.map((ch, i) => (
<div key={i} className="flex items-center justify-between py-1.5">
<div className="flex items-center gap-2">
<span style={{ fontSize: '14px' }}>{ch.icon}</span>
<span className="text-sm" style={{ color: 'var(--foreground)', fontFamily: 'var(--font-body)' }}>
{ch.name}
</span>
</div>
<div className="flex items-center gap-3">
<div className="flex items-center gap-1">
<div className="atlas-status-dot online" />
<span className="text-xs" style={{ color: 'var(--atlas-green)' }}>Verbunden</span>
</div>
<span className="text-xs" style={{ color: 'var(--muted-foreground)', fontFamily: 'var(--font-mono)' }}>
{ch.planned} geplant
</span>
<Sparkline color="#00d4ff" trend="up" />
</div>
</div>
))}
</div>
</div>
</div>
</div>
);
}

View file

@ -0,0 +1,81 @@
# ATLAS Social Command Center — Design References
Dieses Verzeichnis enthält die offiziellen Design-Referenzbilder für den ATLAS Social Command Center.
**Alle zukünftigen Agenten müssen diese Bilder als verbindliche Design-Vorlage verwenden.**
## Logo & Brand
| Datei | Beschreibung |
|-------|-------------|
| `atlas-logo-symbol.png` | ATLAS OS Logo ohne Text — 3D-Pyramide, Cyan-Glow, Deep Navy Hintergrund |
| `atlas-logo-with-text.png` | ATLAS OS Logo mit "Atlas OS" Schriftzug darunter |
**Logo-Verwendung:** Das 3D-Pyramiden-Symbol (ohne Text) wird in der Sidebar oben links verwendet, ca. 48×48px. Darunter steht "Atlas Social" in Space Grotesk.
## Screen-Referenzen
| Datei | Screen | Wichtigste Elemente |
|-------|--------|---------------------|
| `screen-war-room.png` | War Room (Startseite) | 5 KPI-Karten oben, Signal Cards (4 Karten), Content Pipeline Funnel, Kanal-Status rechts |
| `screen-signale.png` | Signale | Filter-Tabs, Signal-Cards mit Potenzial-Score (0-100), "Als Draft erstellen" + "Zur Kampagne" Buttons, Top-Chancen Sidebar, System-Insights |
| `screen-draft-studio.png` | Draft Studio / Entwürfe | Editor links (Titel, Produkt, UTM, Content-Blöcke, Assets), Multi-Platform-Vorschau rechts (TikTok/Instagram/LinkedIn/X), Freigabe-Status unten |
| `screen-kalender.png` | Kalender & Freigaben | Wochenkalender nach Kanal (TikTok/Instagram/LinkedIn/X/YouTube), Freigaben-Queue darunter als Tabelle |
| `screen-freigaben.png` | Freigaben | Freigaben-Queue Tabelle mit Priorität, Schnellvorschau rechts, Audit-Verlauf, Filter-Tabs |
| `screen-kampagnen.png` | Kampagnen | Kampagnen-Cards (4 Karten), Kampagnen-Timeline, Top Kampagnen Sidebar |
| `screen-produkte.png` | Produkte | Tabelle mit Content-Score, Affiliate-Status, "Als Draft nutzen" Button pro Produkt |
| `screen-analytics.png` | Analytics | KPI-Bar, Performance-Chart (Multi-Line), Reichweite nach Kanal (Donut), Content Performance Tabelle, Performance-Funnel |
| `screen-einstellungen.png` | Einstellungen | Verbundene Kanäle, API & Automatisierung, UTM-Standardregeln, Branding, Freigabe-Workflow, Team |
## Design-System
### Farben
- **Background:** `#0a0e1a` (Deep Space Navy)
- **Surface/Cards:** `#0f1629`
- **Border:** `#1e2d4a`
- **ATLAS Cyan:** `#00a3ff` / `#00d4ff` (Signature Color)
- **Grün (Online/Erfolg):** `#10b981`
- **Amber (Warnung):** `#f59e0b`
- **Rot (Fehler/Dringend):** `#ef4444`
- **Text:** `#e2e8f0` / Muted: `#64748b`
### Typografie
- **Headings:** Space Grotesk (700)
- **Body:** Inter (400/500)
- **Metriken/Zahlen:** Space Mono (400/700)
### Layout
- **3 Spalten:** Sidebar (160px) | Content (flex-grow) | Co-Pilot (320px)
- **Sidebar:** Logo oben, Navigation-Sections (Mission/Content/System), User-Footer unten
- **Co-Pilot:** Immer rechts, ATLAS-Logo als Avatar, "Vorgeschlagene Befehle" als Quick-Actions
### Navigation (aus den Screens)
1. War Room
2. Signale
3. Entwürfe (Draft Studio)
4. Kalender
5. Kampagnen
6. Produkte
7. Freigaben
8. Analytics
9. Einstellungen
### KPI-Karten (War Room)
Jede Karte hat: Icon (farbig), Titel, Großer Zahlenwert, Trend-Indikator (↑↓ % vs. gestern), Mini-Sparkline-Chart unten
### Signal Cards
Jede Karte hat: Kategorie-Tag (SMART HOME/KITCHEN/NEWS/RADAR), Produktbild, Titel, Beschreibung, Typ-Badge + Prioritäts-Badge, "Als Draft erstellen" Button, "Zur Kampagne" Button, Potenzial-Score (Zahl + Trend)
### Co-Pilot Panel
- ATLAS 3D-Logo als Avatar (groß, oben)
- "Dein AI Command Partner" Überschrift
- "Vorgeschlagene Befehle" als Liste mit Pfeil-Buttons
- Eingabefeld unten: "Stelle ATLAS eine Frage oder gib einen Befehl..."
- Disclaimer: "ATLAS kann Fehler machen. Prüfe wichtige Infos."
## Technischer Kontext
- **Postiz API:** https://postiz.atlas-os.ai (Publishing-Engine)
- **Catalog:** https://search.atlas-os.ai/catalog.json (Produktquelle)
- **n8n:** https://n8n.atlas-os.ai (Workflow-Automatisierung)
- **Deploy:** VPS3 (82.165.139.249), /var/www/atlas-social-command-center/
- **Domain:** social.atlas-os.ai → Nginx → Static Files
- **Kein Manus-Hosting** — alles auf System ATLAS

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

66
ideas.md Normal file
View file

@ -0,0 +1,66 @@
# ATLAS Social Command Center — Design System
> **Für zukünftige Agenten:** Alle Design-Referenzbilder liegen in `docs/design-references/`.
> Das README dort ist die verbindliche Spezifikation. Immer zuerst lesen!
> 9 Referenzbilder: 32486-32498 (screen-war-room, screen-signale, screen-draft-studio,
> screen-kalender, screen-freigaben, screen-kampagnen, screen-produkte, screen-analytics,
> screen-einstellungen + 2x Logo)
## Gewählter Ansatz: Bloomberg Terminal × Raumschiff-Brücke
Exakt wie in den 9 Referenzbildern definiert. Diese Bilder sind die Ground Truth.
### Design Movement
Operational Intelligence Interface — Bloomberg Terminal × NASA Mission Control × Creator Studio.
Dark Navy, Cyan-Glow, dichte Informationsarchitektur, 3D-Pyramiden-Logo.
### Color Philosophy
- **Background:** #0a0e1a (Deep Space Navy) — OKLCH: oklch(0.12 0.03 265)
- **Surface/Cards:** #0f1629 — OKLCH: oklch(0.15 0.03 265)
- **Border:** #1e2d4a — OKLCH: oklch(0.22 0.05 265)
- **ATLAS Cyan:** #00d4ff — OKLCH: oklch(0.82 0.18 210) — Signature Color
- **Grün:** #10b981 / Amber: #f59e0b / Rot: #ef4444
- **Text:** #e2e8f0 / Muted: #64748b
### Logo (KRITISCH)
Das echte ATLAS OS Logo ist eine 3D-Pyramide mit Cyan-Glow auf Deep Navy.
- Referenz: docs/design-references/atlas-logo-symbol.png (ohne Text)
- In Sidebar: Symbol (~40px) + "Atlas Social" Text
- Im Co-Pilot: Größeres Symbol (~80px) als Avatar
### Typography
- Headings: Space Grotesk (700)
- Body: Inter (400/500)
- Metrics/Numbers: Space Mono (400/700)
### Layout: 3 Spalten
- Sidebar: 160px (Logo, Nav, ATLAS STATUS, User-Footer)
- Content: flex-grow
- Co-Pilot: 320px (Logo-Avatar, Vorgeschlagene Befehle, Chat-Input)
### Navigation (aus Screens)
MISSION: War Room, Signale, Entwürfe, Kalender
CONTENT: Kampagnen, Produkte, Freigaben, Analytics
SYSTEM: Einstellungen
### KPI-Karten (War Room)
5 Karten: Heute geplant / Entwürfe offen / Freigaben nötig / Kanäle verbunden / Affiliate-Potenzial
Jede Karte: Icon, Titel, Großer Wert, Trend-%, Mini-Sparkline
### Signal Cards
Kategorie-Tag, Produktbild, Titel, Beschreibung, Potenzial-Score (0-100),
Empfohlene Plattform, "Als Draft erstellen" + "Zur Kampagne" Buttons
### Co-Pilot Panel
Header: Logo + "ATLAS Co-Pilot" + BETA Badge
Body: 3D-Logo Avatar, "Dein AI Command Partner", Vorgeschlagene Befehle (4-6 Quick-Actions)
Footer: Eingabefeld + Sende-Button + Disclaimer
## Style Decisions
- Dark Mode only, kein Toggle
- ATLAS 3D-Pyramiden-Logo aus docs/design-references/ verwenden
- Alle Metriken in Space Mono
- Status-Dots pulsieren
- Hover: Cyan-Glow (box-shadow: 0 0 12px rgba(0,212,255,0.3))
- Aktive Nav-Items: Left-Border-Indicator in Cyan
- ATLAS STATUS Footer in Sidebar: "Systems Operational" + grüner Dot

View file

@ -39,10 +39,14 @@
"@radix-ui/react-toggle": "^1.1.10",
"@radix-ui/react-toggle-group": "^1.1.11",
"@radix-ui/react-tooltip": "^1.2.8",
"@react-three/drei": "^10.7.7",
"@react-three/fiber": "^9.6.1",
"@types/three": "^0.185.0",
"axios": "^1.12.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "^1.1.1",
"date-fns": "^4.4.0",
"embla-carousel-react": "^8.6.0",
"express": "^4.21.2",
"framer-motion": "^12.23.22",
@ -60,6 +64,7 @@
"streamdown": "^1.4.0",
"tailwind-merge": "^3.3.1",
"tailwindcss-animate": "^1.0.7",
"three": "^0.185.1",
"vaul": "^1.1.2",
"wouter": "^3.3.5",
"zod": "^4.1.12"

555
pnpm-lock.yaml generated

File diff suppressed because it is too large Load diff