2026-01-26 13:12:17 +00:00
|
|
|
import React, { useState, useEffect } from 'react';
|
2026-01-07 09:31:02 +00:00
|
|
|
import { Card } from '../ui/card';
|
|
|
|
|
import { Button } from '../ui/button';
|
|
|
|
|
import { Input } from '../ui/input';
|
2026-01-26 13:12:17 +00:00
|
|
|
import { Plus, Trash2, Zap, Terminal, Activity, Loader2 } from 'lucide-react';
|
|
|
|
|
import { useToast } from '../../components/feedback/ToastProvider';
|
|
|
|
|
import { cn } from '@/lib/utils';
|
|
|
|
|
import { webhookService, Webhook } from '../../services/webhookService';
|
2026-01-07 09:31:02 +00:00
|
|
|
|
|
|
|
|
export const WebhooksView: React.FC = () => {
|
|
|
|
|
const { addToast } = useToast();
|
2026-01-26 13:12:17 +00:00
|
|
|
const [webhooks, setWebhooks] = useState<Webhook[]>([]);
|
|
|
|
|
const [loading, setLoading] = useState(true);
|
2026-01-07 09:31:02 +00:00
|
|
|
const [newUrl, setNewUrl] = useState('');
|
|
|
|
|
|
2026-01-26 13:12:17 +00:00
|
|
|
const fetchWebhooks = async () => {
|
|
|
|
|
setLoading(true);
|
|
|
|
|
try {
|
|
|
|
|
const data = await webhookService.list();
|
|
|
|
|
setWebhooks(data);
|
|
|
|
|
} catch (e) {
|
|
|
|
|
addToast('Failed to load webhooks', 'error');
|
|
|
|
|
} finally {
|
|
|
|
|
setLoading(false);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
fetchWebhooks();
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
const handleCreate = async () => {
|
2026-01-13 18:47:57 +00:00
|
|
|
if (!newUrl) return;
|
2026-01-26 13:12:17 +00:00
|
|
|
try {
|
|
|
|
|
await webhookService.create(newUrl);
|
|
|
|
|
setNewUrl('');
|
|
|
|
|
addToast('Webhook generated successfully', 'success');
|
|
|
|
|
fetchWebhooks();
|
|
|
|
|
} catch (e) {
|
|
|
|
|
addToast('Failed to create webhook', 'error');
|
|
|
|
|
}
|
2026-01-07 09:31:02 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleTest = (_id: string) => {
|
2026-01-26 13:12:17 +00:00
|
|
|
addToast(`Sending test payload to endpoint...`, 'info');
|
2026-01-07 09:31:02 +00:00
|
|
|
};
|
|
|
|
|
|
2026-01-26 13:12:17 +00:00
|
|
|
const handleDelete = async (id: string) => {
|
|
|
|
|
try {
|
|
|
|
|
await webhookService.delete(id);
|
|
|
|
|
setWebhooks(webhooks.filter((w) => w.id !== id));
|
|
|
|
|
addToast('Webhook disconnected', 'info');
|
|
|
|
|
} catch (e) {
|
|
|
|
|
addToast('Failed to delete webhook', 'error');
|
|
|
|
|
}
|
2026-01-07 09:31:02 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
return (
|
2026-01-26 13:12:17 +00:00
|
|
|
<div className="space-y-6 pb-20 container mx-auto px-4 py-8 max-w-5xl">
|
|
|
|
|
<div className="flex items-end justify-between">
|
|
|
|
|
<div>
|
|
|
|
|
<h2 className="text-3xl font-display font-bold text-white mb-1">Webhooks</h2>
|
|
|
|
|
<p className="text-muted-foreground font-mono text-xs flex items-center gap-2">
|
|
|
|
|
<Terminal className="w-3 h-3" /> EVENT SUBSCRIPTION PROTOCOL
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
2026-01-13 18:47:57 +00:00
|
|
|
</div>
|
|
|
|
|
|
2026-01-26 13:12:17 +00:00
|
|
|
<Card variant="glass" className="p-6 border-primary/20 bg-black/40 relative overflow-hidden group">
|
|
|
|
|
<div className="absolute inset-x-0 bottom-0 h-1 bg-gradient-to-r from-transparent via-primary/50 to-transparent opacity-50 group-hover:opacity-100 transition-opacity" />
|
|
|
|
|
<h3 className="font-bold text-white mb-4 flex items-center gap-2 text-sm uppercase tracking-widest">
|
|
|
|
|
<Plus className="w-4 h-4 text-primary" /> Register Endpoint
|
2026-01-13 18:47:57 +00:00
|
|
|
</h3>
|
|
|
|
|
<div className="flex gap-4">
|
|
|
|
|
<Input
|
2026-01-26 13:12:17 +00:00
|
|
|
placeholder="https://api.domain.com/webhook"
|
2026-01-13 18:47:57 +00:00
|
|
|
value={newUrl}
|
|
|
|
|
onChange={(e) => setNewUrl(e.target.value)}
|
2026-01-26 13:12:17 +00:00
|
|
|
className="flex-1 font-mono text-sm"
|
2026-01-13 18:47:57 +00:00
|
|
|
/>
|
2026-01-26 13:12:17 +00:00
|
|
|
<Button onClick={handleCreate} disabled={!newUrl} className="shadow-glow-cyan">
|
|
|
|
|
Create Hook
|
2026-01-13 18:47:57 +00:00
|
|
|
</Button>
|
2026-01-07 09:31:02 +00:00
|
|
|
</div>
|
2026-01-13 18:47:57 +00:00
|
|
|
</Card>
|
2026-01-07 09:31:02 +00:00
|
|
|
|
2026-01-13 18:47:57 +00:00
|
|
|
<div className="space-y-4">
|
2026-01-26 13:12:17 +00:00
|
|
|
{loading ? (
|
|
|
|
|
<div className="flex justify-center py-12">
|
|
|
|
|
<Loader2 className="w-8 h-8 text-primary animate-spin" />
|
|
|
|
|
</div>
|
|
|
|
|
) : webhooks.length === 0 ? (
|
|
|
|
|
<Card variant="glass" className="p-12 text-center border-dashed border-white/10 bg-black/20">
|
|
|
|
|
<Activity className="w-12 h-12 text-muted-foreground mx-auto mb-4 opacity-20" />
|
|
|
|
|
<h4 className="text-white font-bold">No endpoints registered</h4>
|
|
|
|
|
<p className="text-muted-foreground text-sm max-w-sm mx-auto mt-2 font-mono">
|
|
|
|
|
Ready to stream real-time events to your external infra.
|
|
|
|
|
</p>
|
|
|
|
|
</Card>
|
|
|
|
|
) : (
|
|
|
|
|
webhooks.map((hook) => (
|
|
|
|
|
<Card key={hook.id} variant="glass" className="group overflow-hidden relative border-white/5 hover:border-white/10 transition-all bg-black/40">
|
|
|
|
|
{/* Status Indicator Bar */}
|
feat(web): UI premium Discord/Spotify-like — tokens, shadows, focus, layout
Plan UI premium 6–8 semaines (design system, shell, Storybook, a11y):
- Design system: DESIGN_TOKENS.md, APP_SHELL.md, FULL_LAYOUT_PAGE.md. Single source
for layout/shell (index.css), shadows (design-system.css), durations/easing.
- Tokens: shadow-cover-depth, shadow-gold-glow, shadow-fab-glow; layout max-height
(max-h-layout-drawer, max-h-layout-panel, max-h-layout-list). All duration-200/300/500
replaced by --duration-fast/normal/slow. Arbitrary shadows replaced by token classes.
- Shell & player: Sidebar, Header, GlobalPlayer, MiniPlayer, PlayerQueue, PlayerControls,
AudioPlayer use tokens; focus-visible on Sidebar, PlayerQueue, DropdownMenuTrigger/Item,
TabsTrigger. Typography: text-[10px]/[9px] → text-xs where applicable.
- ESLint: no-restricted-syntax (warn) for w-/h-/rounded-/shadow-/text-/spacing arbitrary.
- Scripts: report-arbitrary-values.mjs, capture/compare/generate visual; visual-complete.spec.ts.
- Stories full layout: Dashboard, Playlists, Library, Settings, Profile in DashboardLayout.stories.
- .cursorrules + README: DESIGN_TOKENS, APP_SHELL, visual commands, no arbitrary without justification.
- apps/web/.gitignore: e2e test artifacts (test-results-visual, playwright-report-visual).
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-08 16:15:58 +00:00
|
|
|
<div className={cn("absolute left-0 top-0 bottom-0 w-1", hook.status === 'active' ? 'bg-lime-500 shadow-status-dot-lime' : 'bg-red-500')} />
|
2026-01-26 13:12:17 +00:00
|
|
|
|
|
|
|
|
<div className="flex flex-col md:flex-row items-start md:items-center justify-between p-6 pl-8 gap-4">
|
|
|
|
|
<div className="flex-1">
|
|
|
|
|
<div className="flex items-center gap-3 mb-2">
|
|
|
|
|
<div className={cn("text-xs font-bold px-2 py-0.5 rounded border uppercase tracking-wider", hook.status === 'active' ? 'border-lime-500/30 text-lime-500 bg-lime-500/10' : 'border-red-500/30 text-red-500 bg-red-500/10')}>
|
|
|
|
|
{hook.status}
|
|
|
|
|
</div>
|
|
|
|
|
<span className="font-mono text-white text-sm break-all">{hook.url}</span>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="flex flex-wrap gap-4 text-xs text-muted-foreground items-center">
|
|
|
|
|
<div className="flex gap-2">
|
|
|
|
|
<span className="text-primary/70">events:</span>
|
|
|
|
|
{hook.events.map(e => <span key={e} className="text-white bg-white/5 px-1 rounded">{e}</span>)}
|
|
|
|
|
</div>
|
|
|
|
|
<div className="w-1 h-1 rounded-full bg-white/20" />
|
|
|
|
|
<div className="flex items-center gap-1">
|
|
|
|
|
<Activity className="w-3 h-3" /> Last trigger: {hook.lastTriggered}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
2026-01-13 18:47:57 +00:00
|
|
|
</div>
|
2026-01-26 13:12:17 +00:00
|
|
|
|
|
|
|
|
<div className="flex gap-2 w-full md:w-auto opacity-60 group-hover:opacity-100 transition-opacity">
|
|
|
|
|
<Button variant="outline" size="sm" onClick={() => handleTest(hook.id)} className="border-white/10 hover:bg-primary/20 hover:text-primary hover:border-primary/50">
|
|
|
|
|
<Zap className="w-3 h-3 mr-2" /> Test
|
|
|
|
|
</Button>
|
|
|
|
|
<Button variant="outline" size="sm" onClick={() => handleDelete(hook.id)} className="border-white/10 hover:bg-red-500/20 hover:text-red-500 hover:border-red-500/50">
|
|
|
|
|
<Trash2 className="w-3 h-3" />
|
|
|
|
|
</Button>
|
2026-01-13 18:47:57 +00:00
|
|
|
</div>
|
|
|
|
|
</div>
|
2026-01-26 13:12:17 +00:00
|
|
|
</Card>
|
|
|
|
|
))
|
|
|
|
|
)}
|
2026-01-13 18:47:57 +00:00
|
|
|
</div>
|
2026-01-07 09:31:02 +00:00
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
};
|