Tasks
0 / 5
Scaffold the project structureScaffold the project structure
0%
Build the component registryBuild the component registry
Implement entitlement gatingImplement entitlement gating
Wire up Stripe checkoutWire up Stripe checkout
Polish the landing pagePolish the landing page
task-list.tsx
prompt.md
Collapse
Reset
Copy Code
Download
Run
1
"use client";
2
3
import React, { useState, useEffect } from "react";
4
import { motion, AnimatePresence } from "framer-motion";
5
import { ChevronDown } from "lucide-react";
6
import { cn } from "@/lib/utils";
7
8
const TASKS = [
9
"Scaffold the project structure",
10
"Build the component registry",
11
"Implement entitlement gating",
12
"Wire up Stripe checkout",
13
"Polish the landing page"
14
];
15
16
const OverallProgressRing = ({ completed, total }: { completed: number; total: number }) => {
17
const radius = 7;
18
const circumference = 2 * Math.PI * radius;
19
const offset = circumference - (completed / total) * circumference;
20
21
return (
22
<svg width="18" height="18" viewBox="0 0 16 16" fill="none" className="-rotate-90">
23
<circle cx="8" cy="8" r={radius} stroke="currentColor" strokeWidth="1.5" className="text-zinc-200 dark:text-white/10" />
24
<circle
25
cx="8"
26
cy="8"
27
r={radius}
28
stroke="currentColor"
29
strokeWidth="1.5"
30
className="text-zinc-800 dark:text-zinc-400 transition-all duration-500 ease-in-out"
31
strokeDasharray={circumference}
32
strokeDashoffset={offset}
33
strokeLinecap="round"
34
/>
35
</svg>
36
);
37
};
38
39
const SpinningDotted = () => (
40
<div className="flex items-center justify-center w-[18px] h-[18px] animate-[spin_3s_linear_infinite]">
41
<svg width="18" height="18" viewBox="0 0 16 16" fill="none">
42
<circle cx="8" cy="8" r="7" stroke="currentColor" strokeWidth="1.5" strokeDasharray="2 3" className="text-zinc-400 dark:text-zinc-500" />
43
</svg>
44
</div>
45
);
46
47
const BlueCheck = () => (
48
<div className="flex items-center justify-center w-[18px] h-[18px] rounded-full bg-blue-500">
49
<svg width="10" height="10" viewBox="0 0 16 16" fill="none">
50
<path d="M3 8l3 3 7-7" stroke="white" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" />
51
</svg>
52
</div>
53
);
54
55
const TaskIcon = ({ status }: { status: "pending" | "running" | "completed" }) => {
56
if (status === "completed") {
57
return (
58
<div className="flex items-center justify-center w-[18px] h-[18px] rounded-full bg-zinc-200 dark:bg-white/10 shrink-0">
59
<svg width="10" height="10" viewBox="0 0 16 16" fill="none">
60
<path d="M3 8l3 3 7-7" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" className="text-zinc-600 dark:text-zinc-400" />
61
</svg>
62
</div>
63
);
64
}
65
if (status === "running") {
66
return (
67
<div className="flex items-center justify-center w-[18px] h-[18px] rounded-full border border-zinc-400 dark:border-zinc-300 shrink-0">
68
<svg width="10" height="10" viewBox="0 0 16 16" fill="none">
69
<path d="M6 4l4 4-4 4" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="text-zinc-700 dark:text-zinc-300" />
70
</svg>
71
</div>
72
);
73
}
74
// pending
75
return (
76
<div className="flex items-center justify-center w-[18px] h-[18px] rounded-full border border-dashed border-zinc-300 dark:border-zinc-700 shrink-0" />
77
);
78
};
79
80
const TaskText = ({ title, progress, isCompleted }: { title: string; progress: number; isCompleted: boolean }) => {
81
if (isCompleted) {
82
return (
83
<span className="text-[14px] font-medium text-zinc-400 dark:text-zinc-600 line-through decoration-zinc-300 dark:decoration-zinc-700">
84
{title}
85
</span>
86
);
87
}
88
89
return (
90
<span className="relative text-[14px] font-medium inline-block">
91
<span className="text-zinc-400 dark:text-zinc-600">{title}</span>
92
<span
93
className="absolute left-0 top-0 text-zinc-900 dark:text-zinc-200 whitespace-nowrap"
94
style={{
95
maskImage: `linear-gradient(to right, black 0%, black ${Math.max(0, progress - 15)}%, transparent ${progress}%, transparent 100%)`,
96
WebkitMaskImage: `linear-gradient(to right, black 0%, black ${Math.max(0, progress - 15)}%, transparent ${progress}%, transparent 100%)`,
97
}}
98
>
99
{title}
100
</span>
101
</span>
102
);
103
};
104
105
export function TaskList({ className }: { className?: string }) {
106
const [isExpanded, setIsExpanded] = useState(true);
107
const [currentTaskIndex, setCurrentTaskIndex] = useState(0);
108
const [progress, setProgress] = useState(0);
109
const [isAllComplete, setIsAllComplete] = useState(false);
110
111
useEffect(() => {
112
if (isAllComplete) {
113
const timer = setTimeout(() => {
114
setIsAllComplete(false);
115
setCurrentTaskIndex(0);
116
setProgress(0);
117
}, 3000);
118
return () => clearTimeout(timer);
119
}
120
121
let startTime = performance.now();
122
let animationFrameId: number;
123
const DURATION = 2000;
124
const PAUSE = 500;
125
126
const animate = (time: number) => {
127
const elapsed = time - startTime;
128
129
if (elapsed < DURATION) {
130
setProgress((elapsed / DURATION) * 100);
131
animationFrameId = requestAnimationFrame(animate);
132
} else if (elapsed < DURATION + PAUSE) {
133
setProgress(100);
134
animationFrameId = requestAnimationFrame(animate);
135
} else {
136
if (currentTaskIndex < TASKS.length - 1) {
137
setCurrentTaskIndex((prev) => prev + 1);
138
setProgress(0);
139
startTime = performance.now();
140
animationFrameId = requestAnimationFrame(animate);
141
} else {
142
setIsAllComplete(true);
143
setProgress(100);
144
}
145
}
146
};
147
148
animationFrameId = requestAnimationFrame(animate);
149
150
return () => cancelAnimationFrame(animationFrameId);
151
}, [currentTaskIndex, isAllComplete]);
152
153
return (
154
<motion.div
155
layout
156
className={cn(
157
"w-full max-w-[420px] rounded-2xl bg-white dark:bg-[#141414] border border-zinc-200 dark:border-white/10 overflow-hidden shadow-sm",
158
className
159
)}
160
>
161
<div
162
onClick={() => setIsExpanded(!isExpanded)}
163
className="px-5 py-4 cursor-pointer flex items-center justify-between select-none hover:bg-zinc-50 dark:hover:bg-white/[0.02] transition-colors"
164
>
165
<div className="flex items-center gap-3.5">
166
<div className="w-[18px] h-[18px] flex items-center justify-center shrink-0">
167
<AnimatePresence mode="wait">
168
{isAllComplete ? (
169
<motion.div
170
key="complete-icon"
171
initial={{ opacity: 0, scale: 0.8 }}
172
animate={{ opacity: 1, scale: 1 }}
173
exit={{ opacity: 0, scale: 0.8 }}
174
transition={{ duration: 0.15 }}
175
>
176
<BlueCheck />
177
</motion.div>
178
) : isExpanded ? (
179
<motion.div
180
key="expanded-icon"
181
initial={{ opacity: 0, scale: 0.8 }}
182
animate={{ opacity: 1, scale: 1 }}
183
exit={{ opacity: 0, scale: 0.8 }}
184
transition={{ duration: 0.15 }}
185
>
186
<OverallProgressRing completed={currentTaskIndex} total={TASKS.length} />
187
</motion.div>
188
) : (
189
<motion.div
190
key="running-icon"
191
initial={{ opacity: 0, scale: 0.8 }}
192
animate={{ opacity: 1, scale: 1 }}
193
exit={{ opacity: 0, scale: 0.8 }}
194
transition={{ duration: 0.15 }}
195
>
196
<SpinningDotted />
197
</motion.div>
198
)}
199
</AnimatePresence>
200
</div>
201
202
<div className="text-[14.5px] font-semibold text-zinc-900 dark:text-zinc-100 flex items-center h-5 overflow-hidden">
203
<AnimatePresence mode="wait">
204
{isExpanded ? (
205
<motion.span
206
key="expanded-text"
207
initial={{ opacity: 0, y: 15 }}
208
animate={{ opacity: 1, y: 0 }}
209
exit={{ opacity: 0, y: -15 }}
210
transition={{ duration: 0.2 }}
211
>
212
Tasks
213
</motion.span>
214
) : isAllComplete ? (
215
<motion.span
216
key="complete-text"
217
initial={{ opacity: 0, y: 15 }}
218
animate={{ opacity: 1, y: 0 }}
219
exit={{ opacity: 0, y: -15 }}
220
transition={{ duration: 0.2 }}
221
>
222
All tasks complete
223
</motion.span>
224
) : (
225
<motion.div
226
key="running-text"
227
initial={{ opacity: 0, y: 15 }}
228
animate={{ opacity: 1, y: 0 }}
229
exit={{ opacity: 0, y: -15 }}
230
transition={{ duration: 0.2 }}
231
className="flex items-center"
232
>
233
<TaskText title={TASKS[currentTaskIndex]} progress={progress} isCompleted={false} />
234
</motion.div>
235
)}
236
</AnimatePresence>
237
</div>
238
</div>
239
240
<div className="flex items-center gap-2.5 text-[13px] text-zinc-500 dark:text-zinc-400 font-medium tracking-wide">
241
<span className="font-mono">
242
{isAllComplete ? TASKS.length : currentTaskIndex} / {TASKS.length}
243
</span>
244
<motion.div animate={{ rotate: isExpanded ? 180 : 0 }} transition={{ duration: 0.3, ease: "backOut" }}>
245
<ChevronDown className="w-4 h-4 text-zinc-400" />
246
</motion.div>
247
</div>
248
</div>
249
250
<AnimatePresence initial={false}>
251
{isExpanded && (
252
<motion.div
253
initial={{ height: 0, opacity: 0 }}
254
animate={{ height: "auto", opacity: 1 }}
255
exit={{ height: 0, opacity: 0 }}
256
transition={{ duration: 0.3, ease: "easeInOut" }}
257
className="overflow-hidden"
258
>
259
<div className="px-5 pb-5 pt-1 flex flex-col gap-3.5">
260
{TASKS.map((task, idx) => {
261
const isCompleted = idx < currentTaskIndex || isAllComplete;
262
const isRunning = idx === currentTaskIndex && !isAllComplete;
263
264
let status: "completed" | "running" | "pending" = "pending";
265
if (isCompleted) status = "completed";
266
else if (isRunning) status = "running";
267
268
return (
269
<div key={idx} className="flex items-center justify-between">
270
<div className="flex items-center gap-3.5">
271
<TaskIcon status={status} />
272
<TaskText
273
title={task}
274
progress={isRunning ? progress : isCompleted ? 100 : 0}
275
isCompleted={isCompleted}
276
/>
277
</div>
278
279
<div className="w-10 flex justify-end">
280
<AnimatePresence>
281
{isRunning && (
282
<motion.div
283
initial={{ opacity: 0, scale: 0.8 }}
284
animate={{ opacity: 1, scale: 1 }}
285
exit={{ opacity: 0, scale: 0.8 }}
286
className="px-2 py-0.5 rounded-full bg-zinc-100 dark:bg-white/10 text-[10px] font-medium text-zinc-600 dark:text-zinc-400 font-mono tracking-wide"
287
>
288
{Math.round(progress)}%
289
</motion.div>
290
)}
291
</AnimatePresence>
292
</div>
293
</div>
294
);
295
})}
296
</div>
297
</motion.div>
298
)}
299
</AnimatePresence>
300
</motion.div>
301
);
302
}
303

