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
54 changes: 54 additions & 0 deletions app/(workbench)/app/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { AppShell as AnalyzeWorkspace } from "@/components/app/AppShell";
import { AppShell as WorkspaceShell } from "@/components/dashboard/app-shell";
import { getEntitlementSummary } from "@/lib/server/entitlements";
import { getCurrentUser } from "@/lib/supabase/server";

export const dynamic = "force-dynamic";

const UUID_PATTERN =
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;

type AnimationConverterProps = {
searchParams: Promise<{ project?: string; ws?: string }>;
};

/**
* The analyzer. Signed-in users get the workbench shell from the (workbench)
* layout; anonymous visitors fall through that layout bare, so they keep the
* standalone shell here. `?ws=` / `?project=` carry the save destination from
* "New analysis" / "New sequence" buttons in the workspace desktop.
*/
export default async function AnimationConverter({
searchParams,
}: AnimationConverterProps) {
const user = await getCurrentUser();

if (!user) {
return (
<WorkspaceShell active="analyze" bleed userEmail={null}>
<AnalyzeWorkspace />
</WorkspaceShell>
);
}

const [summary, params] = await Promise.all([
getEntitlementSummary(user.id),
searchParams,
]);

return (
<AnalyzeWorkspace
initialDailyAnalysisUsage={summary.usage.dailyAnalyses}
initialEntitlements={summary.entitlements}
initialPlanTier={summary.planTier}
saveTarget={{
projectId: sanitizeResourceId(params.project),
workspaceId: sanitizeResourceId(params.ws),
}}
/>
);
}

function sanitizeResourceId(value: string | undefined) {
return value && UUID_PATTERN.test(value) ? value : null;
}
7 changes: 6 additions & 1 deletion app/(workbench)/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,12 @@ export default async function WorkbenchLayout({
const tree = buildWorkspaceTree(data.workspaces, data.projects);

return (
<Workbench tree={tree} planTier={summary.planTier} userEmail={user.email}>
<Workbench
tree={tree}
planTier={summary.planTier}
userEmail={user.email}
userId={user.id}
>
{children}
</Workbench>
);
Expand Down
87 changes: 85 additions & 2 deletions app/(workbench)/projects/[projectId]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
import { CreateVersionForm } from "@/components/project/create-version-form";
import { Sparkles } from "lucide-react";
import Link from "next/link";

import { ProjectHeader } from "@/components/project/project-header";
import { SavedAnalysisViewer } from "@/components/project/saved-analysis-viewer";
import { VersionDetail } from "@/components/project/version-detail";
import { VersionTimeline } from "@/components/project/version-timeline";
import { UpgradeGate } from "@/components/app/UpgradeGate";
import { parseSavedAnalysisResult } from "@/lib/contracts/savedAnalysis";

import {
getProjectPageData,
Expand Down Expand Up @@ -30,10 +35,88 @@ export default async function ProjectPage({ params }: ProjectPageProps) {
data.role === "owner" ||
(data.workspace.plan_tier === "studio" && data.role === "admin");

const latestVersion = data.versions[0] ?? null;
const savedResult = latestVersion
? parseSavedAnalysisResult(latestVersion.motion_spec)
: null;
const newSequenceHref = `/app?project=${data.project.id}`;

return (
<div className="mx-auto max-w-6xl space-y-8">
<div className="flex flex-wrap items-center justify-between gap-3">
<nav
aria-label="Breadcrumb"
className="font-mono text-xs text-[var(--muted)]"
>
<Link
href="/workspaces"
className="text-[var(--accent)] transition hover:text-[var(--text)]"
>
Workspaces
</Link>
{" / "}
<Link
href={`/workspaces/${data.workspace.id}`}
className="text-[var(--accent)] transition hover:text-[var(--text)]"
>
{data.workspace.name}
</Link>
{" / "}
<span className="text-[var(--text)]">{data.project.title}</span>
</nav>
{canWrite ? (
<Link
href={newSequenceHref}
className="inline-flex h-9 items-center gap-2 border border-[var(--accent-border)] bg-[var(--accent-dim)] px-3 font-mono text-xs text-[var(--text)] transition hover:border-[var(--accent)] hover:bg-[#00ff88]/10 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-[var(--accent)]"
>
<Sparkles className="size-4" aria-hidden="true" />
New sequence
</Link>
) : null}
</div>

<ProjectHeader data={data} />
{canWrite ? <CreateVersionForm projectId={data.project.id} /> : null}

{savedResult ? (
<section className="space-y-3">
<div className="flex flex-wrap items-center justify-between gap-3">
<h2 className="font-mono text-lg text-[var(--text)]">
Latest sequence
{latestVersion ? (
<span className="ml-2 font-mono text-xs text-[var(--muted)]">
v{latestVersion.version_number}
</span>
) : null}
</h2>
</div>
<SavedAnalysisViewer
result={savedResult}
newAnalysisHref={newSequenceHref}
/>
</section>
) : latestVersion ? (
<VersionDetail version={latestVersion} />
) : (
<section className="flex flex-col items-start gap-3 border border-dashed border-[var(--border)] bg-[#15160f]/60 px-5 py-8">
<h2 className="font-mono text-lg text-[var(--text)]">
No sequences yet
</h2>
<p className="max-w-xl text-sm leading-6 text-[var(--accent)]">
Run an analysis and it will be saved here as this project&apos;s
first sequence — spec, preview, and generated code included.
</p>
{canWrite ? (
<Link
href={newSequenceHref}
className="mt-2 inline-flex h-9 items-center gap-2 border border-[var(--accent-border)] bg-[var(--accent-dim)] px-3 font-mono text-xs text-[var(--text)] transition hover:border-[var(--accent)]"
>
<Sparkles className="size-4" aria-hidden="true" />
Analyze motion
</Link>
) : null}
</section>
)}

<VersionTimeline projectId={data.project.id} versions={data.versions} />
</div>
);
Expand Down
55 changes: 46 additions & 9 deletions app/(workbench)/projects/[projectId]/versions/[versionId]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import Link from "next/link";

import { ProjectHeader } from "@/components/project/project-header";
import { SavedAnalysisViewer } from "@/components/project/saved-analysis-viewer";
import { VersionDetail } from "@/components/project/version-detail";
import { VersionTimeline } from "@/components/project/version-timeline";
import { UpgradeGate } from "@/components/app/UpgradeGate";
import { parseSavedAnalysisResult } from "@/lib/contracts/savedAnalysis";

import {
getVersionPageData,
Expand All @@ -30,20 +32,55 @@ export default async function VersionPage({ params }: VersionPageProps) {
return <UpgradeGate feature="Projects" />;
}
const data = await getVersionPageData(projectId, versionId, user);
const savedResult = parseSavedAnalysisResult(data.version.motion_spec);

return (
<div className="mx-auto max-w-6xl space-y-8">
<Link
href={`/projects/${data.project.id}`}
className="inline-flex font-mono text-sm text-[var(--accent)] transition hover:text-[var(--text)]"
<nav
aria-label="Breadcrumb"
className="font-mono text-xs text-[var(--muted)]"
>
Back to project
</Link>
<Link
href={`/workspaces/${data.workspace.id}`}
className="text-[var(--accent)] transition hover:text-[var(--text)]"
>
{data.workspace.name}
</Link>
{" / "}
<Link
href={`/projects/${data.project.id}`}
className="text-[var(--accent)] transition hover:text-[var(--text)]"
>
{data.project.title}
</Link>
{" / "}
<span className="text-[var(--text)]">
v{data.version.version_number}
</span>
</nav>

<ProjectHeader data={data} />
<div className="grid gap-8 lg:grid-cols-[1fr_340px]">
<VersionDetail data={data} />
<VersionTimeline projectId={data.project.id} versions={data.versions} />
</div>

{savedResult ? (
<section className="space-y-3">
<h2 className="font-mono text-lg text-[var(--text)]">
Sequence v{data.version.version_number}
{data.version.label ? (
<span className="ml-2 font-mono text-xs text-[var(--muted)]">
{data.version.label}
</span>
) : null}
</h2>
<SavedAnalysisViewer
result={savedResult}
newAnalysisHref={`/app?project=${data.project.id}`}
/>
</section>
) : (
<VersionDetail version={data.version} />
)}

<VersionTimeline projectId={data.project.id} versions={data.versions} />
</div>
);
}
59 changes: 9 additions & 50 deletions app/(workbench)/projects/page.tsx
Original file line number Diff line number Diff line change
@@ -1,51 +1,10 @@
import { ArrowUpRight, Sparkles } from "lucide-react";
import Link from "next/link";

import { RecentProjects } from "@/components/dashboard/recent-projects";
import { UpgradeGate } from "@/components/app/UpgradeGate";

import {
getDashboardData,
requireDashboardUser,
resolvePlanGate,
} from "@/app/dashboard/data";

export const dynamic = "force-dynamic";

export default async function ProjectsPage() {
const user = await requireDashboardUser("/projects");
const { isPaid } = await resolvePlanGate(user.id);
if (!isPaid) {
return <UpgradeGate feature="Projects" />;
}
const data = await getDashboardData(user);

return (
<div className="mx-auto max-w-6xl space-y-7">
<header className="grid gap-5 border-b border-[var(--border)] pb-6 lg:grid-cols-[1fr_auto] lg:items-end">
<div>
<p className="font-mono text-xs uppercase tracking-[0.18em] text-[var(--muted)]">
Project index
</p>
<h1 className="mt-3 max-w-3xl font-mono text-3xl leading-tight text-[var(--text)] sm:text-4xl">
Saved motion work, ready to continue.
</h1>
<p className="mt-3 max-w-2xl text-sm leading-6 text-[var(--accent)]">
Open recent analysis projects, review source status, or start a fresh
motion pass in the analyzer.
</p>
</div>
<Link
href="/app"
className="inline-flex h-10 items-center justify-center gap-2 border border-[var(--accent-border)] bg-[var(--accent-dim)] px-4 font-mono text-sm text-[var(--text)] transition hover:border-[var(--accent)] hover:bg-[#00ff88]/10 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-[var(--accent)]"
>
<Sparkles className="size-4" aria-hidden="true" />
Analyze motion
<ArrowUpRight className="size-4" aria-hidden="true" />
</Link>
</header>

<RecentProjects projects={data.projects} />
</div>
);
import { redirect } from "next/navigation";

/**
* The standalone project index was folded into the workspace desktop —
* workspaces are the single organizing unit and projects live inside them.
* Kept as a redirect so old links and bookmarks stay alive.
*/
export default function ProjectsIndexRedirect() {
redirect("/workspaces");
}
42 changes: 36 additions & 6 deletions app/(workbench)/workspaces/[workspaceId]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import { CreateProjectForm } from "@/components/project/create-project-form";
import { Sparkles } from "lucide-react";
import Link from "next/link";

import { WorkspaceFiles } from "@/components/workspace/workspace-files";
import { WorkspaceHeader } from "@/components/workspace/workspace-header";
import { WorkspaceMembers } from "@/components/workspace/workspace-members";
import { WorkspaceProjects } from "@/components/workspace/workspace-projects";
import { UpgradeGate } from "@/components/app/UpgradeGate";

import {
getWorkspacePageData,
getWorkspaceDesktopData,
requireDashboardUser,
resolvePlanGate,
} from "@/app/dashboard/data";
Expand All @@ -25,14 +27,42 @@ export default async function WorkspacePage({ params }: WorkspacePageProps) {
if (!isPaid) {
return <UpgradeGate feature="Workspaces" />;
}
const data = await getWorkspacePageData(workspaceId, user);
const data = await getWorkspaceDesktopData(workspaceId, user);
const newAnalysisHref = `/app?ws=${data.workspace.id}`;

return (
<div className="mx-auto max-w-6xl space-y-8">
<div className="flex flex-wrap items-center justify-between gap-3">
<nav
aria-label="Breadcrumb"
className="font-mono text-xs text-[var(--muted)]"
>
<Link
href="/workspaces"
className="text-[var(--accent)] transition hover:text-[var(--text)]"
>
Workspaces
</Link>
{" / "}
<span className="text-[var(--text)]">{data.workspace.name}</span>
</nav>
<Link
href={newAnalysisHref}
className="inline-flex h-9 items-center gap-2 border border-[var(--accent-border)] bg-[var(--accent-dim)] px-3 font-mono text-xs text-[var(--text)] transition hover:border-[var(--accent)] hover:bg-[#00ff88]/10 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-[var(--accent)]"
>
<Sparkles className="size-4" aria-hidden="true" />
New analysis
</Link>
</div>

<WorkspaceHeader data={data} />
<CreateProjectForm workspaceId={data.workspace.id} />

<div className="grid gap-8 lg:grid-cols-[1fr_320px]">
<WorkspaceProjects projects={data.projects} />
<WorkspaceFiles
projects={data.projects}
sequenceCounts={data.sequenceCounts}
newAnalysisHref={newAnalysisHref}
/>
<WorkspaceMembers
members={data.members}
ownerId={data.workspace.owner_id}
Expand Down
Loading
Loading