Skip to content

Commit 7dd0470

Browse files
INQTRclaude
andauthored
Prepare site for Paddle domain review (#160)
* Prepare site for Paddle domain review * feat: redesign pricing page to match home and features aesthetics - Update typography and headings to use larger, tighter tracking. - Redesign the status banner and pricing cards for a more modern look. - Overhaul comparison table and FAQ section. - Match styling with the rest of the updated site aesthetics. Made-with: Cursor * feat: enhance pricing comparison table with grouped categories and Pro highlights Group features into 5 categories (Core Planning, History & Retention, Analytics & Insights, Exports & Integrations, Support) to make Pro's value proposition more visible. Expand from 9 to 19 features, add filled-circle checkmarks for Pro-exclusive rows, and improve mobile responsiveness. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: update About and Features sections with new content and improved layout * feat: replace 3 core feature cards with shipped Pro features Swap Auto-Complete Voting, Player Management, and Synchronized Timer out of the bento grid in favor of Jira Integration, Time-to-Consensus, and Voter Alignment — three shipped Pro features that better showcase the product's current strengths. Trim recentlyShipped to keep only Sprint Predictability Score and Enhanced Data Exports. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: remove unused Users import in about-content Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent c926d6e commit 7dd0470

File tree

15 files changed

+1296
-923
lines changed

15 files changed

+1296
-923
lines changed

src/app/about/about-content.tsx

Lines changed: 144 additions & 153 deletions
Large diffs are not rendered by default.

src/app/features/feature-animations.tsx

Lines changed: 140 additions & 96 deletions
Original file line numberDiff line numberDiff line change
@@ -110,49 +110,69 @@ export function AnalyticsAnimation() {
110110
);
111111
}
112112

113-
export function TimerAnimation() {
114-
const [time, setTime] = useState(60);
113+
export function JiraIntegrationAnimation() {
114+
const [phase, setPhase] = useState<"idle" | "import" | "sync" | "done">("idle");
115115

116116
useEffect(() => {
117117
let isMounted = true;
118-
let interval: NodeJS.Timeout;
119-
120118
const play = async () => {
121-
setTime(60);
122-
interval = setInterval(() => {
123-
if (!isMounted) return;
124-
setTime((t) => (t > 0 ? t - 1 : 60));
125-
}, 50); // Fast countdown for effect
119+
while (isMounted) {
120+
setPhase("idle");
121+
await new Promise((r) => setTimeout(r, 1000));
122+
if (!isMounted) break;
123+
124+
setPhase("import");
125+
await new Promise((r) => setTimeout(r, 1200));
126+
if (!isMounted) break;
127+
128+
setPhase("sync");
129+
await new Promise((r) => setTimeout(r, 1200));
130+
if (!isMounted) break;
131+
132+
setPhase("done");
133+
await new Promise((r) => setTimeout(r, 2000));
134+
}
126135
};
127-
128136
play();
129-
return () => {
130-
isMounted = false;
131-
clearInterval(interval);
132-
};
137+
return () => { isMounted = false; };
133138
}, []);
134139

135140
return (
136141
<div className="absolute inset-0 flex items-end justify-center pb-4 sm:pb-8 pointer-events-none">
137-
<div className="absolute left-1/2 -translate-x-1/2 bottom-0 w-48 h-48 bg-fuchsia-500/5 rounded-full blur-3xl"></div>
138-
<div className="w-24 h-24 sm:w-28 sm:h-28 rounded-full border-4 border-fuchsia-100 dark:border-fuchsia-900/30 flex items-center justify-center relative z-10 bg-white/50 dark:bg-zinc-900/50 backdrop-blur-sm shadow-md translate-y-2">
139-
<svg className="absolute inset-0 w-full h-full -rotate-90" viewBox="0 0 100 100">
140-
<circle
141-
cx="50"
142-
cy="50"
143-
r="46"
144-
fill="none"
145-
stroke="currentColor"
146-
strokeWidth="8"
147-
className="text-fuchsia-500 transition-all duration-75"
148-
strokeDasharray="289"
149-
strokeDashoffset={289 - (time / 60) * 289}
150-
/>
151-
</svg>
152-
<span className="text-xl sm:text-2xl font-bold font-mono text-fuchsia-600 dark:text-fuchsia-400">
153-
00:{time.toString().padStart(2, '0')}
154-
</span>
155-
</div>
142+
<div className="absolute left-1/2 -translate-x-1/2 bottom-0 w-48 h-48 bg-blue-500/5 rounded-full blur-3xl"></div>
143+
<div className="flex items-center gap-4 sm:gap-6 z-10">
144+
{/* Jira stack */}
145+
<div className="flex flex-col gap-1.5">
146+
<div className="w-20 h-7 sm:w-24 sm:h-8 rounded-lg bg-blue-500/10 dark:bg-blue-500/20 border border-blue-200 dark:border-blue-800/50 flex items-center px-2 gap-1.5">
147+
<div className="w-3 h-3 rounded-sm bg-blue-500"></div>
148+
<div className="h-1.5 w-10 bg-blue-200 dark:bg-blue-800 rounded"></div>
149+
</div>
150+
<div className="w-20 h-7 sm:w-24 sm:h-8 rounded-lg bg-blue-500/10 dark:bg-blue-500/20 border border-blue-200 dark:border-blue-800/50 flex items-center px-2 gap-1.5">
151+
<div className="w-3 h-3 rounded-sm bg-blue-500"></div>
152+
<div className="h-1.5 w-8 bg-blue-200 dark:bg-blue-800 rounded"></div>
153+
</div>
154+
</div>
155+
156+
{/* Arrow / sync area */}
157+
<div className="flex flex-col items-center gap-1">
158+
<div className={`text-xs font-bold transition-all duration-500 ${phase === "import" ? "text-blue-500 opacity-100 translate-x-2" : phase === "sync" ? "text-indigo-500 opacity-100 -translate-x-2" : "opacity-0"}`}>
159+
{phase === "import" ? "→" : "←"}
160+
</div>
161+
<div className={`w-8 h-0.5 rounded transition-all duration-500 ${phase === "import" ? "bg-blue-400 scale-x-100" : phase === "sync" ? "bg-indigo-400 scale-x-100" : "bg-gray-200 dark:bg-zinc-700 scale-x-50"}`}></div>
162+
</div>
163+
164+
{/* AgileKit stack */}
165+
<div className="flex flex-col gap-1.5">
166+
<div className={`w-20 h-7 sm:w-24 sm:h-8 rounded-lg border flex items-center px-2 gap-1.5 transition-all duration-500 ${phase === "done" ? "bg-green-50 dark:bg-green-500/10 border-green-200 dark:border-green-800/50" : "bg-white dark:bg-zinc-800 border-gray-200 dark:border-zinc-700"}`}>
167+
<div className="h-1.5 w-10 bg-gray-200 dark:bg-zinc-700 rounded"></div>
168+
{phase === "done" && <Check className="w-3 h-3 text-green-500" />}
169+
</div>
170+
<div className={`w-20 h-7 sm:w-24 sm:h-8 rounded-lg border flex items-center px-2 gap-1.5 transition-all duration-500 ${phase === "sync" || phase === "done" ? "bg-indigo-50 dark:bg-indigo-500/10 border-indigo-200 dark:border-indigo-800/50" : "bg-white dark:bg-zinc-800 border-gray-200 dark:border-zinc-700"}`}>
171+
{(phase === "sync" || phase === "done") && <span className="text-[10px] font-bold text-indigo-500">5 SP</span>}
172+
<div className="h-1.5 w-8 bg-gray-200 dark:bg-zinc-700 rounded"></div>
173+
</div>
174+
</div>
175+
</div>
156176
</div>
157177
);
158178
}
@@ -244,55 +264,60 @@ export function ScalesAnimation() {
244264
);
245265
}
246266

247-
export function PlayerManagementAnimation() {
248-
const [avatars, setAvatars] = useState<number[]>([]);
267+
export function TimeToConsensusAnimation() {
268+
const [seconds, setSeconds] = useState(0);
269+
const [dots, setDots] = useState<boolean[]>([false, false, false, false]);
270+
const [consensus, setConsensus] = useState(false);
249271

250272
useEffect(() => {
251273
let isMounted = true;
252274
const play = async () => {
253275
while (isMounted) {
254-
setAvatars([]);
255-
await new Promise((r) => setTimeout(r, 1000));
256-
if (!isMounted) break;
257-
258-
setAvatars([1]);
259-
await new Promise((r) => setTimeout(r, 500));
276+
setSeconds(0);
277+
setDots([false, false, false, false]);
278+
setConsensus(false);
279+
await new Promise((r) => setTimeout(r, 800));
260280
if (!isMounted) break;
261281

262-
setAvatars([1, 2]);
263-
await new Promise((r) => setTimeout(r, 700));
282+
for (let s = 1; s <= 12; s++) {
283+
if (!isMounted) break;
284+
setSeconds(s);
285+
if (s === 3) setDots([true, false, false, false]);
286+
if (s === 6) setDots([true, true, false, false]);
287+
if (s === 8) setDots([true, true, true, false]);
288+
if (s === 11) setDots([true, true, true, true]);
289+
await new Promise((r) => setTimeout(r, 150));
290+
}
264291
if (!isMounted) break;
265292

266-
setAvatars([1, 2, 3]);
293+
setConsensus(true);
267294
await new Promise((r) => setTimeout(r, 2500));
268295
}
269296
};
270297
play();
271-
return () => {
272-
isMounted = false;
273-
};
298+
return () => { isMounted = false; };
274299
}, []);
275300

276-
const colors = [
277-
"bg-indigo-500 text-indigo-50",
278-
"bg-violet-500 text-violet-50",
279-
"bg-purple-500 text-purple-50",
280-
];
281-
const initials = ["JD", "AB", "RW"];
301+
const formatTime = (s: number) => `0:${s.toString().padStart(2, "0")}`;
282302

283303
return (
284-
<div className="absolute inset-0 flex items-end justify-center sm:justify-end pr-0 sm:pr-10 pb-4 sm:pb-8 pointer-events-none overflow-hidden">
285-
<div className="absolute right-0 bottom-0 w-48 h-48 bg-indigo-500/5 rounded-full blur-3xl"></div>
286-
<div className="flex -space-x-4 z-10">
287-
{avatars.map((a, i) => (
288-
<div
289-
key={a}
290-
className={`w-14 h-14 rounded-full ${colors[i]} border-4 border-white dark:border-zinc-900 flex items-center justify-center shadow-lg animate-in fade-in zoom-in slide-in-from-right-4 duration-300`}
291-
style={{ zIndex: 10 - a }}
292-
>
293-
<span className="text-sm font-bold">{initials[i]}</span>
294-
</div>
295-
))}
304+
<div className="absolute inset-0 flex items-end justify-center pb-4 sm:pb-8 pointer-events-none">
305+
<div className="absolute left-1/2 -translate-x-1/2 bottom-0 w-48 h-48 bg-rose-500/5 rounded-full blur-3xl"></div>
306+
<div className="flex flex-col items-center gap-3 z-10">
307+
<span className={`text-3xl sm:text-4xl font-bold font-mono transition-colors duration-300 ${consensus ? "text-green-500" : "text-rose-500"}`}>
308+
{formatTime(seconds)}
309+
</span>
310+
<div className="flex gap-2">
311+
{dots.map((filled, i) => (
312+
<div
313+
key={i}
314+
className={`w-3 h-3 rounded-full transition-all duration-300 ${filled ? (consensus ? "bg-green-500 scale-110" : "bg-rose-400") : "bg-gray-200 dark:bg-zinc-700"}`}
315+
></div>
316+
))}
317+
</div>
318+
<span className={`text-xs font-bold uppercase tracking-widest transition-all duration-500 ${consensus ? "text-green-500 opacity-100 translate-y-0" : "opacity-0 translate-y-2"}`}>
319+
Consensus
320+
</span>
296321
</div>
297322
</div>
298323
);
@@ -344,54 +369,73 @@ export function IssuesAnimation() {
344369
);
345370
}
346371

347-
export function AutoCompleteAnimation() {
348-
const [count, setCount] = useState(3);
349-
const [revealed, setRevealed] = useState(false);
372+
export function VoterAlignmentAnimation() {
373+
const [phase, setPhase] = useState<"scattered" | "converging" | "aligned">("scattered");
350374

351375
useEffect(() => {
352376
let isMounted = true;
353377
const play = async () => {
354378
while (isMounted) {
355-
setCount(3);
356-
setRevealed(false);
357-
await new Promise((r) => setTimeout(r, 1000));
358-
if (!isMounted) break;
359-
360-
setCount(2);
361-
await new Promise((r) => setTimeout(r, 1000));
379+
setPhase("scattered");
380+
await new Promise((r) => setTimeout(r, 1200));
362381
if (!isMounted) break;
363382

364-
setCount(1);
383+
setPhase("converging");
365384
await new Promise((r) => setTimeout(r, 1000));
366385
if (!isMounted) break;
367386

368-
setRevealed(true);
387+
setPhase("aligned");
369388
await new Promise((r) => setTimeout(r, 2500));
370389
}
371390
};
372391
play();
373-
return () => {
374-
isMounted = false;
375-
};
392+
return () => { isMounted = false; };
376393
}, []);
377394

395+
const scattered = [
396+
{ top: "12%", left: "10%" },
397+
{ top: "8%", left: "78%" },
398+
{ top: "70%", left: "5%" },
399+
{ top: "75%", left: "85%" },
400+
{ top: "40%", left: "90%" },
401+
{ top: "65%", left: "45%" },
402+
];
403+
404+
const center = { top: "42%", left: "46%" };
405+
406+
const getPos = (i: number) => {
407+
if (phase === "aligned") return center;
408+
if (phase === "converging") {
409+
const s = scattered[i];
410+
return {
411+
top: `${(parseFloat(s.top) + parseFloat(center.top)) / 2}%`,
412+
left: `${(parseFloat(s.left) + parseFloat(center.left)) / 2}%`,
413+
};
414+
}
415+
return scattered[i];
416+
};
417+
378418
return (
379-
<div className="absolute inset-0 flex items-end justify-center sm:justify-end pr-0 sm:pr-8 pb-4 sm:pb-8 pointer-events-none">
380-
<div className="absolute right-0 bottom-0 w-48 h-48 bg-rose-500/5 rounded-full blur-3xl"></div>
381-
<div className="flex flex-col items-center gap-4 z-10">
382-
{!revealed ? (
383-
<div className="w-16 h-16 rounded-full bg-white dark:bg-zinc-800 border-4 border-rose-100 dark:border-rose-900/30 flex items-center justify-center shadow-lg animate-in zoom-in duration-300">
384-
<span className="text-3xl font-bold text-rose-500">{count}</span>
385-
</div>
386-
) : (
387-
<div className="flex gap-2 animate-in slide-in-from-bottom-4 fade-in duration-500">
388-
{[5, 5, 8].map((v, i) => (
389-
<div key={i} className="w-10 h-14 bg-white dark:bg-zinc-800 border border-gray-200 dark:border-zinc-700 rounded-lg flex items-center justify-center font-bold text-lg text-gray-900 dark:text-white shadow-md">
390-
{v}
391-
</div>
392-
))}
393-
</div>
394-
)}
419+
<div className="absolute inset-0 flex items-end justify-center pb-4 sm:pb-8 pointer-events-none">
420+
<div className="absolute left-1/2 -translate-x-1/2 bottom-0 w-48 h-48 bg-fuchsia-500/5 rounded-full blur-3xl"></div>
421+
<div className="relative w-36 h-28 sm:w-44 sm:h-36 z-10">
422+
{/* Background grid */}
423+
<div className="absolute inset-0 grid grid-cols-4 grid-rows-4 gap-px opacity-20">
424+
{Array.from({ length: 16 }).map((_, i) => (
425+
<div key={i} className="border border-gray-300 dark:border-zinc-700 rounded-sm"></div>
426+
))}
427+
</div>
428+
{/* Dots */}
429+
{scattered.map((_, i) => {
430+
const pos = getPos(i);
431+
return (
432+
<div
433+
key={i}
434+
className={`absolute w-3 h-3 rounded-full transition-all duration-700 ease-in-out ${phase === "aligned" ? "bg-green-500 scale-125" : "bg-rose-400"}`}
435+
style={{ top: pos.top, left: pos.left }}
436+
></div>
437+
);
438+
})}
395439
</div>
396440
</div>
397441
);

0 commit comments

Comments
 (0)