feat: ATLAS Social Command Center v1.0 - initial commit
0
.gitkeep
Normal file
|
|
@ -1,5 +1,5 @@
|
||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="de" class="dark">
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
|
|
@ -7,11 +7,9 @@
|
||||||
name="viewport"
|
name="viewport"
|
||||||
content="width=device-width, initial-scale=1.0, maximum-scale=1" />
|
content="width=device-width, initial-scale=1.0, maximum-scale=1" />
|
||||||
<title>ATLAS Social Command Center</title>
|
<title>ATLAS Social Command Center</title>
|
||||||
<!-- THIS IS THE START OF A COMMENT BLOCK, BLOCK TO BE DELETED: Google Fonts here, example:
|
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
<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" />
|
<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" />
|
||||||
THIS IS THE END OF A COMMENT BLOCK, BLOCK TO BE DELETED -->
|
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
|
|
|
||||||
|
|
@ -1,38 +1,69 @@
|
||||||
import { Toaster } from "@/components/ui/sonner";
|
import { Toaster } from "@/components/ui/sonner";
|
||||||
import { TooltipProvider } from "@/components/ui/tooltip";
|
import { TooltipProvider } from "@/components/ui/tooltip";
|
||||||
import NotFound from "@/pages/NotFound";
|
import NotFound from "@/pages/NotFound";
|
||||||
import { Route, Switch } from "wouter";
|
import { Route, Switch, useLocation } from "wouter";
|
||||||
import ErrorBoundary from "./components/ErrorBoundary";
|
import ErrorBoundary from "./components/ErrorBoundary";
|
||||||
import { ThemeProvider } from "./contexts/ThemeContext";
|
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 (
|
return (
|
||||||
<Switch>
|
<div className="flex items-center justify-center h-full" style={{ color: 'var(--atlas-cyan)' }}>
|
||||||
<Route path={"/"} component={Home} />
|
<div className="flex flex-col items-center gap-3">
|
||||||
<Route path={"/404"} component={NotFound} />
|
<div
|
||||||
{/* Final fallback route */}
|
className="w-6 h-6 rounded-full border-2 border-t-transparent animate-spin"
|
||||||
<Route component={NotFound} />
|
style={{ borderColor: 'var(--atlas-cyan)', borderTopColor: 'transparent' }}
|
||||||
</Switch>
|
/>
|
||||||
|
<span className="text-xs" style={{ fontFamily: 'var(--font-mono)', color: 'var(--muted-foreground)' }}>
|
||||||
|
ATLAS LOADING...
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// NOTE: About Theme
|
function AppRoutes() {
|
||||||
// - First choose a default theme according to your design style (dark or light bg), than change color palette in index.css
|
const [location] = useLocation();
|
||||||
// to keep consistent foreground/background color across components
|
|
||||||
// - If you want to make theme switchable, pass `switchable` ThemeProvider and use `useTheme` hook
|
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() {
|
function App() {
|
||||||
return (
|
return (
|
||||||
<ErrorBoundary>
|
<ErrorBoundary>
|
||||||
<ThemeProvider
|
<ThemeProvider defaultTheme="dark">
|
||||||
defaultTheme="light"
|
|
||||||
// switchable
|
|
||||||
>
|
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
<Toaster />
|
<Toaster />
|
||||||
<Router />
|
<AppRoutes />
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
|
|
|
||||||
130
client/src/components/layout/AppShell.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
409
client/src/components/layout/CoPilotPanel.tsx
Normal 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?`;
|
||||||
|
}
|
||||||
188
client/src/components/layout/Sidebar.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
116
client/src/hooks/useCatalog.ts
Normal 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'],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
213
client/src/hooks/usePostiz.ts
Normal 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(),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
@ -3,6 +3,15 @@
|
||||||
|
|
||||||
@custom-variant dark (&:is(.dark *));
|
@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 {
|
@theme inline {
|
||||||
--radius-sm: calc(var(--radius) - 4px);
|
--radius-sm: calc(var(--radius) - 4px);
|
||||||
--radius-md: calc(var(--radius) - 2px);
|
--radius-md: calc(var(--radius) - 2px);
|
||||||
|
|
@ -40,86 +49,95 @@
|
||||||
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||||
--color-sidebar-border: var(--sidebar-border);
|
--color-sidebar-border: var(--sidebar-border);
|
||||||
--color-sidebar-ring: var(--sidebar-ring);
|
--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 {
|
/* ATLAS Dark Theme — Only Mode */
|
||||||
--primary: var(--color-blue-700);
|
:root, .dark {
|
||||||
--primary-foreground: var(--color-blue-50);
|
/* Core ATLAS Colors */
|
||||||
--sidebar-primary: var(--color-blue-600);
|
--atlas-cyan: oklch(0.82 0.18 210); /* #00d4ff */
|
||||||
--sidebar-primary-foreground: var(--color-blue-50);
|
--atlas-cyan-glow: oklch(0.82 0.18 210 / 0.15);
|
||||||
--chart-1: var(--color-blue-300);
|
--atlas-navy: oklch(0.12 0.03 265); /* #0a0e1a */
|
||||||
--chart-2: var(--color-blue-500);
|
--atlas-surface: oklch(0.15 0.03 265); /* #0f1629 */
|
||||||
--chart-3: var(--color-blue-600);
|
--atlas-border: oklch(0.22 0.05 265); /* #1e2d4a */
|
||||||
--chart-4: var(--color-blue-700);
|
--atlas-amber: oklch(0.75 0.15 75); /* #f59e0b */
|
||||||
--chart-5: var(--color-blue-800);
|
--atlas-green: oklch(0.65 0.15 165); /* #10b981 */
|
||||||
--radius: 0.65rem;
|
--atlas-red: oklch(0.62 0.22 25); /* #ef4444 */
|
||||||
--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 {
|
/* Semantic Tokens — Dark Mode Only */
|
||||||
--primary: var(--color-blue-700);
|
--radius: 0.375rem;
|
||||||
--primary-foreground: var(--color-blue-50);
|
--background: oklch(0.12 0.03 265);
|
||||||
--sidebar-primary: var(--color-blue-500);
|
--foreground: oklch(0.88 0.01 220);
|
||||||
--sidebar-primary-foreground: var(--color-blue-50);
|
--card: oklch(0.15 0.03 265);
|
||||||
--background: oklch(0.141 0.005 285.823);
|
--card-foreground: oklch(0.88 0.01 220);
|
||||||
--foreground: oklch(0.85 0.005 65);
|
--popover: oklch(0.17 0.04 265);
|
||||||
--card: oklch(0.21 0.006 285.885);
|
--popover-foreground: oklch(0.88 0.01 220);
|
||||||
--card-foreground: oklch(0.85 0.005 65);
|
--primary: oklch(0.82 0.18 210);
|
||||||
--popover: oklch(0.21 0.006 285.885);
|
--primary-foreground: oklch(0.12 0.03 265);
|
||||||
--popover-foreground: oklch(0.85 0.005 65);
|
--secondary: oklch(0.20 0.04 265);
|
||||||
--secondary: oklch(0.24 0.006 286.033);
|
--secondary-foreground: oklch(0.75 0.01 220);
|
||||||
--secondary-foreground: oklch(0.7 0.005 65);
|
--muted: oklch(0.20 0.04 265);
|
||||||
--muted: oklch(0.274 0.006 286.033);
|
--muted-foreground: oklch(0.52 0.02 220);
|
||||||
--muted-foreground: oklch(0.705 0.015 286.067);
|
--accent: oklch(0.22 0.05 265);
|
||||||
--accent: oklch(0.274 0.006 286.033);
|
--accent-foreground: oklch(0.88 0.01 220);
|
||||||
--accent-foreground: oklch(0.92 0.005 65);
|
--destructive: oklch(0.62 0.22 25);
|
||||||
--destructive: oklch(0.704 0.191 22.216);
|
--destructive-foreground: oklch(0.98 0 0);
|
||||||
--destructive-foreground: oklch(0.985 0 0);
|
--border: oklch(0.22 0.05 265);
|
||||||
--border: oklch(1 0 0 / 10%);
|
--input: oklch(0.20 0.04 265);
|
||||||
--input: oklch(1 0 0 / 15%);
|
--ring: oklch(0.82 0.18 210);
|
||||||
--ring: oklch(0.488 0.243 264.376);
|
|
||||||
--chart-1: var(--color-blue-300);
|
/* Charts */
|
||||||
--chart-2: var(--color-blue-500);
|
--chart-1: oklch(0.82 0.18 210);
|
||||||
--chart-3: var(--color-blue-600);
|
--chart-2: oklch(0.75 0.15 75);
|
||||||
--chart-4: var(--color-blue-700);
|
--chart-3: oklch(0.65 0.15 165);
|
||||||
--chart-5: var(--color-blue-800);
|
--chart-4: oklch(0.62 0.22 25);
|
||||||
--sidebar: oklch(0.21 0.006 285.885);
|
--chart-5: oklch(0.70 0.12 300);
|
||||||
--sidebar-foreground: oklch(0.85 0.005 65);
|
|
||||||
--sidebar-accent: oklch(0.274 0.006 286.033);
|
/* Sidebar */
|
||||||
--sidebar-accent-foreground: oklch(0.985 0 0);
|
--sidebar: oklch(0.13 0.03 265);
|
||||||
--sidebar-border: oklch(1 0 0 / 10%);
|
--sidebar-foreground: oklch(0.88 0.01 220);
|
||||||
--sidebar-ring: oklch(0.488 0.243 264.376);
|
--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 {
|
@layer base {
|
||||||
* {
|
* {
|
||||||
@apply border-border outline-ring/50;
|
@apply border-border outline-ring/50;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
html {
|
||||||
|
color-scheme: dark;
|
||||||
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
@apply bg-background text-foreground;
|
@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),
|
button:not(:disabled),
|
||||||
[role="button"]:not([aria-disabled="true"]),
|
[role="button"]:not([aria-disabled="true"]),
|
||||||
[type="button"]:not(:disabled),
|
[type="button"]:not(:disabled),
|
||||||
|
|
@ -134,24 +152,11 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
@layer components {
|
@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 {
|
.container {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
margin-right: auto;
|
margin-right: auto;
|
||||||
padding-left: 1rem; /* 16px - mobile padding */
|
padding-left: 1rem;
|
||||||
padding-right: 1rem;
|
padding-right: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -162,16 +167,327 @@
|
||||||
|
|
||||||
@media (min-width: 640px) {
|
@media (min-width: 640px) {
|
||||||
.container {
|
.container {
|
||||||
padding-left: 1.5rem; /* 24px - tablet padding */
|
padding-left: 1.5rem;
|
||||||
padding-right: 1.5rem;
|
padding-right: 1.5rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 1024px) {
|
@media (min-width: 1024px) {
|
||||||
.container {
|
.container {
|
||||||
padding-left: 2rem; /* 32px - desktop padding */
|
padding-left: 2rem;
|
||||||
padding-right: 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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
361
client/src/pages/Analytics.tsx
Normal 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)' }}>1–5 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
284
client/src/pages/Approvals.tsx
Normal 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)' }}>1–10 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
305
client/src/pages/Calendar.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
246
client/src/pages/Campaigns.tsx
Normal 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
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
264
client/src/pages/Products.tsx
Normal 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)' }}>1–5 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
314
client/src/pages/Settings.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
367
client/src/pages/Signals.tsx
Normal 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)' }}>
|
||||||
|
1–6 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
448
client/src/pages/WarRoom.tsx
Normal 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: Idee→Draft→Review→Geplant→Verö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>
|
||||||
|
);
|
||||||
|
}
|
||||||
81
docs/design-references/README.md
Normal 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
|
||||||
BIN
docs/design-references/atlas-logo-symbol.png
Normal file
|
After Width: | Height: | Size: 1.4 MiB |
BIN
docs/design-references/atlas-logo-with-text.png
Normal file
|
After Width: | Height: | Size: 1.5 MiB |
BIN
docs/design-references/screen-analytics.png
Normal file
|
After Width: | Height: | Size: 1.6 MiB |
BIN
docs/design-references/screen-draft-studio.png
Normal file
|
After Width: | Height: | Size: 1.5 MiB |
BIN
docs/design-references/screen-einstellungen.png
Normal file
|
After Width: | Height: | Size: 1.5 MiB |
BIN
docs/design-references/screen-freigaben.png
Normal file
|
After Width: | Height: | Size: 1.5 MiB |
BIN
docs/design-references/screen-kalender.png
Normal file
|
After Width: | Height: | Size: 1.6 MiB |
BIN
docs/design-references/screen-kampagnen.png
Normal file
|
After Width: | Height: | Size: 1.7 MiB |
BIN
docs/design-references/screen-produkte.png
Normal file
|
After Width: | Height: | Size: 1.5 MiB |
BIN
docs/design-references/screen-signale.png
Normal file
|
After Width: | Height: | Size: 1.5 MiB |
BIN
docs/design-references/screen-war-room.png
Normal file
|
After Width: | Height: | Size: 1.5 MiB |
66
ideas.md
Normal 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
|
||||||
|
|
@ -39,10 +39,14 @@
|
||||||
"@radix-ui/react-toggle": "^1.1.10",
|
"@radix-ui/react-toggle": "^1.1.10",
|
||||||
"@radix-ui/react-toggle-group": "^1.1.11",
|
"@radix-ui/react-toggle-group": "^1.1.11",
|
||||||
"@radix-ui/react-tooltip": "^1.2.8",
|
"@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",
|
"axios": "^1.12.0",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"cmdk": "^1.1.1",
|
"cmdk": "^1.1.1",
|
||||||
|
"date-fns": "^4.4.0",
|
||||||
"embla-carousel-react": "^8.6.0",
|
"embla-carousel-react": "^8.6.0",
|
||||||
"express": "^4.21.2",
|
"express": "^4.21.2",
|
||||||
"framer-motion": "^12.23.22",
|
"framer-motion": "^12.23.22",
|
||||||
|
|
@ -60,6 +64,7 @@
|
||||||
"streamdown": "^1.4.0",
|
"streamdown": "^1.4.0",
|
||||||
"tailwind-merge": "^3.3.1",
|
"tailwind-merge": "^3.3.1",
|
||||||
"tailwindcss-animate": "^1.0.7",
|
"tailwindcss-animate": "^1.0.7",
|
||||||
|
"three": "^0.185.1",
|
||||||
"vaul": "^1.1.2",
|
"vaul": "^1.1.2",
|
||||||
"wouter": "^3.3.5",
|
"wouter": "^3.3.5",
|
||||||
"zod": "^4.1.12"
|
"zod": "^4.1.12"
|
||||||
|
|
|
||||||