God Mode Valhalla: Initial Standalone Commit

This commit is contained in:
cawcenter
2025-12-14 19:19:14 -05:00
commit 153102b23e
127 changed files with 30781 additions and 0 deletions

View File

@@ -0,0 +1,61 @@
import React, { useEffect, useState } from 'react';
type SystemMetric = {
label: string;
status: 'active' | 'standby' | 'online' | 'connected' | 'ready' | 'error';
color: string;
};
export default function SystemStatus() {
const [metrics, setMetrics] = useState<SystemMetric[]>([
{ label: 'Intelligence Station', status: 'active', color: 'bg-green-500' },
{ label: 'Production Station', status: 'active', color: 'bg-green-500' },
{ label: 'WordPress Ignition', status: 'standby', color: 'bg-yellow-500' },
{ label: 'Core API', status: 'online', color: 'bg-blue-500' },
{ label: 'Directus DB', status: 'connected', color: 'bg-emerald-500' },
{ label: 'WP Connection', status: 'ready', color: 'bg-green-500' }
]);
// In a real scenario, we would poll an API here.
// For now, we simulate the "Live" feeling or check basic connectivity.
useEffect(() => {
const checkHealth = async () => {
// We can check Directus health via SDK in future
// For now, we trust the static state or toggle visually to show life
};
checkHealth();
}, []);
return (
<div className="p-4 rounded-lg bg-slate-900 border border-slate-700 shadow-xl w-full">
<h3 className="text-xs font-bold text-slate-400 uppercase tracking-wider mb-3 flex items-center gap-2">
<span className="w-2 h-2 rounded-full bg-green-500 animate-pulse"></span>
Sub-Station Status
</h3>
<div className="grid grid-cols-1 gap-2">
{metrics.map((m, idx) => (
<div key={idx} className="flex items-center justify-between group">
<span className="text-sm text-slate-300 font-medium group-hover:text-white transition-colors">{m.label}</span>
<div className="flex items-center gap-2">
<span className={`text-[10px] uppercase font-bold px-1.5 py-0.5 rounded text-white ${getStatusColor(m.status)}`}>
{m.status}
</span>
</div>
</div>
))}
</div>
</div>
);
}
function getStatusColor(status: string) {
switch (status) {
case 'active': return 'bg-green-600';
case 'standby': return 'bg-yellow-600';
case 'online': return 'bg-blue-600';
case 'connected': return 'bg-emerald-600';
case 'ready': return 'bg-green-600';
case 'error': return 'bg-red-600';
default: return 'bg-gray-600';
}
}

View File

@@ -0,0 +1,155 @@
import { useState, useEffect } from 'react';
interface SystemStatus {
coreApi: 'online' | 'offline' | 'checking';
database: 'connected' | 'disconnected' | 'checking';
wpConnection: 'ready' | 'error' | 'checking';
}
interface LogEntry {
time: string;
message: string;
type: 'info' | 'success' | 'warning' | 'error';
}
export default function SystemStatusBar() {
const [status, setStatus] = useState<SystemStatus>({
coreApi: 'checking',
database: 'checking',
wpConnection: 'checking'
});
const [logs, setLogs] = useState<LogEntry[]>([]);
const [showLogs, setShowLogs] = useState(false);
useEffect(() => {
checkStatus();
const interval = setInterval(checkStatus, 30000);
return () => clearInterval(interval);
}, []);
const addLog = (message: string, type: LogEntry['type']) => {
const newLog: LogEntry = {
time: new Date().toLocaleTimeString(),
message,
type
};
setLogs(prev => [newLog, ...prev].slice(0, 50));
};
const checkStatus = async () => {
try {
// We check OUR OWN backend API route which can then proxy/check other services or just confirm this server is up.
// This avoids CORS issues and ensures the frontend server is actually serving API routes correctly.
const response = await fetch('/api/system/health');
if (response.ok) {
setStatus({
coreApi: 'online',
database: 'connected',
wpConnection: 'ready'
});
addLog('System check passed', 'success');
} else {
throw new Error(`Health check failed: ${response.status}`);
}
} catch (error) {
console.error('Status check failed:', error);
setStatus({
coreApi: 'offline',
database: 'disconnected',
wpConnection: 'error'
});
addLog(`System check failed: ${error instanceof Error ? error.message : 'Unknown error'}`, 'error');
}
};
const getStatusColor = (state: string) => {
switch (state) {
case 'online':
case 'connected':
case 'ready':
return 'text-green-400';
case 'offline':
case 'disconnected':
case 'error':
return 'text-red-400';
case 'checking':
return 'text-yellow-400';
default:
return 'text-slate-400';
}
};
const getLogColor = (type: LogEntry['type']) => {
switch (type) {
case 'success':
return 'text-green-400';
case 'error':
return 'text-red-400';
case 'warning':
return 'text-yellow-400';
default:
return 'text-slate-400';
}
};
return (
<div className="fixed bottom-0 left-0 right-0 z-50 bg-titanium border-t border-edge-normal shadow-xl">
<div className="container mx-auto px-4 py-3">
<div className="flex items-center justify-between gap-4">
<h3 className="spark-label text-white">API & Logistics</h3>
<div className="flex items-center gap-6 flex-1">
<div className="flex items-center gap-2 text-sm">
<span className="text-silver">Core API</span>
<span className={getStatusColor(status.coreApi)}>
{status.coreApi.charAt(0).toUpperCase() + status.coreApi.slice(1)}
</span>
</div>
<div className="flex items-center gap-2 text-sm">
<span className="text-silver">Database (Directus)</span>
<span className={getStatusColor(status.database)}>
{status.database.charAt(0).toUpperCase() + status.database.slice(1)}
</span>
</div>
<div className="flex items-center gap-2 text-sm">
<span className="text-silver">WP Connection</span>
<span className={getStatusColor(status.wpConnection)}>
{status.wpConnection.charAt(0).toUpperCase() + status.wpConnection.slice(1)}
</span>
</div>
</div>
<button
onClick={() => setShowLogs(!showLogs)}
className="spark-btn-ghost text-sm"
>
{showLogs ? 'Hide' : 'Show'} Processing Log
</button>
</div>
</div>
{showLogs && (
<div className="border-t border-edge-subtle bg-void">
<div className="container mx-auto px-4 py-3 max-h-48 overflow-y-auto">
<div className="space-y-1">
{logs.length === 0 ? (
<div className="text-sm text-silver/50 italic">No recent activity</div>
) : (
logs.map((log, index) => (
<div key={index} className="flex items-start gap-2 text-sm font-mono">
<span className="text-silver/50 shrink-0">[{log.time}]</span>
<span className={getLogColor(log.type)}>{log.message}</span>
</div>
))
)}
</div>
</div>
</div>
)}
</div>
);
}

View File

@@ -0,0 +1,47 @@
// @ts-nocheck
import React, { useState, useEffect } from 'react';
import { getDirectusClient, readItem } from '@/lib/directus/client';
import { Card, CardContent } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
export default function PostEditor({ id }: { id: string }) {
const [post, setPost] = useState<any>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
async function load() {
const client = getDirectusClient();
try {
const data = await client.request(readItem('posts', id));
setPost(data);
} catch (e) { console.error(e); }
finally { setLoading(false); }
}
if (id) load();
}, [id]);
if (loading) return <div>Loading...</div>;
if (!post) return <div>Post not found</div>;
return (
<div className="space-y-6 max-w-4xl">
<Card className="bg-slate-800 border-slate-700 p-6 space-y-4">
<div className="space-y-2">
<Label>Post Title</Label>
<Input value={post.title} className="bg-slate-900 border-slate-700" />
</div>
<div className="space-y-2">
<Label>Slug</Label>
<Input value={post.slug} className="bg-slate-900 border-slate-700" />
</div>
<div className="space-y-2">
<Label>Content (Markdown/HTML)</Label>
<textarea className="w-full bg-slate-900 border-slate-700 rounded p-3 min-h-[300px]" value={post.content || ''}></textarea>
</div>
<Button className="mt-4">Save Changes</Button>
</Card>
</div>
);
}

View File

@@ -0,0 +1,68 @@
import React, { useState, useEffect } from 'react';
import { getDirectusClient, readItems } from '@/lib/directus/client';
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'; // Need to implement Table? Or use grid.
// Assume Table isn't fully ready or use Grid for now to be safe.
import { Card } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
import { Posts as Post } from '@/lib/schemas';
export default function PostList() {
const [posts, setPosts] = useState<Post[]>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
async function load() {
try {
const client = getDirectusClient();
// @ts-ignore
const data = await client.request(readItems('posts', { fields: ['*', 'site.name', 'author.name'], limit: 50 }));
setPosts(data as unknown as Post[]);
} catch (e) { console.error(e); }
finally { setLoading(false); }
}
load();
}, []);
if (loading) return <div>Loading...</div>;
return (
<div className="bg-slate-800 rounded-lg border border-slate-700 overflow-hidden">
<table className="w-full text-left text-sm text-slate-400">
<thead className="bg-slate-900/50 text-slate-200 uppercase font-medium">
<tr>
<th className="px-6 py-3">Title</th>
<th className="px-6 py-3">Site</th>
<th className="px-6 py-3">Status</th>
<th className="px-6 py-3">Date</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-700">
{posts.map(post => (
<tr key={post.id} className="hover:bg-slate-700/50 cursor-pointer transition-colors">
<td className="px-6 py-4">
<div className="font-medium text-slate-200">{post.title}</div>
<div className="text-xs text-slate-500">{post.slug}</div>
</td>
<td className="px-6 py-4">
{/* @ts-ignore */}
{post.site?.name || '-'}
</td>
<td className="px-6 py-4">
<Badge variant={post.status === 'published' ? 'default' : 'secondary'}>
{post.status}
</Badge>
</td>
</tr>
))}
{posts.length === 0 && (
<tr>
<td colSpan={3} className="px-6 py-12 text-center text-slate-500">
No posts found.
</td>
</tr>
)}
</tbody>
</table>
</div>
);
}

View File

@@ -0,0 +1,141 @@
import React, { useState } from 'react';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { getDirectusClient, readItems, createItem, updateItem, deleteItem } from '@/lib/directus/client';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
import { Plus, Trash2, Save, GripVertical } from 'lucide-react';
import { toast } from 'sonner';
const client = getDirectusClient();
interface NavItem {
id: string;
label: string;
url: string;
sort: number;
}
interface NavigationManagerProps {
siteId: string;
}
export default function NavigationManager({ siteId }: NavigationManagerProps) {
const queryClient = useQueryClient();
const [newItem, setNewItem] = useState({ label: '', url: '' });
const { data: items = [], isLoading } = useQuery({
queryKey: ['navigation', siteId],
queryFn: async () => {
// @ts-ignore
const res = await client.request(readItems('navigation', {
filter: { site: { _eq: siteId } },
sort: ['sort']
}));
return res as unknown as NavItem[];
}
});
const createMutation = useMutation({
mutationFn: async () => {
// @ts-ignore
await client.request(createItem('navigation', {
...newItem,
site: siteId,
sort: items.length + 1
}));
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['navigation', siteId] });
setNewItem({ label: '', url: '' });
toast.success('Menu item added');
}
});
const deleteMutation = useMutation({
mutationFn: async (id: string) => {
// @ts-ignore
await client.request(deleteItem('navigation', id));
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['navigation', siteId] });
toast.success('Menu item deleted');
}
});
const updateSortMutation = useMutation({
mutationFn: async ({ id, sort }: { id: string, sort: number }) => {
// @ts-ignore
await client.request(updateItem('navigation', id, { sort }));
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['navigation', siteId] });
}
});
return (
<div className="space-y-6 max-w-4xl">
<div className="bg-zinc-900/50 border border-zinc-800 rounded-lg p-4">
<h3 className="text-white font-medium mb-4">Add Menu Item</h3>
<div className="flex gap-4">
<Input
placeholder="Label (e.g. Home)"
value={newItem.label}
onChange={e => setNewItem({ ...newItem, label: e.target.value })}
className="bg-zinc-950 border-zinc-800"
/>
<Input
placeholder="URL (e.g. /home)"
value={newItem.url}
onChange={e => setNewItem({ ...newItem, url: e.target.value })}
className="bg-zinc-950 border-zinc-800"
/>
<Button onClick={() => createMutation.mutate()} disabled={!newItem.label} className="bg-blue-600 hover:bg-blue-500 whitespace-nowrap">
<Plus className="mr-2 h-4 w-4" /> Add Item
</Button>
</div>
</div>
<div className="rounded-md border border-zinc-800 bg-zinc-900/50 overflow-hidden">
<Table>
<TableHeader className="bg-zinc-950">
<TableRow className="border-zinc-800 hover:bg-zinc-950">
<TableHead className="w-[50px]"></TableHead>
<TableHead className="text-zinc-400">Label</TableHead>
<TableHead className="text-zinc-400">URL</TableHead>
<TableHead className="text-zinc-400 text-right">Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{items.length === 0 ? (
<TableRow>
<TableCell colSpan={4} className="h-24 text-center text-zinc-500">
No menu items. Add one above.
</TableCell>
</TableRow>
) : (
items.map((item, index) => (
<TableRow key={item.id} className="border-zinc-800 hover:bg-zinc-900/50">
<TableCell>
<GripVertical className="h-4 w-4 text-zinc-600 cursor-grab" />
</TableCell>
<TableCell className="font-medium text-white">
{item.label}
</TableCell>
<TableCell className="text-zinc-400 font-mono text-xs">
{item.url}
</TableCell>
<TableCell className="text-right">
<Button variant="ghost" size="icon" className="h-8 w-8 text-zinc-600 hover:text-red-500" onClick={() => deleteMutation.mutate(item.id)}>
<Trash2 className="h-4 w-4" />
</Button>
</TableCell>
</TableRow>
))
)}
</TableBody>
</Table>
</div>
</div>
);
}

View File

@@ -0,0 +1,257 @@
import React, { useState } from 'react';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { getDirectusClient, readItem, updateItem } from '@/lib/directus/client';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Textarea } from '@/components/ui/textarea';
import { Card, CardContent } from '@/components/ui/card';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { GripVertical, Plus, Trash2, LayoutTemplate, Type, Image as ImageIcon, Save, ArrowLeft } from 'lucide-react';
import { toast } from 'sonner';
const client = getDirectusClient();
interface PageBlock {
id: string;
block_type: 'hero' | 'content' | 'features' | 'cta';
block_config: any;
}
interface Page {
id: string;
title: string;
permalink: string;
status: string;
blocks: PageBlock[];
}
interface PageEditorProps {
pageId: string;
onBack?: () => void;
}
export default function PageEditor({ pageId, onBack }: PageEditorProps) {
const queryClient = useQueryClient();
const [blocks, setBlocks] = useState<PageBlock[]>([]);
const [pageMeta, setPageMeta] = useState<Partial<Page>>({});
const { isLoading } = useQuery({
queryKey: ['page', pageId],
queryFn: async () => {
// @ts-ignore
const res = await client.request(readItem('pages', pageId));
const page = res as unknown as Page;
setBlocks(page.blocks || []);
setPageMeta({ title: page.title, permalink: page.permalink, status: page.status });
return page;
},
enabled: !!pageId
});
const saveMutation = useMutation({
mutationFn: async () => {
// @ts-ignore
await client.request(updateItem('pages', pageId, {
...pageMeta,
blocks: blocks
}));
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['page', pageId] });
toast.success('Page saved successfully');
}
});
const addBlock = (block_type: PageBlock['block_type']) => {
const newBlock: PageBlock = {
id: crypto.randomUUID(),
block_type,
block_config: block_type === 'hero' ? { title: 'New Hero', subtitle: 'Subtitle here', bg: 'default' } :
block_type === 'content' ? { content: '<p>Start writing...</p>' } :
block_type === 'features' ? { items: [{ title: 'Feature 1', desc: 'Description' }] } :
{ label: 'Click Me', url: '#' }
};
setBlocks([...blocks, newBlock]);
};
const updateBlock = (id: string, config: any) => {
setBlocks(blocks.map(b => b.id === id ? { ...b, block_config: { ...b.block_config, ...config } } : b));
};
const removeBlock = (id: string) => {
setBlocks(blocks.filter(b => b.id !== id));
};
const moveBlock = (index: number, direction: 'up' | 'down') => {
const newBlocks = [...blocks];
if (direction === 'up' && index > 0) {
[newBlocks[index - 1], newBlocks[index]] = [newBlocks[index], newBlocks[index - 1]];
} else if (direction === 'down' && index < newBlocks.length - 1) {
[newBlocks[index + 1], newBlocks[index]] = [newBlocks[index], newBlocks[index + 1]];
}
setBlocks(newBlocks);
};
if (isLoading) return <div className="p-8 text-center text-zinc-500">Loading editor...</div>;
return (
<div className="flex h-screen bg-zinc-950 text-white overflow-hidden">
{/* Sidebar Controls */}
<div className="w-80 border-r border-zinc-800 bg-zinc-900/50 flex flex-col">
<div className="p-4 border-b border-zinc-800 flex items-center gap-2">
{onBack && <Button variant="ghost" size="icon" onClick={onBack}><ArrowLeft className="h-4 w-4" /></Button>}
<div>
<h2 className="font-bold text-sm">Page Editor</h2>
<Input
value={pageMeta.title || ''}
onChange={e => setPageMeta({ ...pageMeta, title: e.target.value })}
className="h-7 text-xs bg-transparent border-0 px-0 focus-visible:ring-0 placeholder:text-zinc-600"
placeholder="Page Title"
/>
</div>
</div>
<div className="flex-1 overflow-y-auto p-4 space-y-6">
<div>
<label className="text-xs uppercase font-bold text-zinc-500 mb-2 block">Add Blocks</label>
<div className="grid grid-cols-2 gap-2">
<Button variant="outline" className="justify-start border-zinc-800 hover:bg-zinc-800" onClick={() => addBlock('hero')}>
<LayoutTemplate className="mr-2 h-4 w-4 text-purple-400" /> Hero
</Button>
<Button variant="outline" className="justify-start border-zinc-800 hover:bg-zinc-800" onClick={() => addBlock('content')}>
<Type className="mr-2 h-4 w-4 text-blue-400" /> Content
</Button>
<Button variant="outline" className="justify-start border-zinc-800 hover:bg-zinc-800" onClick={() => addBlock('features')}>
<ImageIcon className="mr-2 h-4 w-4 text-green-400" /> Features
</Button>
</div>
</div>
<div>
<label className="text-xs uppercase font-bold text-zinc-500 mb-2 block">Page Settings</label>
<div className="space-y-3">
<div className="space-y-1">
<label className="text-xs text-zinc-400">Permalink</label>
<Input
value={pageMeta.permalink || ''}
onChange={e => setPageMeta({ ...pageMeta, permalink: e.target.value })}
className="bg-zinc-950 border-zinc-800 h-8"
/>
</div>
<div className="space-y-1">
<label className="text-xs text-zinc-400">Status</label>
<select
className="w-full bg-zinc-950 border border-zinc-800 rounded px-2 py-1 text-sm h-8"
value={pageMeta.status || 'draft'}
onChange={e => setPageMeta({ ...pageMeta, status: e.target.value })}
>
<option value="draft">Draft</option>
<option value="published">Published</option>
</select>
</div>
</div>
</div>
</div>
<div className="p-4 border-t border-zinc-800">
<Button className="w-full bg-blue-600 hover:bg-blue-500" onClick={() => saveMutation.mutate()}>
<Save className="mr-2 h-4 w-4" /> Save Page
</Button>
</div>
</div>
{/* Visual Canvas (Preview + Edit) */}
<div className="flex-1 overflow-y-auto bg-zinc-950 p-8">
<div className="max-w-4xl mx-auto space-y-4">
{blocks.map((block, index) => (
<Card key={block.id} className="bg-zinc-900 border-zinc-800 relative group transition-all hover:border-zinc-700">
{/* Block Actions */}
<div className="absolute right-2 top-2 opacity-0 group-hover:opacity-100 transition-opacity flex gap-1 bg-zinc-900 border border-zinc-800 rounded-md p-1 shadow-xl z-20">
<Button variant="ghost" size="icon" className="h-6 w-6" onClick={() => moveBlock(index, 'up')}><span className="sr-only">Up</span></Button>
<Button variant="ghost" size="icon" className="h-6 w-6" onClick={() => moveBlock(index, 'down')}><span className="sr-only">Down</span></Button>
<Button variant="ghost" size="icon" className="h-6 w-6 text-red-500" onClick={() => removeBlock(block.id)}><Trash2 className="h-3 w-3" /></Button>
</div>
<CardContent className="p-6">
{/* Type Label */}
<div className="absolute left-0 top-0 bg-zinc-800 text-zinc-500 text-[10px] uppercase font-bold px-2 py-1 rounded-br opacity-50">{block.block_type}</div>
{/* HERO EDITOR */}
{block.block_type === 'hero' && (
<div className="text-center space-y-4 py-8">
<Input
value={block.block_config.title}
onChange={e => updateBlock(block.id, { title: e.target.value })}
className="text-4xl font-bold bg-transparent border-0 text-center placeholder:text-zinc-700 h-auto focus-visible:ring-0 p-0"
placeholder="Hero Headline"
/>
<Input
value={block.block_config.subtitle}
onChange={e => updateBlock(block.id, { subtitle: e.target.value })}
className="text-xl text-zinc-400 bg-transparent border-0 text-center placeholder:text-zinc-700 h-auto focus-visible:ring-0 p-0"
placeholder="Hero Subtitle"
/>
</div>
)}
{/* CONTENT EDITOR */}
{block.block_type === 'content' && (
<div className="space-y-2">
<Textarea
value={block.block_config.content}
onChange={e => updateBlock(block.id, { content: e.target.value })}
className="min-h-[150px] bg-zinc-950 border-zinc-800 font-serif text-lg leading-relaxed text-zinc-300"
placeholder="Write your HTML content or markdown here..."
/>
</div>
)}
{/* FEATURES EDITOR */}
{block.block_type === 'features' && (
<div className="grid grid-cols-3 gap-4">
{(block.block_config.items || []).map((item: any, i: number) => (
<div key={i} className="p-4 rounded bg-zinc-950 border border-zinc-800 space-y-2">
<Input
value={item.title}
onChange={e => {
const newItems = [...block.block_config.items];
newItems[i].title = e.target.value;
updateBlock(block.id, { items: newItems });
}}
className="font-bold bg-transparent border-0 p-0 h-auto focus-visible:ring-0"
/>
<Textarea
value={item.desc}
onChange={e => {
const newItems = [...block.block_config.items];
newItems[i].desc = e.target.value;
updateBlock(block.id, { items: newItems });
}}
className="text-xs text-zinc-400 bg-transparent border-0 p-0 h-auto resize-none min-h-[40px] focus-visible:ring-0"
/>
</div>
))}
<Button variant="outline" className="h-full border-dashed border-zinc-800 text-zinc-600" onClick={() => {
const newItems = [...(block.block_config.items || []), { title: 'New Feature', desc: 'Desc' }];
updateBlock(block.id, { items: newItems });
}}>
<Plus className="h-4 w-4" />
</Button>
</div>
)}
</CardContent>
</Card>
))}
{blocks.length === 0 && (
<div className="h-64 flex flex-col items-center justify-center border-2 border-dashed border-zinc-800 rounded-lg text-zinc-600">
<LayoutTemplate className="h-12 w-12 mb-4 opacity-20" />
<p>Page is empty.</p>
<p className="text-sm">Use the sidebar to add blocks.</p>
</div>
)}
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,41 @@
import React from 'react';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import SitePagesManager from './SitePagesManager';
import NavigationManager from './NavigationManager';
import ThemeSettings from './ThemeSettings';
import { Card } from '@/components/ui/card';
interface SiteDashboardProps {
siteId: string;
}
export default function SiteDashboard({ siteId }: SiteDashboardProps) {
return (
<Tabs defaultValue="pages" className="space-y-6">
<TabsList className="bg-zinc-900 border border-zinc-800">
<TabsTrigger value="pages" className="data-[state=active]:bg-zinc-800">Pages</TabsTrigger>
<TabsTrigger value="navigation" className="data-[state=active]:bg-zinc-800">Navigation</TabsTrigger>
<TabsTrigger value="appearance" className="data-[state=active]:bg-zinc-800">Appearance</TabsTrigger>
<TabsTrigger value="settings" className="data-[state=active]:bg-zinc-800">Settings</TabsTrigger>
</TabsList>
<TabsContent value="pages" className="space-y-4">
<SitePagesManager siteId={siteId} siteDomain="example.com" />
</TabsContent>
<TabsContent value="navigation">
<NavigationManager siteId={siteId} />
</TabsContent>
<TabsContent value="appearance">
<ThemeSettings siteId={siteId} />
</TabsContent>
<TabsContent value="settings">
<div className="text-zinc-500 p-8 border border-dashed border-zinc-800 rounded-lg text-center">
Advanced site settings coming soon in Milestone 5.
</div>
</TabsContent>
</Tabs>
);
}

View File

@@ -0,0 +1,221 @@
import React, { useState, useEffect } from 'react';
import { getDirectusClient, readItem, updateItem } from '@/lib/directus/client';
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Switch } from '@/components/ui/switch';
import { Badge } from '@/components/ui/badge';
import { Sites as Site } from '@/lib/schemas';
import DomainSetupGuide from '@/components/admin/DomainSetupGuide';
interface SiteEditorProps {
id: string; // Astro passes string params
}
export default function SiteEditor({ id }: SiteEditorProps) {
const [site, setSite] = useState<Site | null>(null);
const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false);
// Feature Flags State (mapped to settings)
const [features, setFeatures] = useState({
maintenance_mode: false,
seo_indexing: true,
https_enforced: true,
analytics_enabled: false,
blog_enabled: true,
leads_capture: true
});
useEffect(() => {
async function load() {
try {
const client = getDirectusClient();
// @ts-ignore
const result = await client.request(readItem('sites', id));
setSite(result as unknown as Site);
// Merge settings into defaults
if (result.settings) {
setFeatures(prev => ({ ...prev, ...(result.settings as Record<string, any>) }));
}
} catch (e) {
console.error(e);
} finally {
setLoading(false);
}
}
if (id) load();
}, [id]);
const handleSave = async () => {
if (!site) return;
setSaving(true);
try {
const client = getDirectusClient();
// @ts-ignore
await client.request(updateItem('sites', id, {
name: site.name,
url: site.url,
status: site.status,
settings: features
}));
// Show toast?
alert("Site Settings Saved!");
} catch (e) {
console.error(e);
alert("Error saving site.");
} finally {
setSaving(false);
}
};
if (loading) return <div>Loading...</div>;
if (!site) return <div>Site not found</div>;
return (
<div className="space-y-6 max-w-4xl">
{/* Header / Meta */}
<Card className="bg-slate-800 border-slate-700">
<CardHeader>
<CardTitle>General Information</CardTitle>
<CardDescription>Basic site identity and connectivity.</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label>Site Name</Label>
<Input
value={site.name}
onChange={(e) => setSite({ ...site, name: e.target.value })}
className="bg-slate-900 border-slate-700"
placeholder="My Awesome Site"
/>
<p className="text-xs text-slate-500">Internal identifier for this site</p>
</div>
<div className="space-y-2">
<Label>Domain</Label>
<Input
value={site.url || ''}
onChange={(e) => setSite({ ...site, url: e.target.value })}
className="bg-slate-900 border-slate-700 font-mono text-blue-400"
placeholder="example.com"
/>
<p className="text-xs text-slate-500">Your custom domain (without https://)</p>
</div>
</div>
</CardContent>
</Card>
{/* Feature Toggles (CheckBox Options) */}
<Card className="bg-slate-800 border-slate-700">
<CardHeader>
<CardTitle>Feature Configuration</CardTitle>
<CardDescription>Enable or disable specific modules and behaviors for this site.</CardDescription>
</CardHeader>
<CardContent>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{/* Maintenance Mode */}
<div className="flex items-center justify-between space-x-2 border p-4 rounded-lg border-slate-700 bg-slate-900/50">
<div className="space-y-0.5">
<Label className="text-base font-medium">Maintenance Mode</Label>
<p className="text-sm text-slate-500">
Show a "Coming Soon" page to all visitors.
</p>
</div>
<Switch
checked={features.maintenance_mode}
onCheckedChange={(c) => setFeatures({ ...features, maintenance_mode: c })}
/>
</div>
{/* SEO Indexing */}
<div className="flex items-center justify-between space-x-2 border p-4 rounded-lg border-slate-700 bg-slate-900/50">
<div className="space-y-0.5">
<Label className="text-base font-medium">Search Indexing</Label>
<p className="text-sm text-slate-500">
Allow Google/Bing to index this site.
</p>
</div>
<Switch
checked={features.seo_indexing}
onCheckedChange={(c) => setFeatures({ ...features, seo_indexing: c })}
/>
</div>
{/* HTTPS Enforced */}
<div className="flex items-center justify-between space-x-2 border p-4 rounded-lg border-slate-700 bg-slate-900/50">
<div className="space-y-0.5">
<Label className="text-base font-medium">Enforce HTTPS</Label>
<p className="text-sm text-slate-500">
Redirect all HTTP traffic to HTTPS.
</p>
</div>
<Switch
checked={features.https_enforced}
onCheckedChange={(c) => setFeatures({ ...features, https_enforced: c })}
/>
</div>
{/* Analytics */}
<div className="flex items-center justify-between space-x-2 border p-4 rounded-lg border-slate-700 bg-slate-900/50">
<div className="space-y-0.5">
<Label className="text-base font-medium">Analytics</Label>
<p className="text-sm text-slate-500">
Inject GTM/GA4 scripts.
</p>
</div>
<Switch
checked={features.analytics_enabled}
onCheckedChange={(c) => setFeatures({ ...features, analytics_enabled: c })}
/>
</div>
{/* Blog */}
<div className="flex items-center justify-between space-x-2 border p-4 rounded-lg border-slate-700 bg-slate-900/50">
<div className="space-y-0.5">
<Label className="text-base font-medium">Blog System</Label>
<p className="text-sm text-slate-500">
Enable generated posts and archive pages.
</p>
</div>
<Switch
checked={features.blog_enabled}
onCheckedChange={(c) => setFeatures({ ...features, blog_enabled: c })}
/>
</div>
{/* Leads */}
<div className="flex items-center justify-between space-x-2 border p-4 rounded-lg border-slate-700 bg-slate-900/50">
<div className="space-y-0.5">
<Label className="text-base font-medium">Lead Capture</Label>
<p className="text-sm text-slate-500">
Process form submissions and webhooks.
</p>
</div>
<Switch
checked={features.leads_capture}
onCheckedChange={(c) => setFeatures({ ...features, leads_capture: c })}
/>
</div>
</div>
</CardContent>
</Card>
{/* Domain Setup Guide */}
<DomainSetupGuide siteDomain={site.url} />
<div className="flex justify-end gap-4">
<Button variant="outline" onClick={() => window.history.back()}>
Cancel
</Button>
<Button onClick={handleSave} disabled={saving} className="bg-green-600 hover:bg-green-700">
{saving ? 'Saving...' : 'Save Configuration'}
</Button>
</div>
</div>
);
}

View File

@@ -0,0 +1,87 @@
import React, { useState, useEffect } from 'react';
import { getDirectusClient, readItems } from '@/lib/directus/client';
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { Sites as Site } from '@/lib/schemas';
export default function SiteList() {
const [sites, setSites] = useState<Site[]>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
async function load() {
try {
const client = getDirectusClient();
// @ts-ignore
const s = await client.request(readItems('sites'));
setSites(s as unknown as Site[]);
} catch (e) {
console.error(e);
} finally {
setLoading(false);
}
}
load();
}, []);
if (loading) return <div className="text-slate-400">Loading sites...</div>;
return (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{sites.map(site => (
<Card key={site.id} className="bg-slate-800 border-slate-700 hover:border-slate-600 transition-all cursor-pointer group" onClick={() => window.location.href = `/admin/sites/${site.id}`}>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium text-slate-200">
{site.name}
</CardTitle>
<Badge className={site.status === 'active' ? 'bg-green-600' : 'bg-slate-600'}>
{site.status}
</Badge>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold text-white mb-2">{site.url || 'No URL set'}</div>
<p className="text-xs text-slate-500 mb-4">
{site.url ? '🟢 Site configured' : '⚠️ Set up site URL'}
</p>
<div className="mt-4 flex gap-2">
<Button
variant="outline"
size="sm"
className="flex-1"
onClick={(e) => {
e.stopPropagation();
window.location.href = `/admin/sites/${site.id}`;
}}
>
Configure
</Button>
<Button
variant="outline"
size="sm"
className="flex-1"
onClick={(e) => {
e.stopPropagation();
if (site.url) {
window.open(`https://${site.url || 'No URL'}`, '_blank');
} else {
alert('Set up a domain first in site settings');
}
}}
>
👁 Preview
</Button>
</div>
</CardContent>
</Card>
))}
{/* Empty State / Add New Placeholder */}
{sites.length === 0 && (
<div className="col-span-full text-center py-12 bg-slate-800/50 rounded-xl border border-dashed border-slate-700">
<p className="text-slate-400">No sites found.</p>
</div>
)}
</div>
);
}

View File

@@ -0,0 +1,166 @@
import React, { useState } from 'react';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { getDirectusClient, readItems, createItem, deleteItem } from '@/lib/directus/client';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
import { Badge } from '@/components/ui/badge';
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from '@/components/ui/dialog';
import { FileText, Plus, Trash2, Edit, ExternalLink } from 'lucide-react';
import { toast } from 'sonner';
const client = getDirectusClient();
interface Page {
id: string;
title: string;
permalink: string;
status: string;
date_updated: string;
}
interface SitePagesManagerProps {
siteId: string;
siteDomain: string;
}
export default function SitePagesManager({ siteId, siteDomain }: SitePagesManagerProps) {
const queryClient = useQueryClient();
const [createOpen, setCreateOpen] = useState(false);
const [newPageTitle, setNewPageTitle] = useState('');
const { data: pages = [], isLoading } = useQuery({
queryKey: ['pages', siteId],
queryFn: async () => {
// @ts-ignore
const res = await client.request(readItems('pages', {
filter: { site: { _eq: siteId } },
sort: ['permalink']
}));
return res as unknown as Page[];
}
});
const createMutation = useMutation({
mutationFn: async () => {
// @ts-ignore
const res = await client.request(createItem('pages', {
title: newPageTitle,
site: siteId, // UUID usually
permalink: `/${newPageTitle.toLowerCase().replace(/ /g, '-')}`,
status: 'draft',
blocks: []
}));
return res;
},
onSuccess: (data: any) => {
queryClient.invalidateQueries({ queryKey: ['pages', siteId] });
toast.success('Page created');
setCreateOpen(false);
setNewPageTitle('');
// Redirect to editor
window.location.href = `/admin/sites/editor/${data.id}`;
},
onError: (e: any) => {
toast.error('Failed to create page: ' + e.message);
}
});
const deleteMutation = useMutation({
mutationFn: async (id: string) => {
// @ts-ignore
await client.request(deleteItem('pages', id));
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['pages', siteId] });
toast.success('Page deleted');
}
});
return (
<div className="space-y-6">
<div className="flex justify-between items-center px-1">
<h3 className="text-lg font-medium text-white flex items-center gap-2">
<FileText className="h-5 w-5 text-blue-400" /> Pages
</h3>
<Button onClick={() => setCreateOpen(true)} className="bg-blue-600 hover:bg-blue-500">
<Plus className="mr-2 h-4 w-4" /> New Page
</Button>
</div>
<div className="rounded-md border border-zinc-800 bg-zinc-900/50 overflow-hidden">
<Table>
<TableHeader className="bg-zinc-950">
<TableRow className="border-zinc-800 hover:bg-zinc-950">
<TableHead className="text-zinc-400">Title</TableHead>
<TableHead className="text-zinc-400">Permalink</TableHead>
<TableHead className="text-zinc-400">Status</TableHead>
<TableHead className="text-zinc-400 text-right">Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{pages.length === 0 ? (
<TableRow>
<TableCell colSpan={4} className="h-24 text-center text-zinc-500">
No pages yet.
</TableCell>
</TableRow>
) : (
pages.map((page) => (
<TableRow key={page.id} className="border-zinc-800 hover:bg-zinc-900/50">
<TableCell className="font-medium text-white">
{page.title}
</TableCell>
<TableCell className="text-zinc-400 font-mono text-xs">
{page.permalink}
</TableCell>
<TableCell>
<Badge variant="outline" className={page.status === 'published' ? 'bg-green-500/10 text-green-500 border-green-500/20' : 'bg-zinc-500/10 text-zinc-500'}>
{page.status}
</Badge>
</TableCell>
<TableCell className="text-right">
<div className="flex justify-end gap-2">
<Button variant="ghost" size="icon" className="h-8 w-8 text-zinc-400 hover:text-white" onClick={() => window.location.href = `/admin/sites/editor/${page.id}`}>
<Edit className="h-4 w-4" />
</Button>
<Button variant="ghost" size="icon" className="h-8 w-8 text-zinc-400 hover:text-red-500" onClick={() => { if (confirm('Delete page?')) deleteMutation.mutate(page.id); }}>
<Trash2 className="h-4 w-4" />
</Button>
{page.status === 'published' && (
<Button variant="ghost" size="icon" className="h-8 w-8 text-blue-400 hover:text-blue-300" onClick={() => window.open(`https://${siteDomain}${page.permalink}`, '_blank')}>
<ExternalLink className="h-4 w-4" />
</Button>
)}
</div>
</TableCell>
</TableRow>
))
)}
</TableBody>
</Table>
</div>
<Dialog open={createOpen} onOpenChange={setCreateOpen}>
<DialogContent className="bg-zinc-900 border-zinc-800 text-white">
<DialogHeader>
<DialogTitle>Create New Page</DialogTitle>
</DialogHeader>
<div className="py-4">
<label className="text-xs uppercase font-bold text-zinc-500 mb-2 block">Page Title</label>
<Input
value={newPageTitle}
onChange={e => setNewPageTitle(e.target.value)}
placeholder="e.g. About Us"
className="bg-zinc-950 border-zinc-800"
/>
</div>
<DialogFooter>
<Button variant="ghost" onClick={() => setCreateOpen(false)}>Cancel</Button>
<Button onClick={() => createMutation.mutate()} disabled={!newPageTitle} className="bg-blue-600 hover:bg-blue-500">Create & Edit</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
);
}

View File

@@ -0,0 +1,178 @@
import React, { useState } from 'react';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { getDirectusClient, readItems, createItem, updateItem, deleteItem } from '@/lib/directus/client';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Badge } from '@/components/ui/badge';
import { Card, CardHeader, CardTitle, CardContent, CardFooter } from '@/components/ui/card';
import {
Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, DialogTrigger
} from '@/components/ui/dialog';
import { Globe, Plus, Settings, Trash2, ExternalLink } from 'lucide-react';
import { toast } from 'sonner';
const client = getDirectusClient();
interface Site {
id: string;
name: string;
url: string;
status: 'active' | 'inactive';
settings?: any;
}
export default function SitesManager() {
const queryClient = useQueryClient();
const [editorOpen, setEditorOpen] = useState(false);
const [editingSite, setEditingSite] = useState<Partial<Site>>({});
// Fetch
const { data: sites = [], isLoading } = useQuery({
queryKey: ['sites'],
queryFn: async () => {
// @ts-ignore
const res = await client.request(readItems('sites', { limit: -1 }));
return res as unknown as Site[];
}
});
// Mutations
const mutation = useMutation({
mutationFn: async (site: Partial<Site>) => {
if (site.id) {
// @ts-ignore
await client.request(updateItem('sites', site.id, site));
} else {
// @ts-ignore
await client.request(createItem('sites', site));
}
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['sites'] });
toast.success(editingSite.id ? 'Site updated' : 'Site created');
setEditorOpen(false);
}
});
const deleteMutation = useMutation({
mutationFn: async (id: string) => {
// @ts-ignore
await client.request(deleteItem('sites', id));
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['sites'] });
toast.success('Site deleted');
}
});
return (
<div className="space-y-6">
<div className="flex justify-between items-center bg-zinc-900/50 p-6 rounded-lg border border-zinc-800 backdrop-blur-sm">
<div>
<h2 className="text-xl font-bold text-white">Your Sites</h2>
<p className="text-zinc-400 text-sm">Manage your deployed web properties.</p>
</div>
<Button className="bg-blue-600 hover:bg-blue-500" onClick={() => { setEditingSite({}); setEditorOpen(true); }}>
<Plus className="mr-2 h-4 w-4" /> Add Site
</Button>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{sites.map((site) => (
<Card key={site.id} className="bg-zinc-900 border-zinc-800 hover:border-zinc-700 transition-colors group">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium text-zinc-200">
{site.name}
</CardTitle>
<Badge variant={site.status === 'active' ? 'default' : 'secondary'} className={site.status === 'active' ? 'bg-green-500/10 text-green-500 hover:bg-green-500/20' : ''}>
{site.status || 'inactive'}
</Badge>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold truncate text-white tracking-tight">{site.url}</div>
<p className="text-xs text-zinc-500 mt-1 flex items-center">
<Globe className="h-3 w-3 mr-1" />
deployed via Launchpad
</p>
</CardContent>
<CardFooter className="flex justify-between border-t border-zinc-800 pt-4">
<Button variant="ghost" size="sm" className="text-zinc-400 hover:text-white" onClick={() => window.open(`https://${site.url}`, '_blank')}>
<ExternalLink className="h-4 w-4 mr-2" /> Visit
</Button>
<div className="flex gap-2">
<Button variant="outline" size="sm" className="bg-zinc-800 border-zinc-700 hover:bg-zinc-700 text-zinc-300" onClick={() => window.location.href = `/admin/sites/${site.id}`}>
Manage Content
</Button>
<Button variant="outline" size="sm" className="bg-purple-900/30 border-purple-700 hover:bg-purple-800/40 text-purple-300" onClick={() => window.open(`/preview/site/${site.id}`, '_blank')}>
👁 Preview
</Button>
</div>
<div className="flex gap-1">
<Button variant="ghost" size="icon" className="h-8 w-8 text-zinc-400 hover:text-white" onClick={() => { setEditingSite(site); setEditorOpen(true); }}>
<Settings className="h-4 w-4" />
</Button>
<Button variant="ghost" size="icon" className="h-8 w-8 text-zinc-400 hover:text-red-500" onClick={() => { if (confirm('Delete site?')) deleteMutation.mutate(site.id); }}>
<Trash2 className="h-4 w-4" />
</Button>
</div>
</CardFooter>
</Card>
))}
{sites.length === 0 && (
<div className="col-span-full h-64 flex flex-col items-center justify-center border-2 border-dashed border-zinc-800 rounded-lg text-zinc-500">
<Globe className="h-10 w-10 mb-4 opacity-20" />
<p>No sites configured yet.</p>
<Button variant="link" onClick={() => { setEditingSite({}); setEditorOpen(true); }}>Create your first site</Button>
</div>
)}
</div>
<Dialog open={editorOpen} onOpenChange={setEditorOpen}>
<DialogContent className="bg-zinc-900 border-zinc-800 text-white">
<DialogHeader>
<DialogTitle>{editingSite.id ? 'Edit Site' : 'New Site'}</DialogTitle>
</DialogHeader>
<div className="space-y-4 py-4">
<div className="space-y-2">
<label className="text-xs uppercase font-bold text-zinc-500">Site Name</label>
<Input
value={editingSite.name || ''}
onChange={e => setEditingSite({ ...editingSite, name: e.target.value })}
placeholder="My Awesome Blog"
className="bg-zinc-950 border-zinc-800"
/>
</div>
<div className="space-y-2">
<label className="text-xs uppercase font-bold text-zinc-500">Domain</label>
<div className="flex">
<span className="inline-flex items-center px-3 rounded-l-md border border-r-0 border-zinc-800 bg-zinc-900 text-zinc-500 text-sm">https://</span>
<Input
value={editingSite.url || ''}
onChange={e => setEditingSite({ ...editingSite, url: e.target.value })}
placeholder="example.com"
className="rounded-l-none bg-zinc-950 border-zinc-800"
/>
</div>
</div>
<div className="space-y-2">
<label className="text-xs uppercase font-bold text-zinc-500">Status</label>
<select
className="flex h-9 w-full rounded-md border border-zinc-800 bg-zinc-950 px-3 py-1 text-sm shadow-sm transition-colors text-white"
value={editingSite.status || 'active'}
onChange={e => setEditingSite({ ...editingSite, status: e.target.value as any })}
>
<option value="active">Active</option>
<option value="inactive">Inactive</option>
</select>
</div>
</div>
<DialogFooter>
<Button variant="ghost" onClick={() => setEditorOpen(false)}>Cancel</Button>
<Button onClick={() => mutation.mutate(editingSite)} className="bg-blue-600 hover:bg-blue-500">Save Site</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
);
}

View File

@@ -0,0 +1,116 @@
import React, { useState, useEffect } from 'react';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { getDirectusClient, readItems, createItem, updateItem } from '@/lib/directus/client';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Textarea } from '@/components/ui/textarea';
import { Save, Palette, Type } from 'lucide-react';
import { toast } from 'sonner';
const client = getDirectusClient();
interface GlobalSettings {
id?: string;
site: string;
primary_color: string;
secondary_color: string;
footer_text: string;
}
interface ThemeSettingsProps {
siteId: string;
}
export default function ThemeSettings({ siteId }: ThemeSettingsProps) {
const queryClient = useQueryClient();
const [settings, setSettings] = useState<Partial<GlobalSettings>>({});
const { data: globalRecord, isLoading } = useQuery({
queryKey: ['globals', siteId],
queryFn: async () => {
// @ts-ignore
const res = await client.request(readItems('globals', {
filter: { site: { _eq: siteId } },
limit: 1
}));
const record = res[0] as GlobalSettings;
if (record) setSettings(record);
return record;
}
});
const saveMutation = useMutation({
mutationFn: async () => {
if (globalRecord?.id) {
// @ts-ignore
await client.request(updateItem('globals', globalRecord.id, settings));
} else {
// @ts-ignore
await client.request(createItem('globals', { ...settings, site: siteId }));
}
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['globals', siteId] });
toast.success('Theme settings saved');
}
});
if (isLoading) return <div className="text-zinc-500">Loading settings...</div>;
return (
<div className="max-w-2xl space-y-8">
<div className="space-y-4">
<h3 className="text-lg font-medium text-white flex items-center gap-2">
<Palette className="h-5 w-5 text-purple-400" /> Colors
</h3>
<div className="grid grid-cols-2 gap-6">
<div className="space-y-2">
<label className="text-xs uppercase font-bold text-zinc-500">Primary Color</label>
<div className="flex gap-2">
<div className="w-10 h-10 rounded border border-zinc-700" style={{ backgroundColor: settings.primary_color || '#000000' }}></div>
<Input
value={settings.primary_color || ''}
onChange={e => setSettings({ ...settings, primary_color: e.target.value })}
placeholder="#000000"
className="bg-zinc-950 border-zinc-800 font-mono"
/>
</div>
</div>
<div className="space-y-2">
<label className="text-xs uppercase font-bold text-zinc-500">Secondary Color</label>
<div className="flex gap-2">
<div className="w-10 h-10 rounded border border-zinc-700" style={{ backgroundColor: settings.secondary_color || '#ffffff' }}></div>
<Input
value={settings.secondary_color || ''}
onChange={e => setSettings({ ...settings, secondary_color: e.target.value })}
placeholder="#ffffff"
className="bg-zinc-950 border-zinc-800 font-mono"
/>
</div>
</div>
</div>
</div>
<div className="space-y-4 pt-4 border-t border-zinc-800">
<h3 className="text-lg font-medium text-white flex items-center gap-2">
<Type className="h-5 w-5 text-blue-400" /> Typography & Text
</h3>
<div className="space-y-2">
<label className="text-xs uppercase font-bold text-zinc-500">Footer Text</label>
<Textarea
value={settings.footer_text || ''}
onChange={e => setSettings({ ...settings, footer_text: e.target.value })}
className="bg-zinc-950 border-zinc-800 min-h-[100px]"
placeholder="© 2024 My Company. All rights reserved."
/>
</div>
</div>
<div className="pt-6">
<Button onClick={() => saveMutation.mutate()} className="bg-blue-600 hover:bg-blue-500 w-full md:w-auto">
<Save className="mr-2 h-4 w-4" /> Save Changes
</Button>
</div>
</div>
);
}

View File

@@ -0,0 +1,178 @@
import React, { useEffect, useState } from 'react';
import { useStore } from '@nanostores/react';
import { debugIsOpen, activeTab, logs, type LogEntry } from '../../stores/debugStore';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { getDirectusClient } from '@/lib/directus/client';
// Create a client for the devtools if one doesn't exist in context
// (Ideally this component is inside the main QueryClientProvider, but we'll see)
const queryClient = new QueryClient();
export default function DebugToolbar() {
const isOpen = useStore(debugIsOpen);
const currentTab = useStore(activeTab);
const logEntries = useStore(logs);
const [backendStatus, setBackendStatus] = useState<'checking' | 'online' | 'error'>('checking');
const [latency, setLatency] = useState<number | null>(null);
useEffect(() => {
if (isOpen && currentTab === 'backend') {
checkBackend();
}
}, [isOpen, currentTab]);
const checkBackend = async () => {
setBackendStatus('checking');
const start = performance.now();
try {
const client = getDirectusClient();
await client.request(() => ({
path: '/server/ping',
method: 'GET'
}));
setLatency(Math.round(performance.now() - start));
setBackendStatus('online');
} catch (e) {
setBackendStatus('error');
}
};
if (!isOpen) {
return (
<button
onClick={() => debugIsOpen.set(true)}
className="fixed bottom-4 right-4 z-[9999] p-3 bg-black text-white rounded-full shadow-2xl hover:scale-110 transition-transform border border-gray-700"
title="Open Debug Toolbar"
>
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4" />
</svg>
</button>
);
}
return (
<div className="fixed bottom-0 left-0 right-0 h-[33vh] z-[9999] bg-black/95 text-white border-t border-gray-800 shadow-[0_-4px_20px_rgba(0,0,0,0.5)] flex flex-col font-mono text-sm backdrop-blur">
{/* Header */}
<div className="flex items-center justify-between px-4 py-2 border-b border-gray-800 bg-gray-900/50">
<div className="flex items-center gap-4">
<span className="font-bold text-yellow-500"> Spark Debug</span>
<div className="flex gap-1 bg-gray-800 rounded p-1">
{(['console', 'backend', 'network'] as const).map(tab => (
<button
key={tab}
onClick={() => activeTab.set(tab)}
className={`px-3 py-1 rounded text-xs uppercase font-medium transition-colors ${currentTab === tab
? 'bg-gray-700 text-white'
: 'text-gray-400 hover:text-white'
}`}
>
{tab}
</button>
))}
</div>
</div>
<button
onClick={() => debugIsOpen.set(false)}
className="p-1 hover:bg-gray-800 rounded"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
{/* Content */}
<div className="flex-1 overflow-hidden relative">
{/* Console Tab */}
{currentTab === 'console' && (
<div className="h-full overflow-y-auto p-4 space-y-1">
{logEntries.length === 0 && (
<div className="text-gray-500 text-center mt-10">No logs captured yet...</div>
)}
{logEntries.map((log) => (
<div key={log.id} className="flex gap-2 font-mono text-xs border-b border-gray-800/50 pb-1">
<span className="text-gray-500 shrink-0">[{log.timestamp}]</span>
<span className={`shrink-0 w-12 font-bold uppercase ${log.type === 'error' ? 'text-red-500' :
log.type === 'warn' ? 'text-yellow-500' :
'text-blue-400'
}`}>
{log.type}
</span>
<span className="text-gray-300 break-all">
{log.messages.join(' ')}
</span>
</div>
))}
<div className="absolute bottom-4 right-4">
<button
onClick={() => logs.set([])}
className="px-2 py-1 bg-gray-800 text-xs rounded hover:bg-gray-700"
>
Clear
</button>
</div>
</div>
)}
{/* Backend Tab */}
{currentTab === 'backend' && (
<div className="h-full p-6 flex flex-col items-center justify-center gap-4">
<div className={`text-4xl ${backendStatus === 'online' ? 'text-green-500' :
backendStatus === 'error' ? 'text-red-500' :
'text-yellow-500 animate-pulse'
}`}>
{backendStatus === 'online' ? '● Online' :
backendStatus === 'error' ? '✖ Error' : '● Checking...'}
</div>
<div className="text-center space-y-2">
<p className="text-gray-400">
Directus URL: <span className="text-white">{import.meta.env.PUBLIC_DIRECTUS_URL}</span>
</p>
{latency && (
<p className="text-gray-400">
Latency: <span className="text-white">{latency}ms</span>
</p>
)}
</div>
<button
onClick={checkBackend}
className="px-4 py-2 bg-gray-800 rounded hover:bg-gray-700 transition"
>
Re-check Connection
</button>
</div>
)}
{/* Network / React Query Tab */}
{currentTab === 'network' && (
<div className="h-full w-full relative bg-gray-900">
<div className="absolute inset-0 flex items-center justify-center text-gray-500">
{/*
React Query Devtools needs a QueryClientProvider context.
In Astro, components are islands. If this island doesn't share context with the main app
(which it likely won't if they are separate roots), we might see empty devtools.
However, putting it here is the best attempt.
*/}
<div className="text-center">
<p className="mb-2">React Query Devtools</p>
<p className="text-xs">
(If empty, data fetching might be happening Server-Side or in a different Context)
</p>
</div>
</div>
{/* We force mount devtools panel here if possible */}
<QueryClientProvider client={queryClient}>
<ReactQueryDevtools initialIsOpen={true} />
</QueryClientProvider>
</div>
)}
</div>
</div>
);
}

View File

@@ -0,0 +1,39 @@
import React from 'react';
import Hero from './blocks/Hero';
import Content from './blocks/Content';
import Features from './blocks/Features';
interface Block {
id: string;
block_type: string;
block_config: any;
}
interface BlockRendererProps {
blocks: Block[];
}
export default function BlockRenderer({ blocks }: BlockRendererProps) {
if (!blocks || !Array.isArray(blocks)) return null;
return (
<div className="flex flex-col">
{blocks.map(block => {
switch (block.block_type) {
case 'hero':
return <Hero key={block.id} {...block.block_config} />;
case 'content':
return <Content key={block.id} {...block.block_config} />;
case 'features':
return <Features key={block.id} {...block.block_config} />;
case 'cta':
// reuse Hero styled as CTA or simple banner
return <Hero key={block.id} {...block.block_config} bg="dark" />;
default:
console.warn(`Unknown block type: ${block.block_type}`);
return null;
}
})}
</div>
);
}

View File

@@ -0,0 +1,13 @@
import React from 'react';
interface ContentProps {
content: string;
}
export default function Content({ content }: ContentProps) {
return (
<section className="py-12 px-8">
<div className="prose prose-lg dark:prose-invert mx-auto" dangerouslySetInnerHTML={{ __html: content }} />
</section>
);
}

View File

@@ -0,0 +1,40 @@
import React from 'react';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { CheckCircle2 } from 'lucide-react';
interface FeatureItem {
title: string;
desc: string;
icon?: string;
}
interface FeaturesProps {
items: FeatureItem[];
layout?: 'grid' | 'list';
}
export default function Features({ items, layout = 'grid' }: FeaturesProps) {
return (
<section className="py-16 px-8 bg-zinc-50 dark:bg-zinc-900/50">
<div className="max-w-6xl mx-auto">
<div className={`grid gap-8 ${layout === 'list' ? 'grid-cols-1' : 'grid-cols-1 md:grid-cols-3'}`}>
{items?.map((item, i) => (
<Card key={i} className="border-0 shadow-lg bg-white dark:bg-zinc-900 dark:border-zinc-800">
<CardHeader className="pb-2">
<div className="w-12 h-12 rounded-full bg-blue-100 dark:bg-blue-900/30 flex items-center justify-center mb-4 text-blue-600 dark:text-blue-400">
<CheckCircle2 className="h-6 w-6" />
</div>
<CardTitle className="text-xl">{item.title}</CardTitle>
</CardHeader>
<CardContent>
<p className="text-zinc-600 dark:text-zinc-400 leading-relaxed">
{item.desc}
</p>
</CardContent>
</Card>
))}
</div>
</div>
</section>
);
}

View File

@@ -0,0 +1,38 @@
import React from 'react';
import { Button } from '@/components/ui/button';
interface HeroProps {
title: string;
subtitle?: string;
bg?: string;
ctaLabel?: string;
ctaUrl?: string;
}
export default function Hero({ title, subtitle, bg, ctaLabel, ctaUrl }: HeroProps) {
const bgClass = bg === 'dark' ? 'bg-zinc-900 text-white' :
bg === 'image' ? 'bg-zinc-800 text-white' : // Placeholder for image logic
'bg-white text-zinc-900';
return (
<section className={`py-20 px-8 text-center ${bgClass}`}>
<div className="max-w-4xl mx-auto space-y-6">
<h1 className="text-5xl md:text-6xl font-extrabold tracking-tight">
{title}
</h1>
{subtitle && (
<p className="text-xl md:text-2xl opacity-80 max-w-2xl mx-auto">
{subtitle}
</p>
)}
{(ctaLabel && ctaUrl) && (
<div className="pt-4">
<Button asChild size="lg" className="text-lg px-8 py-6 rounded-full">
<a href={ctaUrl}>{ctaLabel}</a>
</Button>
</div>
)}
</div>
</section>
);
}

View File

@@ -0,0 +1,21 @@
import React from 'react';
import { QueryClientProvider } from '@tanstack/react-query';
import { Toaster as SonnerToaster } from 'sonner';
import { queryClient } from '@/lib/react-query';
interface CoreProviderProps {
children: React.ReactNode;
}
export const CoreProvider = ({ children }: CoreProviderProps) => {
return (
<QueryClientProvider client={queryClient}>
{children}
<SonnerToaster position="top-right" theme="system" richColors closeButton />
</QueryClientProvider>
);
};
export const GlobalToaster = () => {
return <SonnerToaster position="top-right" theme="system" richColors closeButton />;
}

View File

View File

View File

View File

View File

@@ -0,0 +1,109 @@
import React, { useState } from 'react';
import { Card } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Textarea } from "@/components/ui/textarea";
import { Input } from "@/components/ui/input";
import { Progress } from "@/components/ui/progress";
import { CheckCircle2, AlertTriangle, XCircle, Search, FileText } from 'lucide-react';
// We import the analysis functions directly since this is a client component in Astro/React
import { analyzeSeo, analyzeReadability } from '@/lib/testing/seo';
const TestRunner = () => {
const [content, setContent] = useState('');
const [keyword, setKeyword] = useState('');
const [results, setResults] = useState<any>(null);
const runTests = () => {
const seo = analyzeSeo(content, keyword);
const read = analyzeReadability(content);
setResults({ seo, read });
};
return (
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 h-[calc(100vh-140px)]">
{/* Input Column */}
<div className="flex flex-col gap-4">
<Card className="p-4 space-y-4 bg-card/50 backdrop-blur">
<h3 className="font-semibold text-sm flex items-center gap-2">
<FileText className="h-4 w-4" /> Content Source
</h3>
<div className="space-y-2">
<Input
placeholder="Target Keyword"
value={keyword}
onChange={(e) => setKeyword(e.target.value)}
/>
<Textarea
className="min-h-[400px] font-mono text-sm"
placeholder="Paste content here to analyze..."
value={content}
onChange={(e) => setContent(e.target.value)}
/>
</div>
<Button onClick={runTests} className="w-full">
<Search className="h-4 w-4 mr-2" /> Run Analysis
</Button>
</Card>
</div>
{/* Results Column */}
<div className="flex flex-col gap-4 overflow-y-auto">
{results ? (
<>
<Card className="p-6 bg-card/50 backdrop-blur space-y-6">
<div>
<div className="flex justify-between mb-2">
<h3 className="font-semibold">SEO Score</h3>
<span className={`font-bold ${results.seo.score >= 80 ? 'text-green-500' : 'text-yellow-500'}`}>
{results.seo.score}/100
</span>
</div>
<Progress value={results.seo.score} className="h-2" />
<div className="mt-4 space-y-2">
{results.seo.issues.length === 0 && (
<div className="flex items-center gap-2 text-green-500 text-sm">
<CheckCircle2 className="h-4 w-4" /> No issues found!
</div>
)}
{results.seo.issues.map((issue: string, i: number) => (
<div key={i} className="flex items-start gap-2 text-yellow-500 text-sm">
<AlertTriangle className="h-4 w-4 shrink-0 mt-0.5" />
<span>{issue}</span>
</div>
))}
</div>
</div>
<div className="pt-6 border-t border-border/50">
<div className="flex justify-between mb-2">
<h3 className="font-semibold">Readability</h3>
<span className="text-muted-foreground text-sm">{results.read.feedback}</span>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="p-3 bg-background/50 rounded border border-border/50 text-center">
<div className="text-2xl font-bold">{results.read.gradeLevel}</div>
<div className="text-xs text-muted-foreground">Grade Level</div>
</div>
<div className="p-3 bg-background/50 rounded border border-border/50 text-center">
<div className="text-2xl font-bold">{results.read.score}</div>
<div className="text-xs text-muted-foreground">Flow Score</div>
</div>
</div>
</div>
</Card>
</>
) : (
<Card className="flex-1 flex flex-col items-center justify-center p-8 text-muted-foreground opacity-50 border-dashed">
<Search className="h-12 w-12 mb-4" />
<p>No results yet. Run analysis to see scores.</p>
</Card>
)}
</div>
</div>
);
};
export default TestRunner;

View File

@@ -0,0 +1,29 @@
import React from 'react';
import { Construction } from 'lucide-react';
import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
interface UnderConstructionProps {
title: string;
description?: string;
eta?: string;
}
const UnderConstruction = ({ title, description = "This module is currently being built.", eta = "Coming Soon" }: UnderConstructionProps) => {
return (
<Card className="border-dashed border-2 border-border/50 bg-card/20 backdrop-blur-sm h-[400px] flex flex-col items-center justify-center text-center p-8">
<div className="p-4 rounded-full bg-primary/10 mb-6 animate-pulse">
<Construction className="h-12 w-12 text-primary" />
</div>
<h2 className="text-2xl font-bold mb-2">{title}</h2>
<p className="text-muted-foreground max-w-md mb-6">
{description}
</p>
<Badge variant="outline" className="px-4 py-1 border-primary/20 text-primary">
{eta}
</Badge>
</Card>
);
};
export default UnderConstruction;

View File

@@ -0,0 +1,43 @@
import * as React from "react"
const AlertDialog = ({ open, onOpenChange, children }: any) => {
if (!open) return null
return (
<div className="fixed inset-0 z-50 flex items-center justify-center">
<div className="fixed inset-0 bg-black/50" onClick={() => onOpenChange(false)} />
<div className="relative z-50">{children}</div>
</div>
)
}
const AlertDialogContent = ({ children, className }: any) => (
<div className={`bg-slate-800 rounded-lg shadow-lg max-w-md w-full p-6 ${className}`}>
{children}
</div>
)
const AlertDialogHeader = ({ children }: any) => <div className="mb-4">{children}</div>
const AlertDialogTitle = ({ children, className }: any) => <h2 className={`text-xl font-bold ${className}`}>{children}</h2>
const AlertDialogDescription = ({ children, className }: any) => <p className={`text-sm text-slate-400 ${className}`}>{children}</p>
const AlertDialogFooter = ({ children }: any) => <div className="mt-6 flex justify-end gap-2">{children}</div>
const AlertDialogAction = ({ children, onClick, disabled, className }: any) => (
<button onClick={onClick} disabled={disabled} className={`px-4 py-2 rounded ${className}`}>
{children}
</button>
)
const AlertDialogCancel = ({ children, disabled, className }: any) => (
<button disabled={disabled} className={`px-4 py-2 rounded ${className}`}>
{children}
</button>
)
export {
AlertDialog,
AlertDialogContent,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogAction,
AlertDialogCancel,
}

View File

@@ -0,0 +1,35 @@
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const badgeVariants = cva(
"inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
{
variants: {
variant: {
default:
"border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80",
secondary:
"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
destructive:
"border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80",
outline: "text-foreground",
},
},
defaultVariants: {
variant: "default",
},
}
)
export interface BadgeProps
extends React.HTMLAttributes<HTMLDivElement>,
VariantProps<typeof badgeVariants> { }
function Badge({ className, variant, ...props }: BadgeProps) {
return (
<div className={cn(badgeVariants({ variant }), className)} {...props} />
)
}
export { Badge, badgeVariants }

View File

@@ -0,0 +1,55 @@
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const buttonVariants = cva(
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
destructive:
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
outline:
"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
secondary:
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-10 px-4 py-2",
sm: "h-9 rounded-md px-3",
lg: "h-11 rounded-md px-8",
icon: "h-10 w-10",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button"
return (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
/>
)
}
)
Button.displayName = "Button"
export { Button, buttonVariants }

View File

@@ -0,0 +1,78 @@
import * as React from "react"
import { cn } from "@/lib/utils"
const Card = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn(
"rounded-lg border bg-card text-card-foreground shadow-sm",
className
)}
{...props}
/>
))
Card.displayName = "Card"
const CardHeader = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex flex-col space-y-1.5 p-6", className)}
{...props}
/>
))
CardHeader.displayName = "CardHeader"
const CardTitle = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLHeadingElement>
>(({ className, ...props }, ref) => (
<h3
ref={ref}
className={cn(
"text-2xl font-semibold leading-none tracking-tight",
className
)}
{...props}
/>
))
CardTitle.displayName = "CardTitle"
const CardDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => (
<p
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))
CardDescription.displayName = "CardDescription"
const CardContent = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
))
CardContent.displayName = "CardContent"
const CardFooter = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex items-center p-6 pt-0", className)}
{...props}
/>
))
CardFooter.displayName = "CardFooter"
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }

View File

@@ -0,0 +1,20 @@
import * as React from "react"
import { cn } from "@/lib/utils"
const Checkbox = React.forwardRef<HTMLInputElement, React.InputHTMLAttributes<HTMLInputElement>>(
({ className, ...props }, ref) => (
<input
type="checkbox"
className={cn(
"peer h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground",
className
)}
ref={ref}
{...props}
/>
)
)
Checkbox.displayName = "Checkbox"
export { Checkbox }

View File

@@ -0,0 +1,31 @@
import * as React from "react"
const Dialog = ({ open, onOpenChange, children }: any) => {
if (!open) return null
return (
<div className="fixed inset-0 z-50 flex items-start justify-center pt-24">
<div className="fixed inset-0 bg-black/80 backdrop-blur-sm" onClick={() => onOpenChange(false)} />
<div className="relative z-50 animate-in fade-in zoom-in-95 duration-200">{children}</div>
</div>
)
}
const DialogTrigger = ({ children, asChild, onClick, ...props }: any) => {
// This is a simplified trigger that just renders children.
// In a real implementation (Radix UI), this controls the dialog state.
// For now, we rely on the parent controlling 'open' state.
return <div onClick={onClick} {...props}>{children}</div>
}
const DialogContent = ({ children, className }: any) => (
<div className={`bg-zinc-900 border border-zinc-800 rounded-lg shadow-2xl max-w-lg w-full p-6 ${className}`}>
{children}
</div>
)
const DialogHeader = ({ children }: any) => <div className="mb-4 text-left">{children}</div>
const DialogTitle = ({ children, className }: any) => <h2 className={`text-xl font-bold bg-clip-text text-transparent bg-gradient-to-r from-white to-zinc-400 ${className}`}>{children}</h2>
const DialogDescription = ({ children, className }: any) => <p className={`text-sm text-zinc-400 ${className}`}>{children}</p>
const DialogFooter = ({ children }: any) => <div className="mt-6 flex justify-end gap-2">{children}</div>
export { Dialog, DialogContent, DialogTrigger, DialogHeader, DialogTitle, DialogDescription, DialogFooter }

View File

@@ -0,0 +1,200 @@
"use client"
import * as React from "react"
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
import { Check, ChevronRight, Circle } from "lucide-react"
import { cn } from "@/lib/utils"
const DropdownMenu = DropdownMenuPrimitive.Root
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
const DropdownMenuGroup = DropdownMenuPrimitive.Group
const DropdownMenuPortal = DropdownMenuPrimitive.Portal
const DropdownMenuSub = DropdownMenuPrimitive.Sub
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
const DropdownMenuSubTrigger = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
inset?: boolean
}
>(({ className, inset, children, ...props }, ref) => (
<DropdownMenuPrimitive.SubTrigger
ref={ref}
className={cn(
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent",
inset && "pl-8",
className
)}
{...props}
>
{children}
<ChevronRight className="ml-auto h-4 w-4" />
</DropdownMenuPrimitive.SubTrigger>
))
DropdownMenuSubTrigger.displayName =
DropdownMenuPrimitive.SubTrigger.displayName
const DropdownMenuSubContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
>(({ className, ...props }, ref) => (
<DropdownMenuPrimitive.SubContent
ref={ref}
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className
)}
{...props}
/>
))
DropdownMenuSubContent.displayName =
DropdownMenuPrimitive.SubContent.displayName
const DropdownMenuContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
>(({ className, sideOffset = 4, ...props }, ref) => (
<DropdownMenuPrimitive.Portal>
<DropdownMenuPrimitive.Content
ref={ref}
sideOffset={sideOffset}
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className
)}
{...props}
/>
</DropdownMenuPrimitive.Portal>
))
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
const DropdownMenuItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
inset?: boolean
}
>(({ className, inset, ...props }, ref) => (
<DropdownMenuPrimitive.Item
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
inset && "pl-8",
className
)}
{...props}
/>
))
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
const DropdownMenuCheckboxItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
>(({ className, children, checked, ...props }, ref) => (
<DropdownMenuPrimitive.CheckboxItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
checked={checked}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<Check className="h-4 w-4" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.CheckboxItem>
))
DropdownMenuCheckboxItem.displayName =
DropdownMenuPrimitive.CheckboxItem.displayName
const DropdownMenuRadioItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
>(({ className, children, ...props }, ref) => (
<DropdownMenuPrimitive.RadioItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<Circle className="h-2 w-2 fill-current" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.RadioItem>
))
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
const DropdownMenuLabel = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
inset?: boolean
}
>(({ className, inset, ...props }, ref) => (
<DropdownMenuPrimitive.Label
ref={ref}
className={cn(
"px-2 py-1.5 text-sm font-semibold",
inset && "pl-8",
className
)}
{...props}
/>
))
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
const DropdownMenuSeparator = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
>(({ className, ...props }, ref) => (
<DropdownMenuPrimitive.Separator
ref={ref}
className={cn("-mx-1 my-1 h-px bg-muted", className)}
{...props}
/>
))
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
const DropdownMenuShortcut = ({
className,
...props
}: React.HTMLAttributes<HTMLSpanElement>) => {
return (
<span
className={cn("ml-auto text-xs tracking-widest opacity-60", className)}
{...props}
/>
)
}
DropdownMenuShortcut.displayName = "DropdownMenuShortcut"
export {
DropdownMenu,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuCheckboxItem,
DropdownMenuRadioItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuGroup,
DropdownMenuPortal,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuRadioGroup,
}

View File

@@ -0,0 +1,22 @@
import * as React from "react"
import { cn } from "@/lib/utils"
const Input = React.forwardRef<HTMLInputElement, React.InputHTMLAttributes<HTMLInputElement>>(
({ className, type, ...props }, ref) => {
return (
<input
type={type}
className={cn(
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
className
)}
ref={ref}
{...props}
/>
)
}
)
Input.displayName = "Input"
export { Input }

View File

@@ -0,0 +1,12 @@
import * as React from "react"
import { cn } from "@/lib/utils"
const Label = React.forwardRef<HTMLLabelElement, React.LabelHTMLAttributes<HTMLLabelElement>>(
({ className, ...props }, ref) => (
<label ref={ref} className={cn("text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70", className)} {...props} />
)
)
Label.displayName = "Label"
export { Label }

View File

@@ -0,0 +1,14 @@
import * as React from "react"
import { cn } from "@/lib/utils"
// Simplified Progress without radix for speed, or if radix is missing
const Progress = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement> & { value?: number }>(
({ className, value, ...props }, ref) => (
<div ref={ref} className={cn("relative h-4 w-full overflow-hidden rounded-full bg-secondary", className)} {...props}>
<div className="h-full w-full flex-1 bg-primary transition-all" style={{ transform: `translateX(-${100 - (value || 0)}%)` }} />
</div>
)
)
Progress.displayName = "Progress"
export { Progress }

View File

@@ -0,0 +1,20 @@
import * as React from "react"
const Select = ({ children, value, onValueChange }: any) => {
return (
<select
value={value}
onChange={(e) => onValueChange(e.target.value)}
className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
>
{children}
</select>
)
}
const SelectTrigger = ({ children, className }: any) => <>{children}</>
const SelectValue = ({ placeholder }: any) => <option value="">{placeholder}</option>
const SelectContent = ({ children, className }: any) => <>{children}</>
const SelectItem = ({ value, children }: any) => <option value={value}>{children}</option>
export { Select, SelectTrigger, SelectValue, SelectContent, SelectItem }

View File

@@ -0,0 +1,20 @@
import * as React from "react"
import { cn } from "@/lib/utils"
const Slider = React.forwardRef<HTMLInputElement, React.InputHTMLAttributes<HTMLInputElement>>(
({ className, ...props }, ref) => (
<input
type="range"
className={cn(
"w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer dark:bg-gray-700",
className
)}
ref={ref}
{...props}
/>
)
)
Slider.displayName = "Slider"
export { Slider }

View File

@@ -0,0 +1,22 @@
import { cn } from "@/lib/utils"
function Spinner({ className, size = "default" }: { className?: string; size?: "sm" | "default" | "lg" }) {
const sizeClasses = {
sm: "h-4 w-4",
default: "h-8 w-8",
lg: "h-12 w-12"
}
return (
<div className={cn("flex items-center justify-center", className)}>
<div
className={cn(
"animate-spin rounded-full border-2 border-current border-t-transparent text-primary",
sizeClasses[size]
)}
/>
</div>
)
}
export { Spinner }

View File

@@ -0,0 +1,33 @@
import * as React from "react"
import type { Primitive } from "@radix-ui/react-primitive"
// Simplified Switch to avoid Radix dependency issues if not installed, or use standard div toggle
import { cn } from "@/lib/utils"
const Switch = React.forwardRef<HTMLButtonElement, React.ButtonHTMLAttributes<HTMLButtonElement> & { checked?: boolean, onCheckedChange?: (checked: boolean) => void }>(
({ className, checked, onCheckedChange, ...props }, ref) => (
<button
type="button"
role="switch"
aria-checked={checked}
ref={ref}
onClick={() => onCheckedChange?.(!checked)}
className={cn(
"peer inline-flex h-[24px] w-[44px] shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input",
checked ? "bg-primary" : "bg-slate-700",
className
)}
{...props}
>
<span
className={cn(
"pointer-events-none block h-5 w-5 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-5 data-[state=unchecked]:translate-x-0",
checked ? "translate-x-5 bg-white" : "translate-x-0 bg-slate-400"
)}
/>
</button>
)
)
Switch.displayName = "Switch"
export { Switch }

119
src/components/ui/table.tsx Normal file
View File

@@ -0,0 +1,119 @@
import * as React from "react"
import { cn } from "@/lib/utils"
const Table = React.forwardRef<
HTMLTableElement,
React.HTMLAttributes<HTMLTableElement>
>(({ className, ...props }, ref) => (
<div className="relative w-full overflow-auto">
<table
ref={ref}
className={cn("w-full caption-bottom text-sm", className)}
{...props}
/>
</div>
))
Table.displayName = "Table"
const TableHeader = React.forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => (
<thead ref={ref} className={cn("[&_tr]:border-b", className)} {...props} />
))
TableHeader.displayName = "TableHeader"
const TableBody = React.forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => (
<tbody
ref={ref}
className={cn("[&_tr:last-child]:border-0", className)}
{...props}
/>
))
TableBody.displayName = "TableBody"
const TableFooter = React.forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => (
<tfoot
ref={ref}
className={cn(
"border-t bg-muted/50 font-medium [&>tr]:last:border-b-0",
className
)}
{...props}
/>
))
TableFooter.displayName = "TableFooter"
const TableRow = React.forwardRef<
HTMLTableRowElement,
React.HTMLAttributes<HTMLTableRowElement>
>(({ className, ...props }, ref) => (
<tr
ref={ref}
className={cn(
"border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted",
className
)}
{...props}
/>
))
TableRow.displayName = "TableRow"
const TableHead = React.forwardRef<
HTMLTableCellElement,
React.ThHTMLAttributes<HTMLTableCellElement>
>(({ className, ...props }, ref) => (
<th
ref={ref}
className={cn(
"h-10 px-2 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
className
)}
{...props}
/>
))
TableHead.displayName = "TableHead"
const TableCell = React.forwardRef<
HTMLTableCellElement,
React.TdHTMLAttributes<HTMLTableCellElement>
>(({ className, ...props }, ref) => (
<td
ref={ref}
className={cn(
"p-2 align-middle [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
className
)}
{...props}
/>
))
TableCell.displayName = "TableCell"
const TableCaption = React.forwardRef<
HTMLTableCaptionElement,
React.HTMLAttributes<HTMLTableCaptionElement>
>(({ className, ...props }, ref) => (
<caption
ref={ref}
className={cn("mt-4 text-sm text-muted-foreground", className)}
{...props}
/>
))
TableCaption.displayName = "TableCaption"
export {
Table,
TableHeader,
TableBody,
TableFooter,
TableHead,
TableRow,
TableCell,
TableCaption,
}

View File

@@ -0,0 +1,52 @@
import * as React from "react"
import * as TabsPrimitive from "@radix-ui/react-tabs"
import { cn } from "@/lib/utils"
const Tabs = TabsPrimitive.Root
const TabsList = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.List>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
>(({ className, ...props }, ref) => (
<TabsPrimitive.List
ref={ref}
className={cn(
"inline-flex h-10 items-center justify-center rounded-md bg-muted p-1 text-muted-foreground",
className
)}
{...props}
/>
))
TabsList.displayName = TabsPrimitive.List.displayName
const TabsTrigger = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
>(({ className, ...props }, ref) => (
<TabsPrimitive.Trigger
ref={ref}
className={cn(
"inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-sm",
className
)}
{...props}
/>
))
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName
const TabsContent = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
>(({ className, ...props }, ref) => (
<TabsPrimitive.Content
ref={ref}
className={cn(
"mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
className
)}
{...props}
/>
))
TabsContent.displayName = TabsPrimitive.Content.displayName
export { Tabs, TabsList, TabsTrigger, TabsContent }

View File

@@ -0,0 +1,19 @@
import * as React from "react"
export interface TextareaProps
extends React.TextareaHTMLAttributes<HTMLTextAreaElement> { }
const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
({ className, ...props }, ref) => {
return (
<textarea
className={`flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 ${className}`}
ref={ref}
{...props}
/>
)
}
)
Textarea.displayName = "Textarea"
export { Textarea }