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
36 changes: 36 additions & 0 deletions app/(workbench)/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import type { ReactNode } from "react";

import { Workbench } from "@/components/app/Workbench";
import { getEntitlementSummary } from "@/lib/server/entitlements";
import { getCurrentUser } from "@/lib/supabase/server";
import { buildWorkspaceTree } from "@/lib/workbench/tree";

import { getDashboardData } from "../dashboard/data";

export const dynamic = "force-dynamic";

export default async function WorkbenchLayout({
children,
}: {
children: ReactNode;
}) {
// The layout can't know the requested path, so it must not own the auth
// redirect — each page calls requireDashboardUser() with the correct `next`.
// When signed out we render children bare and let that page-level guard fire.
const user = await getCurrentUser();
if (!user) {
return <>{children}</>;
}

const [data, summary] = await Promise.all([
getDashboardData(user),
getEntitlementSummary(user.id),
]);
const tree = buildWorkspaceTree(data.workspaces, data.projects);

return (
<Workbench tree={tree} planTier={summary.planTier} userEmail={user.email}>
{children}
</Workbench>
);
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
import { AppShell } from "@/components/dashboard/app-shell";
import { CreateVersionForm } from "@/components/project/create-version-form";
import { ProjectHeader } from "@/components/project/project-header";
import { VersionTimeline } from "@/components/project/version-timeline";
import { UpgradeGate } from "@/components/app/UpgradeGate";

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

export const dynamic = "force-dynamic";

Expand All @@ -15,22 +19,22 @@ type ProjectPageProps = {

export default async function ProjectPage({ params }: ProjectPageProps) {
const { projectId } = await params;
const user = await requireDashboardUser(`/projects/${projectId}`, {
paidOnly: true,
});
const user = await requireDashboardUser(`/projects/${projectId}`);
const { isPaid } = await resolvePlanGate(user.id);
if (!isPaid) {
return <UpgradeGate feature="Projects" />;
}
const data = await getProjectPageData(projectId, user);
const canWrite =
data.project.owner_id === user.id ||
data.role === "owner" ||
(data.workspace.plan_tier === "studio" && data.role === "admin");

return (
<AppShell active="projects" userEmail={user.email}>
<div className="mx-auto max-w-6xl space-y-8">
<ProjectHeader data={data} />
{canWrite ? <CreateVersionForm projectId={data.project.id} /> : null}
<VersionTimeline projectId={data.project.id} versions={data.versions} />
</div>
</AppShell>
<div className="mx-auto max-w-6xl space-y-8">
<ProjectHeader data={data} />
{canWrite ? <CreateVersionForm projectId={data.project.id} /> : null}
<VersionTimeline projectId={data.project.id} versions={data.versions} />
</div>
);
}
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import Link from "next/link";

import { AppShell } from "@/components/dashboard/app-shell";
import { ProjectHeader } from "@/components/project/project-header";
import { VersionDetail } from "@/components/project/version-detail";
import { VersionTimeline } from "@/components/project/version-timeline";
import { UpgradeGate } from "@/components/app/UpgradeGate";

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

export const dynamic = "force-dynamic";

Expand All @@ -23,25 +24,26 @@ export default async function VersionPage({ params }: VersionPageProps) {
const { projectId, versionId } = await params;
const user = await requireDashboardUser(
`/projects/${projectId}/versions/${versionId}`,
{ paidOnly: true },
);
const { isPaid } = await resolvePlanGate(user.id);
if (!isPaid) {
return <UpgradeGate feature="Projects" />;
}
const data = await getVersionPageData(projectId, versionId, user);

return (
<AppShell active="projects" userEmail={user.email}>
<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)]"
>
Back to project
</Link>
<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>
<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)]"
>
Back to project
</Link>
<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>
</AppShell>
</div>
);
}
18 changes: 12 additions & 6 deletions app/projects/page.tsx → app/(workbench)/projects/page.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,27 @@
import { ArrowUpRight, Sparkles } from "lucide-react";
import Link from "next/link";

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

import { getDashboardData, requireDashboardUser } from "../dashboard/data";
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 (
<AppShell active="projects" userEmail={user.email}>
<div className="space-y-7">
<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)]">
Expand All @@ -39,7 +46,6 @@ export default async function ProjectsPage() {
</header>

<RecentProjects projects={data.projects} />
</div>
</AppShell>
</div>
);
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
import { AppShell } from "@/components/dashboard/app-shell";
import { CreateProjectForm } from "@/components/project/create-project-form";
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, requireDashboardUser } from "../../dashboard/data";
import {
getWorkspacePageData,
requireDashboardUser,
resolvePlanGate,
} from "@/app/dashboard/data";

export const dynamic = "force-dynamic";

Expand All @@ -16,24 +20,24 @@ type WorkspacePageProps = {

export default async function WorkspacePage({ params }: WorkspacePageProps) {
const { workspaceId } = await params;
const user = await requireDashboardUser(`/workspaces/${workspaceId}`, {
paidOnly: true,
});
const user = await requireDashboardUser(`/workspaces/${workspaceId}`);
const { isPaid } = await resolvePlanGate(user.id);
if (!isPaid) {
return <UpgradeGate feature="Workspaces" />;
}
const data = await getWorkspacePageData(workspaceId, user);

return (
<AppShell active="workspaces" userEmail={user.email}>
<div className="mx-auto max-w-6xl space-y-8">
<WorkspaceHeader data={data} />
<CreateProjectForm workspaceId={data.workspace.id} />
<div className="grid gap-8 lg:grid-cols-[1fr_320px]">
<WorkspaceProjects projects={data.projects} />
<WorkspaceMembers
members={data.members}
ownerId={data.workspace.owner_id}
/>
</div>
<div className="mx-auto max-w-6xl space-y-8">
<WorkspaceHeader data={data} />
<CreateProjectForm workspaceId={data.workspace.id} />
<div className="grid gap-8 lg:grid-cols-[1fr_320px]">
<WorkspaceProjects projects={data.projects} />
<WorkspaceMembers
members={data.members}
ownerId={data.workspace.owner_id}
/>
</div>
</AppShell>
</div>
);
}
18 changes: 12 additions & 6 deletions app/workspaces/page.tsx → app/(workbench)/workspaces/page.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,28 @@
import { Plus } from "lucide-react";
import Link from "next/link";

import { AppShell } from "@/components/dashboard/app-shell";
import { WorkspaceList } from "@/components/dashboard/workspace-list";
import { CreateWorkspaceForm } from "@/components/workspace/create-workspace-form";
import { UpgradeGate } from "@/components/app/UpgradeGate";

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

export const dynamic = "force-dynamic";

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

return (
<AppShell active="workspaces" userEmail={user.email}>
<div className="space-y-7">
<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)]">
Expand All @@ -42,7 +49,6 @@ export default async function WorkspacesPage() {
<CreateWorkspaceForm />
</div>
<WorkspaceList workspaces={data.workspaces} />
</div>
</AppShell>
</div>
);
}
17 changes: 17 additions & 0 deletions app/@modal/(.)account/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { AccountContent } from "@/components/account/AccountContent";
import { RouteModal } from "@/components/app/RouteModal";

export const dynamic = "force-dynamic";

/**
* Intercepts a soft navigation to /account from anywhere in the app and renders
* the Account surface as a modal over the current page. A hard load of /account
* bypasses this and renders the standalone page.
*/
export default function AccountModal() {
return (
<RouteModal title="Account">
<AccountContent />
</RouteModal>
);
}
17 changes: 17 additions & 0 deletions app/@modal/(.)billing/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { RouteModal } from "@/components/app/RouteModal";
import { BillingContent } from "@/components/billing/BillingContent";

export const dynamic = "force-dynamic";

/**
* Intercepts a soft navigation to /billing from anywhere in the app and renders
* the Billing surface as a modal over the current page. A hard load of /billing
* bypasses this and renders the standalone page.
*/
export default function BillingModal() {
return (
<RouteModal title="Billing">
<BillingContent />
</RouteModal>
);
}
8 changes: 8 additions & 0 deletions app/@modal/default.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/**
* Fallback for the `@modal` parallel slot. Every route that isn't an active
* intercept (i.e. everything except a soft navigation to /account or /billing)
* resolves the slot to this, which renders nothing.
*/
export default function ModalDefault() {
return null;
}
Loading
Loading