Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 43 additions & 12 deletions apps/code/src/app/dashboard/DashboardShell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
Stamp,
UserRound,
} from "lucide-react";
import { motion, useReducedMotion } from "motion/react";
import dynamic from "next/dynamic";
import Link from "next/link";
import { usePathname, useRouter, useSearchParams } from "next/navigation";
Expand Down Expand Up @@ -102,8 +103,8 @@ const setupActivationCopy: Record<
"DevPass will activate as soon as Stripe confirms the payment.",
},
success: {
title: "DevPass activated",
description: "Refreshing your dashboard.",
title: "Welcome aboard",
description: "Loading your dashboard.",
},
error: {
title: "Activation failed",
Expand Down Expand Up @@ -217,6 +218,7 @@ export default function DashboardShell({
);
const [setupActivationStatus, setSetupActivationStatus] =
useState<SetupActivationStatus | null>(null);
const reduceMotion = useReducedMotion();
const activeSetupSession = useRef<string | null>(null);
const finalizeDevPlanRef = useRef(finalizeMutation.mutateAsync);
const purchaseTrackedSession = useRef<string | null>(null);
Expand Down Expand Up @@ -308,13 +310,12 @@ export default function DashboardShell({
};

finalizeDevPlan()
.then((result) => {
.then(async (result) => {
if (signal.aborted) {
return;
}
if (result?.status === "ok" || result?.status === "already_processed") {
setSetupActivationStatus("success");
toast.success("DevPass activated");
if (purchaseTrackedSession.current !== sessionId) {
purchaseTrackedSession.current = sessionId;
const tier = devPlanStatusRef.current?.devPlan;
Expand All @@ -336,6 +337,9 @@ export default function DashboardShell({
return Array.isArray(key) && key[1] === "/dev-plans/status";
},
});
// Hold the success screen long enough for the stamp to land and
// be read before the setup param is cleared and the card unmounts.
await wait(1600, signal);
} else if (result?.status === "payment_pending") {
shouldClearSetupParam = false;
setSetupActivationStatus("processing");
Expand Down Expand Up @@ -512,14 +516,41 @@ export default function DashboardShell({
{activeSetupActivationCopy ? (
<main className="container mx-auto flex min-h-[calc(100vh-120px)] max-w-3xl items-center justify-center px-4 py-12">
<div className="w-full rounded-xl border bg-background p-8 text-center shadow-sm sm:p-12">
<div className="mx-auto mb-6 flex h-20 w-20 items-center justify-center rounded-full bg-muted">
<Loader2
className={cn(
"h-10 w-10 text-foreground",
activeSetupActivationStatus !== "error" && "animate-spin",
)}
/>
</div>
{activeSetupActivationStatus === "success" ? (
<motion.div
initial={
reduceMotion
? { opacity: 0 }
: { opacity: 0, scale: 2.4, rotate: -18 }
}
animate={
reduceMotion
? { opacity: 1 }
: { opacity: 1, scale: 1, rotate: -8 }
}
transition={{ type: "spring", duration: 0.4 }}
className="mx-auto mb-6 flex justify-center"
>
<div className="rounded-md border-4 border-double border-emerald-700/80 px-6 py-3 text-center font-mono uppercase text-emerald-800 mix-blend-multiply dark:border-emerald-400/80 dark:text-emerald-300 dark:mix-blend-screen">
<div className="flex items-center justify-center gap-2 text-base font-bold tracking-[0.3em]">
<Stamp className="h-4 w-4" />
DevPass activated
</div>
<div className="mt-0.5 text-[9px] tracking-[0.2em]">
Visa granted · welcome aboard
</div>
</div>
</motion.div>
) : (
<div className="mx-auto mb-6 flex h-20 w-20 items-center justify-center rounded-full bg-muted">
<Loader2
className={cn(
"h-10 w-10 text-foreground",
activeSetupActivationStatus !== "error" && "animate-spin",
)}
/>
</div>
)}
<h1 className="text-2xl font-semibold sm:text-3xl">
{activeSetupActivationCopy.title}
</h1>
Expand Down
21 changes: 15 additions & 6 deletions apps/code/src/app/dashboard/components/QuickStart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -144,12 +144,21 @@ export default function QuickStart({ apiKey }: { apiKey: string }) {
onClick={handleCopy}
className="inline-flex items-center gap-1 text-xs text-muted-foreground hover:text-foreground transition-colors"
>
{copied ? (
<Check className="h-3 w-3" />
) : (
<Copy className="h-3 w-3" />
)}
{copied ? "Copied" : "Copy"}
<span className="relative flex h-3 w-3 items-center justify-center">
<Copy
className={`h-3 w-3 transition-[opacity,transform] duration-150 ease-out motion-reduce:transition-[opacity] ${
copied ? "scale-[0.45] opacity-0" : "scale-100 opacity-100"
}`}
/>
<Check
className={`absolute h-3 w-3 transition-[opacity,transform] duration-150 ease-out motion-reduce:transition-[opacity] ${
copied ? "scale-100 opacity-100" : "scale-[0.45] opacity-0"
}`}
/>
</span>
<span className="min-w-[6ch] text-left">
{copied ? "Copied" : "Copy"}
</span>
</button>
</div>

Expand Down
62 changes: 45 additions & 17 deletions apps/code/src/app/dashboard/components/ResetPassCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import { useQueryClient } from "@tanstack/react-query";
import { ArrowUpRight, Loader2, Plane, Stamp } from "lucide-react";
import { AnimatePresence, motion } from "motion/react";
import { AnimatePresence, motion, useReducedMotion } from "motion/react";
import Link from "next/link";
import { usePostHog } from "posthog-js/react";
import { useState } from "react";
Expand Down Expand Up @@ -114,7 +114,10 @@ export default function ResetPassCard({
const queryClient = useQueryClient();
const { uiUrl, posthogKey } = useAppConfig();
const posthog = usePostHog();
const [justStamped, setJustStamped] = useState(false);
const [stampOverlay, setStampOverlay] = useState<
{ kind: "redeemed" } | { kind: "purchased"; amount: number } | null
>(null);
const reduceMotion = useReducedMotion();

const available = includedRemaining + purchased;
const nothingToReset = premiumCreditsUsed <= 0;
Expand Down Expand Up @@ -158,8 +161,8 @@ export default function ResetPassCard({
"/dev-plans/reset-pass/redeem",
{
onSuccess: async () => {
setJustStamped(true);
setTimeout(() => setJustStamped(false), 2200);
setStampOverlay({ kind: "redeemed" });
setTimeout(() => setStampOverlay(null), 2200);
Comment on lines +164 to +165

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Overlapping redeem/purchase actions can clear the overlay early.

Each onSuccess schedules an independent, untracked setTimeout(..., 2200). If a user redeems and then purchases (or vice versa) within 2200ms, the second overlay ("purchased") is shown, but the first timer still fires at its original 2200ms mark and clears stampOverlay prematurely — before the second overlay's own 2200ms has elapsed. A later, now-stale second timeout then fires as a no-op.

🐛 Proposed fix: track and clear the pending timeout
+	const stampTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
+
+	const showStampOverlay = (
+		overlay: NonNullable<typeof stampOverlay>,
+	) => {
+		if (stampTimeoutRef.current) {
+			clearTimeout(stampTimeoutRef.current);
+		}
+		setStampOverlay(overlay);
+		stampTimeoutRef.current = setTimeout(() => setStampOverlay(null), 2200);
+	};
+
+	useEffect(() => {
+		return () => {
+			if (stampTimeoutRef.current) {
+				clearTimeout(stampTimeoutRef.current);
+			}
+		};
+	}, []);
+
 	const redeemMutation = api.useMutation(
 		"post",
 		"/dev-plans/reset-pass/redeem",
 		{
 			onSuccess: async () => {
-				setStampOverlay({ kind: "redeemed" });
-				setTimeout(() => setStampOverlay(null), 2200);
+				showStampOverlay({ kind: "redeemed" });
 				await invalidateStatus();
 			},

Apply the analogous change to purchaseMutation.onSuccess, replacing its setStampOverlay/setTimeout pair with showStampOverlay({ kind: "purchased", amount: data.amount }).

Also applies to: 179-180, 208-233

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/code/src/app/dashboard/components/ResetPassCard.tsx` around lines 164 -
165, Replace the direct setStampOverlay/setTimeout pairs in both redeem and
purchase success handlers with a shared showStampOverlay helper that clears the
existing pending timeout before scheduling a new 2200ms dismissal; update
purchaseMutation.onSuccess to call showStampOverlay({ kind: "purchased", amount:
data.amount }) and preserve the corresponding redeemed overlay payload.

await invalidateStatus();
},
onError: () => {
Expand All @@ -173,9 +176,8 @@ export default function ResetPassCard({
"/dev-plans/reset-pass/purchase",
{
onSuccess: async (data) => {
toast.success("Reset Pass stamped into your passport", {
description: `$${data.amount} was charged to your saved payment method.`,
});
setStampOverlay({ kind: "purchased", amount: data.amount });
setTimeout(() => setStampOverlay(null), 2200);
await invalidateStatus();
},
onError: (error) => {
Expand Down Expand Up @@ -203,24 +205,50 @@ export default function ResetPassCard({

return (
<div className="relative mt-4 overflow-hidden rounded-lg border border-dashed border-stone-400/70 bg-stone-50/70 dark:border-stone-600/70 dark:bg-stone-900/30">
{/* Full-card stamp slammed on a successful redeem */}
{/* Full-card stamp slammed on a successful redeem or purchase */}
<AnimatePresence>
{justStamped && (
{stampOverlay && (
<motion.div
initial={{ opacity: 0, scale: 2.2, rotate: -18 }}
animate={{ opacity: 1, scale: 1, rotate: -8 }}
key={stampOverlay.kind}
initial={
reduceMotion
? { opacity: 0 }
: stampOverlay.kind === "redeemed"
? { opacity: 0, scale: 2.2, rotate: -18 }
: { opacity: 0, scale: 2.2, rotate: 16 }
}
animate={
reduceMotion
? { opacity: 1 }
: {
opacity: 1,
scale: 1,
rotate: stampOverlay.kind === "redeemed" ? -8 : 6,
}
}
exit={{ opacity: 0 }}
transition={{ type: "spring", duration: 0.4 }}
className="pointer-events-none absolute inset-0 z-10 flex items-center justify-center"
>
<div className="rounded-md border-4 border-double border-emerald-700/80 px-6 py-2 text-center font-mono uppercase text-emerald-800 mix-blend-multiply dark:border-emerald-400/80 dark:text-emerald-300 dark:mix-blend-screen">
<div className="text-sm font-bold tracking-[0.3em]">
Allowance restored
{stampOverlay.kind === "redeemed" ? (
<div className="rounded-md border-4 border-double border-emerald-700/80 px-6 py-2 text-center font-mono uppercase text-emerald-800 mix-blend-multiply dark:border-emerald-400/80 dark:text-emerald-300 dark:mix-blend-screen">
<div className="text-sm font-bold tracking-[0.3em]">
Allowance restored
</div>
<div className="mt-0.5 text-[9px] tracking-[0.2em]">
Fresh week · full premium limit
</div>
</div>
<div className="mt-0.5 text-[9px] tracking-[0.2em]">
Fresh week · full premium limit
) : (
<div className="rounded-md border-4 border-double border-indigo-700/80 px-6 py-2 text-center font-mono uppercase text-indigo-800 mix-blend-multiply dark:border-indigo-400/80 dark:text-indigo-300 dark:mix-blend-screen">
<div className="text-sm font-bold tracking-[0.3em]">
Pass acquired
</div>
<div className="mt-0.5 text-[9px] tracking-[0.2em]">
${stampOverlay.amount} charged · redeem anytime
</div>
</div>
</div>
)}
</motion.div>
)}
</AnimatePresence>
Expand Down
24 changes: 20 additions & 4 deletions apps/code/src/app/data/[year]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -88,8 +88,20 @@ function ScoreMeter({ label, value }: { label: string; value: number }) {

function ModelRow({ model, rank }: { model: ModelSurveyModel; rank: number }) {
const topUseCase = model.useCases[0];
// Only the rows near the fold stagger in; deep rows render instantly so a
// long registry never feels like it's loading.
const staggered = rank <= 10;
const staggerStep = (rank - 1) * 70;
const delayMs = 140 + staggerStep;
return (
<div className="border-b border-dashed border-stone-300/80 px-4 py-5 last:border-b-0 dark:border-stone-700/80 sm:flex sm:flex-wrap sm:items-center sm:gap-x-6 sm:gap-y-4 sm:px-6">
<div
className={`border-b border-dashed border-stone-300/80 px-4 py-5 last:border-b-0 dark:border-stone-700/80 sm:flex sm:flex-wrap sm:items-center sm:gap-x-6 sm:gap-y-4 sm:px-6 ${
staggered
? "motion-safe:animate-in motion-safe:fade-in motion-safe:slide-in-from-bottom-3 motion-safe:fill-mode-both"
: ""
}`}
style={staggered ? { animationDelay: `${delayMs}ms` } : undefined}
>
<div className="flex min-w-0 items-center gap-4 sm:flex-1">
<div
className={
Expand Down Expand Up @@ -186,7 +198,7 @@ export default async function CensusPage({
{/* Hero */}
<section className="relative overflow-hidden border-b">
<div className="pointer-events-none absolute inset-0 bg-[radial-gradient(ellipse_55%_55%_at_50%_-5%,_var(--tw-gradient-stops))] from-emerald-500/10 via-transparent to-transparent" />
<div className="container relative mx-auto max-w-3xl px-4 pt-16 pb-12 text-center sm:pt-20">
<div className="container relative mx-auto max-w-3xl px-4 pt-16 pb-12 text-center motion-safe:animate-in motion-safe:fade-in motion-safe:slide-in-from-bottom-3 motion-safe:fill-mode-both sm:pt-20">
<div className="mb-4 inline-flex items-center gap-1.5 rounded-full border border-emerald-500/30 bg-emerald-500/10 px-3.5 py-1.5 font-mono text-xs font-medium uppercase tracking-[0.15em] text-emerald-600 dark:text-emerald-400">
<ClipboardCheck className="h-3.5 w-3.5" />
{isCurrentYear
Expand Down Expand Up @@ -232,8 +244,12 @@ export default async function CensusPage({
label: "Models on the registry",
value: results.totalModelsRated,
},
].map((stat) => (
<div key={stat.label}>
].map((stat, index) => (
<div
key={stat.label}
className="motion-safe:animate-in motion-safe:fade-in motion-safe:slide-in-from-bottom-3 motion-safe:fill-mode-both"
style={{ animationDelay: `${index * 70}ms` }}
>
<div className="font-mono text-2xl font-bold tabular-nums sm:text-3xl">
{stat.value.toLocaleString()}
</div>
Expand Down
23 changes: 17 additions & 6 deletions apps/code/src/components/CodingModelsShowcase.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -282,14 +282,25 @@ export function CodingModelsShowcase({
<button
type="button"
onClick={() => copyToClipboard(model.id)}
className="shrink-0 p-1 rounded hover:bg-muted transition-colors"
className="shrink-0 p-1 rounded hover:bg-muted transition-[background-color,color,transform] duration-150 ease-out active:scale-[0.98]"
title="Copy model ID"
>
{copiedModel === model.id ? (
<Check className="h-3 w-3 text-green-600" />
) : (
<Copy className="h-3 w-3 text-muted-foreground" />
)}
<span className="relative flex h-3 w-3 items-center justify-center">
<Copy
className={`h-3 w-3 text-muted-foreground transition-[opacity,transform] duration-150 ease-out motion-reduce:transition-[opacity] ${
copiedModel === model.id
? "scale-[0.45] opacity-0"
: "scale-100 opacity-100"
}`}
/>
<Check
className={`absolute h-3 w-3 text-green-600 transition-[opacity,transform] duration-150 ease-out motion-reduce:transition-[opacity] ${
copiedModel === model.id
? "scale-100 opacity-100"
: "scale-[0.45] opacity-0"
}`}
/>
</span>
</button>
</div>

Expand Down
2 changes: 1 addition & 1 deletion apps/code/src/components/ui/button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import * as React from "react";
import { cn } from "@/lib/utils";

const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive active:scale-[0.98]",
{
variants: {
variant: {
Expand Down
33 changes: 30 additions & 3 deletions apps/playground/src/components/ai-elements/code-block.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"use client";

import { CheckIcon, CopyIcon } from "lucide-react";
import { AnimatePresence, motion, useReducedMotion } from "motion/react";
import {
createContext,
memo,
Expand Down Expand Up @@ -461,6 +462,7 @@ export const CodeBlockCopyButton = ({
const [isCopied, setIsCopied] = useState(false);
const timeoutRef = useRef<number>(0);
const { code } = use(CodeBlockContext);
const reduceMotion = useReducedMotion();

const copyToClipboard = useCallback(async () => {
if (typeof window === "undefined" || !navigator?.clipboard?.writeText) {
Expand Down Expand Up @@ -490,8 +492,6 @@ export const CodeBlockCopyButton = ({
[],
);

const Icon = isCopied ? CheckIcon : CopyIcon;

return (
<Button
className={cn("shrink-0", className)}
Expand All @@ -500,7 +500,34 @@ export const CodeBlockCopyButton = ({
variant="ghost"
{...props}
>
{children ?? <Icon size={14} />}
{children ?? (
<span className="relative flex size-4 items-center justify-center">
<AnimatePresence initial={false}>
<motion.span
key={isCopied ? "check" : "copy"}
initial={
reduceMotion
? { opacity: 0 }
: { opacity: 0, transform: "scale(0.45) rotate(-18deg)" }
}
animate={
reduceMotion
? { opacity: 1 }
: { opacity: 1, transform: "scale(1) rotate(0deg)" }
}
exit={
reduceMotion
? { opacity: 0 }
: { opacity: 0, transform: "scale(0.45) rotate(18deg)" }
}
transition={{ duration: 0.16, ease: "easeOut" }}
className="absolute inset-0 flex items-center justify-center"
>
{isCopied ? <CheckIcon size={14} /> : <CopyIcon size={14} />}
</motion.span>
</AnimatePresence>
</span>
)}
</Button>
);
};
Expand Down
Loading
Loading