Skip to content

Commit dfd4f7e

Browse files
committed
feat: template marketplace frontend gallery + fix pre-existing useAuth import bug
1 parent fdd6670 commit dfd4f7e

4 files changed

Lines changed: 202 additions & 10 deletions

File tree

apps/web/src/app/flows/page.tsx

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
'use client';
22
import { useEffect, useState } from 'react';
33
import { useRouter } from 'next/navigation';
4-
import { Plus, Play, Pencil, Trash2, Zap, Clock } from 'lucide-react';
4+
import { Plus, Play, Pencil, Trash2, Zap, Clock, Sparkles } from 'lucide-react';
55
import { flowsApi } from '@/lib/api';
66

77
interface Flow {
@@ -58,13 +58,22 @@ export default function FlowsPage() {
5858
<div className="w-7 h-7 rounded-lg bg-accent flex items-center justify-center text-sm"></div>
5959
<span className="font-semibold text-lg">agentflow</span>
6060
</div>
61-
<button
62-
onClick={() => setShowModal(true)}
63-
className="flex items-center gap-2 px-4 py-2 bg-accent hover:bg-accent-dim rounded-lg text-sm font-semibold text-white transition-colors"
64-
>
65-
<Plus size={15} />
66-
New Flow
67-
</button>
61+
<div className="flex items-center gap-3">
62+
<button
63+
onClick={() => router.push('/templates')}
64+
className="flex items-center gap-2 px-4 py-2 border border-border hover:border-accent rounded-lg text-sm font-semibold text-text-dim hover:text-text transition-colors"
65+
>
66+
<Sparkles size={15} />
67+
Templates
68+
</button>
69+
<button
70+
onClick={() => setShowModal(true)}
71+
className="flex items-center gap-2 px-4 py-2 bg-accent hover:bg-accent-dim rounded-lg text-sm font-semibold text-white transition-colors"
72+
>
73+
<Plus size={15} />
74+
New Flow
75+
</button>
76+
</div>
6877
</header>
6978

7079
{/* Content */}
Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
'use client';
2+
3+
import { useEffect, useState } from 'react';
4+
import { useRouter } from 'next/navigation';
5+
import { ArrowLeft, Sparkles, ArrowRight } from 'lucide-react';
6+
import { templatesApi } from '@/lib/api';
7+
8+
interface Template {
9+
id: string;
10+
name: string;
11+
description: string;
12+
category: string;
13+
icon: string | null;
14+
useCount: number;
15+
}
16+
17+
const CATEGORY_LABELS: Record<string, string> = {
18+
ai: 'AI',
19+
data: 'Data',
20+
automation: 'Automation',
21+
monitoring: 'Monitoring',
22+
communication: 'Communication',
23+
};
24+
25+
const CATEGORY_COLORS: Record<string, string> = {
26+
ai: 'bg-accent/20 text-accent',
27+
data: 'bg-blue-900 text-blue-400',
28+
automation: 'bg-purple-900 text-purple-400',
29+
monitoring: 'bg-yellow-900 text-yellow-400',
30+
communication: 'bg-green-900 text-green-400',
31+
};
32+
33+
export default function TemplatesPage() {
34+
const router = useRouter();
35+
const [templates, setTemplates] = useState<Template[]>([]);
36+
const [loading, setLoading] = useState(true);
37+
const [usingId, setUsingId] = useState<string | null>(null);
38+
const [activeCategory, setActiveCategory] = useState<string | null>(null);
39+
40+
useEffect(() => {
41+
templatesApi.list()
42+
.then(setTemplates)
43+
.finally(() => setLoading(false));
44+
}, []);
45+
46+
const categories = Array.from(new Set(templates.map((t) => t.category)));
47+
const filtered = activeCategory
48+
? templates.filter((t) => t.category === activeCategory)
49+
: templates;
50+
51+
const handleUse = async (id: string) => {
52+
setUsingId(id);
53+
try {
54+
const flow = await templatesApi.use(id);
55+
router.push(`/flows/${flow.id}`);
56+
} catch {
57+
setUsingId(null);
58+
}
59+
};
60+
61+
return (
62+
<div className="min-h-screen bg-bg text-text">
63+
<header className="border-b border-border px-8 py-4 flex items-center gap-4">
64+
<button
65+
onClick={() => router.push('/flows')}
66+
className="flex items-center gap-1.5 text-sm text-text-dim hover:text-text transition-colors"
67+
>
68+
<ArrowLeft size={15} /> Flows
69+
</button>
70+
<div className="w-px h-5 bg-border" />
71+
<div className="flex items-center gap-2">
72+
<Sparkles size={15} className="text-accent" />
73+
<span className="font-semibold text-lg">Template Marketplace</span>
74+
</div>
75+
</header>
76+
77+
<main className="max-w-6xl mx-auto px-8 py-10">
78+
<div className="mb-8">
79+
<h1 className="text-2xl font-semibold text-text">Start from a template</h1>
80+
<p className="text-sm text-text-dim mt-1">
81+
{templates.length} ready-made agent pipeline{templates.length !== 1 ? 's' : ''} — clone and customize in seconds
82+
</p>
83+
</div>
84+
85+
{/* Category filter */}
86+
{!loading && categories.length > 0 && (
87+
<div className="flex flex-wrap gap-2 mb-8">
88+
<button
89+
onClick={() => setActiveCategory(null)}
90+
className={`text-xs font-medium px-3 py-1.5 rounded-full transition-colors ${
91+
activeCategory === null
92+
? 'bg-accent text-white'
93+
: 'bg-surface border border-border text-text-dim hover:text-text'
94+
}`}
95+
>
96+
All
97+
</button>
98+
{categories.map((cat) => (
99+
<button
100+
key={cat}
101+
onClick={() => setActiveCategory(cat)}
102+
className={`text-xs font-medium px-3 py-1.5 rounded-full transition-colors ${
103+
activeCategory === cat
104+
? 'bg-accent text-white'
105+
: 'bg-surface border border-border text-text-dim hover:text-text'
106+
}`}
107+
>
108+
{CATEGORY_LABELS[cat] || cat}
109+
</button>
110+
))}
111+
</div>
112+
)}
113+
114+
{loading ? (
115+
<div className="flex items-center justify-center h-48">
116+
<div className="w-6 h-6 border-2 border-accent border-t-transparent rounded-full animate-spin" />
117+
</div>
118+
) : filtered.length === 0 ? (
119+
<div className="flex flex-col items-center justify-center h-64 border border-dashed border-border rounded-2xl">
120+
<div className="text-4xl mb-3">📭</div>
121+
<p className="text-text-dim">No templates in this category yet</p>
122+
</div>
123+
) : (
124+
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
125+
{filtered.map((template) => (
126+
<div
127+
key={template.id}
128+
className="bg-surface border border-border rounded-xl p-5 hover:border-accent transition-all group flex flex-col"
129+
>
130+
<div className="flex items-start justify-between mb-3">
131+
<div className="w-9 h-9 rounded-lg bg-accent/20 flex items-center justify-center text-lg">
132+
{template.icon || '⚡'}
133+
</div>
134+
<span
135+
className={`text-xs font-medium px-2 py-0.5 rounded-full ${
136+
CATEGORY_COLORS[template.category] || 'bg-muted text-text-dim'
137+
}`}
138+
>
139+
{CATEGORY_LABELS[template.category] || template.category}
140+
</span>
141+
</div>
142+
143+
<h3 className="font-semibold text-text mb-1">{template.name}</h3>
144+
<p className="text-xs text-text-dim leading-relaxed mb-4 flex-1">
145+
{template.description}
146+
</p>
147+
148+
<div className="flex items-center justify-between mt-auto pt-3 border-t border-border">
149+
<span className="text-xs text-muted">
150+
{template.useCount > 0
151+
? `Used ${template.useCount} time${template.useCount !== 1 ? 's' : ''}`
152+
: 'Not used yet'}
153+
</span>
154+
<button
155+
onClick={() => handleUse(template.id)}
156+
disabled={usingId === template.id}
157+
className="flex items-center gap-1.5 text-xs font-semibold px-3 py-1.5 rounded-lg bg-accent hover:bg-accent-dim text-white transition-colors disabled:opacity-50"
158+
>
159+
{usingId === template.id ? (
160+
<div className="w-3 h-3 border-2 border-white border-t-transparent rounded-full animate-spin" />
161+
) : (
162+
<>
163+
Use template <ArrowRight size={11} />
164+
</>
165+
)}
166+
</button>
167+
</div>
168+
</div>
169+
))}
170+
</div>
171+
)}
172+
</main>
173+
</div>
174+
);
175+
}

apps/web/src/components/executions/ExecutionStream.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
'use client';
22

33
import { useExecutionStream } from '@/hooks/useExecutionStream';
4-
import { useAuth } from '@/hooks/useAuth';
4+
import { useAuthStore } from '@/store/auth.store';
55

66
interface Props {
77
executionId: string;
@@ -22,7 +22,7 @@ const statusColor: Record<string, string> = {
2222
};
2323

2424
export function ExecutionStream({ executionId }: Props) {
25-
const { token } = useAuth();
25+
const token = useAuthStore((s) => s.accessToken);
2626
const { logs, status, done, connected } = useExecutionStream({
2727
executionId,
2828
token,

apps/web/src/lib/api.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,3 +34,11 @@ export const flowsApi = {
3434
api.post(`/flows/${id}/execute`).then((r) => r.data),
3535
remove: (id: string) => api.delete(`/flows/${id}`).then((r) => r.data),
3636
};
37+
38+
// Templates calls
39+
export const templatesApi = {
40+
list: (category?: string) =>
41+
api.get('/templates', { params: category ? { category } : {} }).then((r) => r.data),
42+
get: (id: string) => api.get(`/templates/${id}`).then((r) => r.data),
43+
use: (id: string) => api.post(`/templates/${id}/use`).then((r) => r.data),
44+
};

0 commit comments

Comments
 (0)