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
186 changes: 186 additions & 0 deletions app/_components/AuthBrandPanel.tsx
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
import { Fragment, type ReactNode } from "react";
import type { Stage } from "@/lib/pets/growth";
import { PetArt } from "@/app/dashboard/_components/PetArt";
import { fadeUp } from "@/lib/ui/motion";
import { Logo } from "./Logo";

const STAGES: { stage: Stage; label: string }[] = [
{ stage: "egg", label: "Egg" },
{ stage: "hatchling", label: "Hatchling" },
{ stage: "juvenile", label: "Juvenile" },
{ stage: "adult", label: "Adult" },
];

// The only two sizings StageStrip is ever asked for — desktop brand panel
// vs. the compact mobile hero. A closed set of variants instead of five
// independent style props means there's no way to call it with a mismatched
// combination (e.g. mobile-sized art with desktop-sized chevrons).
const STAGE_STRIP_VARIANTS = {
full: {
artClassName: "h-20 w-auto",
tileClassName: "px-3 py-5",
chevronSize: 16,
gapClassName: "gap-2.5",
labels: true,
},
compact: {
artClassName: "h-9 w-auto",
tileClassName: "px-1.5 py-2.5",
chevronSize: 12,
gapClassName: "gap-1.5",
labels: false,
},
} as const;

// Shared growth-stage row, sized differently for the desktop brand panel vs.
// the compact mobile hero below. Labels are dropped on mobile — four
// two-word labels don't fit at that width without wrapping into the chevrons.
function StageStrip({
variant,
}: {
variant: keyof typeof STAGE_STRIP_VARIANTS;
}) {
const { artClassName, tileClassName, chevronSize, gapClassName, labels } =
STAGE_STRIP_VARIANTS[variant];

return (
<div className={`flex items-stretch ${gapClassName}`}>
{STAGES.map(({ stage, label }, i) => (
<Fragment key={stage}>
<div
className={`flex flex-1 flex-col items-center justify-center gap-2 rounded-xl border border-dash-border bg-dash-card/70 ${tileClassName}`}
>
<PetArt stage={stage} mood="healthy" className={artClassName} />
{labels && (
<span className="text-xs font-semibold text-dash-muted">
{label}
</span>
)}
</div>
{i < STAGES.length - 1 && (
<svg
width={chevronSize}
height={chevronSize}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
className="shrink-0 self-center text-dash-border"
aria-hidden="true"
>
<path d="M9 6l6 6-6 6" />
</svg>
)}
</Fragment>
))}
</div>
);
}

// Shared left panel for sign-in/sign-up — hidden below lg, where
// MobileAuthHero (below) takes over. Doubles as the app's de facto landing
// page for signed-out visitors, since "/" redirects straight here instead of
// showing separate marketing content.
export function AuthBrandPanel() {
const headline = fadeUp(80);

return (
<div className="relative hidden flex-col justify-center gap-12 overflow-hidden border-r border-dash-border bg-gradient-to-br from-dash-card to-dash-bg px-16 py-16 lg:flex">
<div
aria-hidden="true"
className="pointer-events-none absolute -top-32 -right-32 h-96 w-96 rounded-full bg-dash-accent/10 blur-3xl"
/>

<div className="mx-auto flex w-full max-w-xl flex-col gap-12">
<div className={`flex items-center gap-2.5 ${fadeUp().className}`}>
<Logo className="h-9 w-9" />
<span className="text-xl font-bold text-dash-heading">
Commit Pet
</span>
</div>

<div
className={`flex flex-col gap-4 ${headline.className}`}
style={headline.style}
>
<h1 className="max-w-[15ch] text-5xl leading-[1.1] font-bold tracking-tight text-dash-heading">
A pet that grows as you commit.
</h1>
<p className="max-w-[38ch] text-lg text-dash-muted">
Install it on a GitHub repo, watch it grow from egg to adult, and
retire once you ship.
</p>
</div>

<div {...fadeUp(160)}>
<StageStrip variant="full" />
</div>
</div>
</div>
);
}

// Compact hero for the auth pages below `lg`, shown above the Clerk widget
// instead of just a bare logo. Same brand moment as AuthBrandPanel (headline,
// subtext, growth-stage strip) at mobile scale.
export function MobileAuthHero() {
const headline = fadeUp(80);
const stageStrip = fadeUp(160);

return (
<div className="relative flex flex-col items-center gap-5 overflow-hidden bg-gradient-to-b from-dash-card to-dash-bg px-6 pt-12 pb-8 lg:hidden">
<div
aria-hidden="true"
className="pointer-events-none absolute -top-20 left-1/2 h-64 w-64 -translate-x-1/2 rounded-full bg-dash-accent/10 blur-3xl"
/>

<div className={`flex items-center gap-2 ${fadeUp().className}`}>
<Logo className="h-7 w-7" />
<span className="text-base font-bold text-dash-heading">
Commit Pet
</span>
</div>

<div
className={`flex flex-col items-center gap-2 text-center ${headline.className}`}
style={headline.style}
>
<h1 className="max-w-[16ch] text-[26px] leading-[1.2] font-bold tracking-tight text-dash-heading">
A pet that grows as you commit.
</h1>
<p className="max-w-[34ch] text-sm text-dash-muted">
Install it on a GitHub repo, watch it grow from egg to adult, and
retire once you ship.
</p>
</div>

<div
className={`w-full max-w-xs ${stageStrip.className}`}
style={stageStrip.style}
>
<StageStrip variant="compact" />
</div>
</div>
);
}

// Shared page shell for /sign-in and /sign-up — desktop split panel plus
// mobile hero, with the Clerk widget (SignIn or SignUp) as children. Was
// duplicated verbatim in both page files; this is the one copy.
export function AuthShell({ children }: { children: ReactNode }) {
const widget = fadeUp(200);

return (
<div className="grid flex-1 lg:grid-cols-[1.15fr_1fr]">
<AuthBrandPanel />
<div className="flex flex-1 flex-col bg-dash-bg">
<MobileAuthHero />
<div className="flex flex-1 flex-col items-center justify-center px-6 py-10 lg:py-16">
<div className={widget.className} style={widget.style}>
{children}
</div>
</div>
</div>
</div>
);
}
38 changes: 8 additions & 30 deletions app/dashboard/[repoId]/page.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,14 +3,10 @@ import Link from "next/link";
import { notFound } from "next/navigation";
import { getAccessibleInstallationIds } from "@/lib/github/user-auth";
import { getDashboardPet } from "@/lib/pets/dashboard-data";
import { repoShortName } from "@/lib/pets/repo-name";
import { getMcpTokenStatus } from "@/lib/mcp/tokens";
import { Hero } from "../_components/Hero";
import { GrowthCard } from "../_components/GrowthCard";
import { OpenIssuesCard } from "../_components/OpenIssuesCard";
import { HealthCard } from "../_components/HealthCard";
import { BadgeCard } from "../_components/BadgeCard";
import { RepoInfoCard } from "../_components/RepoInfoCard";
import { McpTokenCard } from "../_components/McpTokenCard";
import { fadeUp } from "@/lib/ui/motion";
import { PetDetailSection } from "../_components/PetDetailSection";

export async function generateMetadata({
params,
Expand DownExpand Up@@ -47,38 +43,20 @@ export default async function PetDetailPage({

return (
<div className="flex flex-1 flex-col gap-6 px-6 py-8 sm:px-12">
<div className="flex items-center gap-2 text-[13px]">
<div
className={`flex items-center gap-2 text-[13px] ${fadeUp().className}`}
>
<Link
href="/dashboard"
className="font-semibold text-dash-accent hover:text-[#C2560B]"
>
← Dashboard
</Link>
<span className="text-dash-muted">/</span>
<span className="text-dash-muted">{pet.fullName}</span>
<span className="text-dash-muted">{repoShortName(pet.fullName)}</span>
</div>

<div className="flex flex-col items-start gap-6 lg:flex-row">
<div className="flex min-w-0 flex-1 flex-col gap-6 lg:flex-[2]">
<Hero pet={pet} />
{pet.phase === "development" ? (
<GrowthCard pet={pet} />
) : (
<OpenIssuesCard pet={pet} />
)}
</div>

<div className="flex w-full flex-col gap-6 lg:w-auto lg:flex-1">
<HealthCard pet={pet} />
<BadgeCard pet={pet} />
<McpTokenCard
repoId={pet.repoId}
hasToken={tokenStatus.exists}
lastUsedRelative={tokenStatus.lastUsedRelative}
/>
<RepoInfoCard pet={pet} />
</div>
</div>
<PetDetailSection pet={pet} tokenStatus={tokenStatus} />
</div>
);
}
4 changes: 2 additions & 2 deletions app/dashboard/_components/Bar.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -8,9 +8,9 @@ export function Bar({
}) {
const width = `${Math.round(Math.max(0, Math.min(1, progress)) * 100)}%`;
return (
<div className="h-2.5 overflow-hidden rounded-full bg-dash-track">
<div className="h-2.5 overflow-hidden rounded-full bg-dash-track shadow-[inset_0_1px_2px_rgba(43,33,21,0.1)]">
<div
className={`h-full rounded-full ${fillClassName}`}
className={`h-full rounded-full transition-[width] duration-500 ease-out ${fillClassName}`}
style={{ width }}
/>
</div>
Expand Down
17 changes: 11 additions & 6 deletions app/dashboard/_components/CopyButton.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -37,7 +37,7 @@ export function CopyButton({
<button
type="button"
onClick={handleClick}
className="flex items-center justify-center gap-1.5 rounded-lg border border-dash-border p-2 text-sm font-semibold text-dash-heading hover:bg-dash-neutral-pill"
className="flex items-center justify-center gap-1.5 rounded-lg border border-dash-border p-2 text-sm font-semibold text-dash-heading transition-[background-color,transform] duration-150 ease-out hover:bg-dash-neutral-pill focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-dash-accent/50 focus-visible:ring-offset-2 focus-visible:ring-offset-dash-card active:scale-[0.97]"
>
<svg
width="14"
Expand All@@ -51,11 +51,16 @@ export function CopyButton({
<rect x="9" y="9" width="12" height="12" rx="2" />
<path d="M5 15H4a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h10a1 1 0 0 1 1 1v1" />
</svg>
{status === "copied"
? "Copied!"
: status === "error"
? "Couldn't copy"
: label}
<span
key={status}
className="motion-safe:animate-[fade-up_200ms_ease-out_both]"
>
{status === "copied"
? "Copied!"
: status === "error"
? "Couldn't copy"
: label}
</span>
</button>
);
}
2 changes: 1 addition & 1 deletion app/dashboard/_components/ExternalLink.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -12,7 +12,7 @@ export function ExternalLink({
return (
<a
href={href}
className={`inline-flex items-center gap-1.5 font-semibold ${className}`}
className={`inline-flex items-center gap-1.5 rounded-sm font-semibold transition-colors duration-150 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-dash-accent/50 focus-visible:ring-offset-2 focus-visible:ring-offset-dash-card ${className}`}
>
{children}
<svg
Expand Down
2 changes: 1 addition & 1 deletion app/dashboard/_components/GrowthCard.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -14,7 +14,7 @@ export function GrowthCard({ pet }: { pet: DashboardPet }) {
<div className="flex flex-col gap-3 rounded-2xl border border-dash-border bg-dash-card p-6">
<h2 className="text-[15px] font-bold text-dash-heading">Growth</h2>
<Bar progress={progress} />
<span className="text-xs text-dash-muted">{label}</span>
<span className="text-xs tabular-nums text-dash-muted">{label}</span>
<p className="text-xs text-dash-muted">
{pet.lastCommitRelative
? `Last commit ${pet.lastCommitRelative}`
Expand Down
4 changes: 3 additions & 1 deletion app/dashboard/_components/HealthCard.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -21,7 +21,9 @@ export function HealthCard({ pet }: { pet: DashboardPet }) {
<div className="flex flex-col gap-3 rounded-2xl border border-dash-border bg-dash-card p-6">
<h2 className="text-[15px] font-bold text-dash-heading">Health</h2>
<div className="flex items-baseline gap-2.5">
<span className={`text-[32px] font-bold ${MOOD[mood].text}`}>
<span
className={`text-[32px] font-bold tabular-nums ${MOOD[mood].text}`}
>
{pet.health}%
</span>
<MoodPill mood={mood} />
Expand Down
3 changes: 2 additions & 1 deletion app/dashboard/_components/Hero.tsx
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
import { stageForXp } from "@/lib/pets/growth";
import { moodFor } from "@/lib/pets/mood";
import { repoShortName } from "@/lib/pets/repo-name";
import type { DashboardPet } from "@/lib/pets/dashboard-data";
import { PetArt } from "./PetArt";
import { MoodPill, PhasePill, StagePill } from "./Pills";
Expand All@@ -23,7 +24,7 @@ export function Hero({ pet }: { pet: DashboardPet }) {
<div className="flex min-w-0 flex-1 flex-col gap-3">
<div className="flex flex-wrap items-center gap-2.5">
<h1 className="text-2xl font-bold text-dash-heading">
{pet.fullName}
{repoShortName(pet.fullName)}
</h1>
<PhasePill phase={pet.phase} />
</div>
Expand Down
15 changes: 12 additions & 3 deletions app/dashboard/_components/McpTokenCard.tsx
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
"use client";

import { useState } from "react";
import { useRouter } from "next/navigation";
import { regenerateMcpToken } from "../[repoId]/actions";
import { CopyButton } from "./CopyButton";

Expand All@@ -16,12 +17,20 @@ export function McpTokenCard({
const [token, setToken] = useState<string | null>(null);
const [pending, setPending] = useState(false);
const [error, setError] = useState(false);
const router = useRouter();

async function handleGenerate() {
setPending(true);
setError(false);
try {
setToken(await regenerateMcpToken(Number(repoId)));
// hasToken/lastUsedRelative are a server-fetched snapshot (see
// PetsSection in app/dashboard/page.tsx) — without this, they'd stay
// stale for the rest of the session. Doesn't affect what's on screen
// right now (the reveal below reads local `token` state, not the
// prop), but it means swiping away and back in the carousel won't
// show "No token generated yet" for a pet that already has one.
router.refresh();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Refresh masks token lookup failures

The catch in getMcpTokenStatus() maps read failures to { exists: false, lastUsedRelative: null }, so router.refresh() re-runs PetsSection with an indistinguishable no-token result and McpTokenCard renders the absent-token state despite the existing token — should we preserve an explicit unknown/error status?

Severityweb_search

Want Baz to fix this for you? Activate Fixer

Other fix methods

Fix in Cursor

Prompt for AI Agents
Before applying, verify this suggestion against the current code. In
`app/dashboard/_components/McpTokenCard.tsx` around lines 22-33, especially
`handleGenerate` and its `router.refresh()` call, fix the refreshed token-status flow so
a `getMcpTokenStatus()` read failure cannot appear as “No token generated yet.”
Update the status contract and its `PetsSection`/card consumers to preserve an explicit
unknown or error state, rendering an error or retaining the prior known state while
reserving `exists: false` for a successful no-token lookup.

} catch {
setError(true);
} finally {
Expand All@@ -38,7 +47,7 @@ export function McpTokenCard({
</p>

{token ? (
<>
<div className="flex flex-col gap-3 motion-safe:animate-[fade-up_350ms_cubic-bezier(0.16,1,0.3,1)_both]">
<div className="overflow-x-auto rounded-lg bg-[#2B2115] p-3">
<code className="font-mono text-[11.5px] whitespace-pre text-[#F5EFE4]">
{token}
Expand All@@ -49,7 +58,7 @@ export function McpTokenCard({
MCP client, pointed at <code className="font-mono">/api/mcp</code>.
</p>
<CopyButton text={token} label="Copy token" />
</>
</div>
) : (
<>
<p className="text-xs text-dash-muted">
Expand All@@ -66,7 +75,7 @@ export function McpTokenCard({
type="button"
onClick={handleGenerate}
disabled={pending}
className="flex items-center justify-center gap-1.5 rounded-lg border border-dash-border p-2 text-sm font-semibold text-dash-heading hover:bg-dash-neutral-pill disabled:opacity-50"
className="flex items-center justify-center gap-1.5 rounded-lg border border-dash-border p-2 text-sm font-semibold text-dash-heading transition-[background-color,transform] duration-150 ease-out hover:bg-dash-neutral-pill focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-dash-accent/50 focus-visible:ring-offset-2 focus-visible:ring-offset-dash-card active:scale-[0.97] disabled:opacity-50 disabled:active:scale-100"
>
{pending
? "Generating…"
Expand Down
Loading
Loading