From 580b0b8f7c4982f32db30efd3ac263d63f3ebdff Mon Sep 17 00:00:00 2001 From: Armaan Jain <84474476+Developing-Gamer@users.noreply.github.com> Date: Tue, 11 Nov 2025 08:51:33 +0530 Subject: [PATCH] Launch checklist redesign (#1009) https://www.loom.com/share/952ec76dff514ba99be9d90e0e9625f8 ## Summary by CodeRabbit * **New Features** * Auto-expand next checklist section and animated progress; per-task expand/collapse controls. * OAuth provider guides now in tabbed view with inline callback URLs. * **UI/UX Improvements** * Status badges restyled into card visuals with improved dark-mode icon coloring. * Redesigned checklist header, compact progress bar, updated production card and confetti celebration. * New decorative rainbow-beam animation. * **Documentation** * Updated Convex integration template reference. --- > [!NOTE] > Redesigns the Launch Checklist with expandable task cards, auto-focus on next task, animated progress, OAuth/email help panels, confetti on enabling production mode, and adds rainbow-beam CSS; updates Convex guide link. > > - **Frontend (dashboard)** > - **Launch Checklist UI** (`apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/launch-checklist/page-client.tsx`): > - New expandable `TaskCard` sections with keyboard/ARIA toggles and per-task completion badge. > - Auto-expands the section containing the next task; animated progress bar in header. > - CTA reworked with "Up next" indicator and rainbow-beam button effect. > - OAuth provider help moved to collapsible tabbed guides with inline callback URLs. > - Email setup help as a collapsible step list; production mode section refined with switch and updated footer. > - Confetti animation when `project.isProductionMode` toggles to true. > - Improved dark-mode and status styling for icons/cards. > - **Styles** > - `apps/dashboard/src/app/globals.css`: Adds rainbow-beam CSS variables and `@keyframes rainbow-beam` animation. > - **Docs** > - `docs/content/docs/(guides)/others/convex.mdx`: Updates template link to a production-ready Stack-Auth + Convex + Shadcn repo. > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit bfe629b2c22a547305f0a5d28af3c0af69f87d87. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). --------- Co-authored-by: Konsti Wohlwend --- .../launch-checklist/page-client.tsx | 543 ++++++++++++------ apps/dashboard/src/app/globals.css | 15 + docs/content/docs/(guides)/others/convex.mdx | 2 +- 3 files changed, 398 insertions(+), 162 deletions(-) diff --git a/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/launch-checklist/page-client.tsx b/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/launch-checklist/page-client.tsx index 61ec893e5..f1cb8c90d 100644 --- a/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/launch-checklist/page-client.tsx +++ b/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/launch-checklist/page-client.tsx @@ -4,6 +4,7 @@ import { InlineCode } from "@/components/inline-code"; import { StyledLink } from "@/components/link"; import { useRouter } from "@/components/router"; import { SettingSwitch } from "@/components/settings"; +import { runAsynchronously } from "@stackframe/stack-shared/dist/utils/promises"; import { Badge, Button, @@ -13,16 +14,16 @@ import { CardFooter, CardHeader, CardTitle, - Progress, Tabs, TabsContent, TabsList, TabsTrigger, Typography, - cn, + cn } from "@stackframe/stack-ui"; -import { CheckCircle2, Circle } from "lucide-react"; -import { useState } from "react"; +import * as confetti from "canvas-confetti"; +import { CheckCircle2, ChevronDown, ChevronUp, Circle, Clock } from "lucide-react"; +import { useEffect, useRef, useState } from "react"; import { AppEnabledGuard } from "../app-enabled-guard"; import { PageLayout } from "../page-layout"; import { useAdminApp } from "../use-admin-app"; @@ -146,30 +147,20 @@ type LaunchTask = { const STATUS_META: Record< LaunchTaskStatus, { - badgeLabel: string, - badgeVariant: React.ComponentProps["variant"], - badgeClass?: string, cardClass: string, inactiveIcon: string, } > = { done: { - badgeLabel: "Complete", - badgeVariant: "default", - badgeClass: "bg-emerald-500 text-white", - cardClass: "border-emerald-200 bg-emerald-50", - inactiveIcon: "text-emerald-500", + cardClass: "border-primary/30 bg-background transition-all duration-300 hover:shadow-lg dark:border-primary/40 dark:shadow-primary/5", + inactiveIcon: "text-emerald-500 dark:text-emerald-400", }, action: { - badgeLabel: "Up next", - badgeVariant: "outline", - cardClass: "border-border bg-background", + cardClass: "border-primary/30 bg-background transition-all duration-300 hover:shadow-lg dark:border-primary/40 dark:shadow-primary/5", inactiveIcon: "text-muted-foreground", }, blocked: { - badgeLabel: "Resolve", - badgeVariant: "outline", - cardClass: "border-border bg-background", + cardClass: "border-primary/30 bg-background transition-all duration-300 hover:shadow-lg dark:border-primary/40 dark:shadow-primary/5", inactiveIcon: "text-muted-foreground", }, }; @@ -182,16 +173,16 @@ function ChecklistRow(props: { }) { const Icon = props.done ? CheckCircle2 : Circle; const iconClass = props.done - ? "text-emerald-500" + ? "text-emerald-500 dark:text-emerald-400" : STATUS_META[props.status].inactiveIcon; return ( -
  • - -
    - +
  • + +
    +

    {props.title} - +

    {props.detail}
  • @@ -202,46 +193,106 @@ function TaskCard(props: { task: LaunchTask, children?: React.ReactNode, footer?: React.ReactNode, + isExpanded: boolean, + onToggle: () => void, }) { const meta = STATUS_META[props.task.status]; + const allItemsDone = props.task.items.every((item) => item.done); return ( - - -
    - {props.task.title} - {props.task.subtitle} - - {meta.badgeLabel} - + + { + if (e.key === "Enter" || e.key === " ") { + e.preventDefault(); + props.onToggle(); + } + }} + > +
    +
    +
    + {props.task.title} + {allItemsDone && ( + + + Complete + + )} +
    + + {props.task.subtitle} + +
    +
    - -
      - {props.task.items.map((item) => ( - - ))} -
    - {props.children} -
    - - {props.footer ?? - (props.task.status === "done" ? ( - - ) : ( - - ))} - +
    +
    + +
      + {props.task.items.map((item) => ( + + ))} +
    + {props.children} +
    + + {props.footer ?? ( + + )} + +
    +
    ); } @@ -255,6 +306,8 @@ export default function PageClient() { const [showOauthGuides, setShowOauthGuides] = useState(false); const [showEmailHelp, setShowEmailHelp] = useState(false); + const [animatedProgress, setAnimatedProgress] = useState(0); + const prevProductionModeRef = useRef(undefined); const domainConfigs = project.config.domains; const hasDomainConfigured = domainConfigs.length > 0; @@ -263,7 +316,7 @@ export default function PageClient() { const isSharedEmailServer = emailServerConfig.isShared; const oauthProviders = project.config.oauthProviders; const sharedOAuthProviders = oauthProviders.filter( - (provider) => provider.type === "shared", + (provider: { type: string }) => provider.type === "shared", ); const baseProjectPath = `/projects/${project.id}`; @@ -274,7 +327,7 @@ export default function PageClient() { done: hasDomainConfigured, detail: hasDomainConfigured ? (
    - {domainConfigs.slice(0, 3).map(({ domain }) => ( + {domainConfigs.slice(0, 3).map(({ domain }: { domain: string }) => ( {domain} ))} {domainConfigs.length > 3 && ( @@ -310,7 +363,7 @@ export default function PageClient() { }; const sharedProviderLabels = sharedOAuthProviders.map( - (provider) => PROVIDER_GUIDES.get(provider.id)?.label ?? provider.id, + (provider: { id: string }) => PROVIDER_GUIDES.get(provider.id)?.label ?? provider.id, ); const oauthTask: LaunchTask = { id: "oauth", @@ -335,7 +388,7 @@ export default function PageClient() { Swap custom keys for:
    - {sharedProviderLabels.map((label) => ( + {sharedProviderLabels.map((label: string) => ( {label} @@ -402,7 +455,7 @@ export default function PageClient() { Fix these before enabling production mode:
      - {productionModeErrors.map((error) => ( + {productionModeErrors.map((error: { message: string, relativeFixUrl: string }) => (
    • {error.message}{" "} @@ -449,81 +502,197 @@ export default function PageClient() { value: allItems.length === 0 ? 100 : (completed / allItems.length) * 100, }; + // Track which section is expanded (only one at a time, excluding "Checks complete") + const [expandedTaskId, setExpandedTaskId] = useState(null); + const prevNextTaskIdRef = useRef(null); + + // Auto-expand the section containing the next task on mount and when next task changes + useEffect(() => { + const nextTaskId = next?.task.id ?? null; + const prevNextTaskId = prevNextTaskIdRef.current; + + // Only auto-expand if: + // 1. This is the initial load (prevNextTaskId is null), OR + // 2. The next task actually changed to a different section + if (prevNextTaskId === null || (nextTaskId !== null && nextTaskId !== prevNextTaskId)) { + if (nextTaskId !== null) { + setExpandedTaskId(nextTaskId); + } else { + // If all tasks are done, collapse all sections + setExpandedTaskId(null); + } + } + + // Update the ref to track the current next task + prevNextTaskIdRef.current = nextTaskId; + }, [next]); + + const handleTaskToggle = (taskId: string) => { + setExpandedTaskId((current) => (current === taskId ? null : taskId)); + }; + + // Animate progress bar on mount and when progress changes + useEffect(() => { + const timer = setTimeout(() => { + setAnimatedProgress(checklistProgress.value); + }, 100); + return () => clearTimeout(timer); + }, [checklistProgress.value]); + + // Trigger confetti when production mode is turned on + useEffect(() => { + const currentProductionMode = project.isProductionMode; + const prevProductionMode = prevProductionModeRef.current; + + // Only trigger confetti when production mode changes from false to true + if (prevProductionMode !== undefined && !prevProductionMode && currentProductionMode) { + // Create a confetti effect dropping from the top + const duration = 3000; + const animationEnd = Date.now() + duration; + const defaults = { startVelocity: 30, spread: 360, ticks: 60, zIndex: 9999 }; + + function randomInRange(min: number, max: number) { + return Math.random() * (max - min) + min; + } + + const interval = setInterval(() => { + const timeLeft = animationEnd - Date.now(); + + if (timeLeft <= 0) { + clearInterval(interval); + return; + } + + const particleCount = 50 * (timeLeft / duration); + const result = confetti.default({ + ...defaults, + particleCount, + origin: { x: randomInRange(0.1, 0.9), y: 0 }, + }); + if (result) { + runAsynchronously(result, { noErrorLogging: true }); + } + }, 250); + + // Cleanup interval on unmount or when production mode changes + return () => { + clearInterval(interval); + }; + } + + // Update the ref to track the current production mode state + prevProductionModeRef.current = currentProductionMode; + }, [project.isProductionMode]); + const providerEntries = Array.from(PROVIDER_GUIDES.entries()); const defaultProviderTab = providerEntries[0]?.[0] ?? "google"; const oauthChildren = sharedOAuthProviders.length > 0 ? ( -
      - - Need new credentials? - - - Create an OAuth app with the provider, set Stack as the callback URL, - then paste the client ID and secret into the provider settings. - - +
      +
      - {showOauthGuides ? "Hide provider guides" : "Show provider guides"} - - {showOauthGuides && ( - - +
      + + + {providerEntries.map(([id, guide]) => ( + + {guide.label} + + ))} + {providerEntries.map(([id, guide]) => ( - - {guide.label} - - ))} - - {providerEntries.map(([id, guide]) => ( - - - - {guide.label} setup guide + + + View {guide.label} documentation → - - - Callback URL: - - {guide.callbackUrl} - - ))} - - )} +
      +

      Callback URL

      + {guide.callbackUrl} +
      + + ))} + +
      +
    ) : undefined; const emailChildren = ( -
    - - Quick email setup - - +
    +
    - {showEmailHelp ? "Hide setup steps" : "How do I connect my server?"} - - {showEmailHelp && ( -
      -
    1. Verify a sending domain with your email provider.
    2. -
    3. - Switch Stack to Custom SMTP or Resend, then paste the credentials. -
    4. -
    5. Send a test email to confirm delivery.
    6. -
    - )} +
    +
      +
    1. Verify a sending domain with your email provider.
    2. +
    3. + Switch Stack to Custom SMTP or Resend, then paste the credentials. +
    4. +
    5. Send a test email to confirm delivery.
    6. +
    +
    +
    ); const productionChildren = ( -
    +
    {productionTaskStatus === "done" && ( - + Production mode is live. - + )} - +
    +
    + + + Up next: {checklistProgress.next.item.title} + +
    +
    + {/* Rainbow beam effect - outer glow */} +
    + {/* Rainbow beam effect - sharp edge */} +
    + + +
    +
    ) : ( - - All checks are green. Enable production mode when you are ready. - +
    + + + All checks complete. Enable production mode when ready. + +
    )}
    - {orderedTasks.map((task) => { + {orderedTasks.map((task, index) => { const extras = taskExtras[task.id] ?? {}; + const isExpanded = expandedTaskId === task.id; return ( - + className="animate-in fade-in slide-in-from-bottom-4" + style={{ + animationDelay: `${Math.min(index * 50, 300)}ms`, + animationDuration: "500ms", + animationFillMode: "backwards", + }} + > + handleTaskToggle(task.id)} + {...extras} + /> +
    ); })}
    diff --git a/apps/dashboard/src/app/globals.css b/apps/dashboard/src/app/globals.css index 6569d3703..598eec1c6 100644 --- a/apps/dashboard/src/app/globals.css +++ b/apps/dashboard/src/app/globals.css @@ -41,6 +41,9 @@ --ring: 240 10% 3.9%; --radius: 0.5rem; + + --rainbow-beam-blur: linear-gradient(90deg, #3b82f6, #8b5cf6, #ec4899, #f59e0b, #10b981, #3b82f6); + --rainbow-beam-sharp: linear-gradient(90deg, #60a5fa, #a78bfa, #f472b6, #fbbf24, #34d399, #60a5fa); } .dark { @@ -226,6 +229,18 @@ body:has(.show-site-loading-indicator) .site-loading-indicator { } } +@keyframes rainbow-beam { + 0% { + background-position: 0% 50%; + } + 50% { + background-position: 100% 50%; + } + 100% { + background-position: 0% 50%; + } +} + /* Pacifica styles */ [data-pacifica-surface] { diff --git a/docs/content/docs/(guides)/others/convex.mdx b/docs/content/docs/(guides)/others/convex.mdx index 8467f83a8..ebbed4962 100644 --- a/docs/content/docs/(guides)/others/convex.mdx +++ b/docs/content/docs/(guides)/others/convex.mdx @@ -77,5 +77,5 @@ export const myQuery = query({ }); ``` -You can find the full example [here on GitHub](https://github.com/stack-auth/convex-next-template). +You can find the production-ready template with Stack-Auth, Convex & Shadcn pre-configured [here on GitHub](https://github.com/developing-gamer/next-convex-stack-template).