feat(agency): implement dynamic day-0 routing to seamlessly send agency users to the Command Center instead of the Maker projects grid

This commit is contained in:
2026-06-08 13:13:51 -07:00
parent 9323a92eff
commit 63b16e76bb

View File

@@ -17,6 +17,7 @@ import {
} from "@/components/ui/alert-dialog";
import { Loader2, Trash2 } from "lucide-react";
import { toast } from "sonner";
import AgencyDashboard from "../agency/page";
interface ProjectWithStats {
id: string;
@@ -43,24 +44,64 @@ function timeAgo(dateStr?: string | null): string {
}
function StatusDot({ status }: { status?: string }) {
const color = status === "live" ? "#2e7d32" : status === "building" ? "#3d5afe" : "#d4a04a";
const anim = status === "building" ? "vibn-breathe 2.5s ease infinite" : "none";
const color =
status === "live"
? "#2e7d32"
: status === "building"
? "#3d5afe"
: "#d4a04a";
const anim =
status === "building" ? "vibn-breathe 2.5s ease infinite" : "none";
return (
<span style={{ width: 7, height: 7, borderRadius: "50%", background: color, display: "inline-block", flexShrink: 0, animation: anim }} />
<span
style={{
width: 7,
height: 7,
borderRadius: "50%",
background: color,
display: "inline-block",
flexShrink: 0,
animation: anim,
}}
/>
);
}
function StatusTag({ status }: { status?: string }) {
const label = status === "live" ? "Live" : status === "building" ? "Building" : "Defining";
const color = status === "live" ? "#2e7d32" : status === "building" ? "#3d5afe" : "#9a7b3a";
const bg = status === "live" ? "#2e7d3210" : status === "building" ? "#3d5afe10" : "#d4a04a12";
const label =
status === "live"
? "Live"
: status === "building"
? "Building"
: "Defining";
const color =
status === "live"
? "#2e7d32"
: status === "building"
? "#3d5afe"
: "#9a7b3a";
const bg =
status === "live"
? "#2e7d3210"
: status === "building"
? "#3d5afe10"
: "#d4a04a12";
return (
<span style={{
display: "inline-flex", alignItems: "center", gap: 5,
padding: "3px 9px", borderRadius: 4,
fontSize: "0.68rem", fontWeight: 600, letterSpacing: "0.02em",
color, background: bg, fontFamily: "var(--font-inter), ui-sans-serif, sans-serif",
}}>
<span
style={{
display: "inline-flex",
alignItems: "center",
gap: 5,
padding: "3px 9px",
borderRadius: 4,
fontSize: "0.68rem",
fontWeight: 600,
letterSpacing: "0.02em",
color,
background: bg,
fontFamily: "var(--font-inter), ui-sans-serif, sans-serif",
}}
>
<StatusDot status={status} /> {label}
</span>
);
@@ -72,10 +113,12 @@ export default function ProjectsPage() {
const { data: session, status } = useSession();
const [projects, setProjects] = useState<ProjectWithStats[]>([]);
const [isAgency, setIsAgency] = useState<boolean | null>(null);
const [loading, setLoading] = useState(true);
const [loadError, setLoadError] = useState<string | null>(null);
const [showNew, setShowNew] = useState(false);
const [projectToDelete, setProjectToDelete] = useState<ProjectWithStats | null>(null);
const [projectToDelete, setProjectToDelete] =
useState<ProjectWithStats | null>(null);
const [isDeleting, setIsDeleting] = useState(false);
const [hoveredId, setHoveredId] = useState<string | null>(null);
@@ -83,19 +126,41 @@ export default function ProjectsPage() {
try {
setLoading(true);
setLoadError(null);
// Fetch Workspace metadata first to determine routing
const wsRes = await fetch(`/api/workspaces/${workspace}`, {
credentials: "include",
});
if (wsRes.ok) {
const wsData = await wsRes.json();
setIsAgency(wsData.isAgency);
if (wsData.isAgency) {
setLoading(false);
return; // Stop fetching projects if it's an agency, let AgencyDashboard handle its own data
}
}
const res = await fetch("/api/projects", { credentials: "include" });
if (res.status === 401) {
throw new Error("Your session expired — please log in again.");
}
if (!res.ok) {
let body: { error?: string; details?: string } = {};
try { body = await res.json(); } catch { /* keep {} */ }
throw new Error(body.error || `HTTP ${res.status} ${res.statusText}`.trim());
try {
body = await res.json();
} catch {
/* keep {} */
}
throw new Error(
body.error || `HTTP ${res.status} ${res.statusText}`.trim(),
);
}
const data = await res.json();
setProjects(data.projects ?? []);
} catch (err) {
setLoadError(err instanceof Error ? err.message : "Failed to load projects");
setLoadError(
err instanceof Error ? err.message : "Failed to load projects",
);
} finally {
setLoading(false);
}
@@ -133,7 +198,9 @@ export default function ProjectsPage() {
const statusSummary = () => {
const live = projects.filter((p) => p.status === "live").length;
const building = projects.filter((p) => p.status === "building").length;
const defining = projects.filter((p) => !p.status || p.status === "defining").length;
const defining = projects.filter(
(p) => !p.status || p.status === "defining",
).length;
const parts = [];
if (defining) parts.push(`${defining} defining`);
if (building) parts.push(`${building} building`);
@@ -141,63 +208,112 @@ export default function ProjectsPage() {
return `${projects.length} total · ${parts.join(" · ")}`;
};
if (isAgency === true) {
return <AgencyDashboard />;
}
return (
<div
className="vibn-enter"
style={{ padding: "44px 52px", maxWidth: 900, fontFamily: "var(--font-inter), ui-sans-serif, sans-serif" }}
style={{
padding: "44px 52px",
maxWidth: 900,
fontFamily: "var(--font-inter), ui-sans-serif, sans-serif",
}}
>
{/* Header */}
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "flex-end", marginBottom: 36 }}>
<div
style={{
display: "flex",
justifyContent: "space-between",
alignItems: "flex-end",
marginBottom: 36,
}}
>
<div>
<h1 style={{
fontFamily: "var(--font-lora), ui-serif, serif", fontSize: "1.9rem",
fontWeight: 400, color: "#1a1a1a", letterSpacing: "-0.03em",
lineHeight: 1.15, marginBottom: 4,
}}>
<h1
style={{
fontFamily: "var(--font-lora), ui-serif, serif",
fontSize: "1.9rem",
fontWeight: 400,
color: "#1a1a1a",
letterSpacing: "-0.03em",
lineHeight: 1.15,
marginBottom: 4,
}}
>
Projects
</h1>
{!loading && (
<p style={{ fontSize: "0.82rem", color: "#a09a90" }}>{statusSummary()}</p>
<p style={{ fontSize: "0.82rem", color: "#a09a90" }}>
{statusSummary()}
</p>
)}
</div>
<button
onClick={() => setShowNew(true)}
style={{
display: "flex", alignItems: "center", gap: 6,
padding: "8px 16px", borderRadius: 7,
background: "#1a1a1a", color: "#fff",
display: "flex",
alignItems: "center",
gap: 6,
padding: "8px 16px",
borderRadius: 7,
background: "#1a1a1a",
color: "#fff",
border: "1px solid #1a1a1a",
fontSize: "0.78rem", fontWeight: 600,
fontFamily: "var(--font-inter), ui-sans-serif, sans-serif", cursor: "pointer",
fontSize: "0.78rem",
fontWeight: 600,
fontFamily: "var(--font-inter), ui-sans-serif, sans-serif",
cursor: "pointer",
}}
>
<span style={{ fontSize: "1rem", lineHeight: 1, fontWeight: 300 }}>+</span>
<span style={{ fontSize: "1rem", lineHeight: 1, fontWeight: 300 }}>
+
</span>
New project
</button>
</div>
{/* Loading */}
{loading && (
<div style={{ display: "flex", justifyContent: "center", paddingTop: 64 }}>
<Loader2 style={{ width: 28, height: 28, color: "#b5b0a6" }} className="animate-spin" />
<div
style={{ display: "flex", justifyContent: "center", paddingTop: 64 }}
>
<Loader2
style={{ width: 28, height: 28, color: "#b5b0a6" }}
className="animate-spin"
/>
</div>
)}
{/* Error */}
{!loading && loadError && (
<div style={{
marginBottom: 24, padding: "14px 18px",
background: "#fdecea", border: "1px solid #f4c4be",
borderRadius: 10, color: "#8b2a1f", fontSize: "0.85rem",
display: "flex", alignItems: "center", gap: 12,
}}>
<div
style={{
marginBottom: 24,
padding: "14px 18px",
background: "#fdecea",
border: "1px solid #f4c4be",
borderRadius: 10,
color: "#8b2a1f",
fontSize: "0.85rem",
display: "flex",
alignItems: "center",
gap: 12,
}}
>
<span style={{ flex: 1 }}>{loadError}</span>
<button
onClick={fetchProjects}
style={{
padding: "5px 12px", borderRadius: 6,
background: "#8b2a1f", color: "#fff", border: "none",
fontSize: "0.78rem", fontWeight: 600, cursor: "pointer",
padding: "5px 12px",
borderRadius: 6,
background: "#8b2a1f",
color: "#fff",
border: "none",
fontSize: "0.78rem",
fontWeight: 600,
cursor: "pointer",
fontFamily: "var(--font-inter), ui-sans-serif, sans-serif",
}}
>
@@ -218,11 +334,17 @@ export default function ProjectsPage() {
<Link
href={`/${workspace}/project/${p.id}`}
style={{
width: "100%", display: "flex", alignItems: "center",
padding: "18px 22px", borderRadius: 10,
background: "#fff", border: "1px solid #e8e4dc",
cursor: "pointer", fontFamily: "var(--font-inter), ui-sans-serif, sans-serif",
textDecoration: "none", boxShadow: "0 1px 2px #1a1a1a05",
width: "100%",
display: "flex",
alignItems: "center",
padding: "18px 22px",
borderRadius: 10,
background: "#fff",
border: "1px solid #e8e4dc",
cursor: "pointer",
fontFamily: "var(--font-inter), ui-sans-serif, sans-serif",
textDecoration: "none",
boxShadow: "0 1px 2px #1a1a1a05",
transition: "all 0.15s",
}}
onMouseEnter={(e) => {
@@ -237,64 +359,137 @@ export default function ProjectsPage() {
}}
>
{/* Project initial */}
<div style={{
width: 36, height: 36, borderRadius: 9, marginRight: 16,
background: "#1a1a1a12",
display: "flex", alignItems: "center", justifyContent: "center",
flexShrink: 0,
}}>
<span style={{
fontFamily: "var(--font-lora), ui-serif, serif",
fontSize: "1.05rem", fontWeight: 500, color: "#1a1a1a",
}}>
<div
style={{
width: 36,
height: 36,
borderRadius: 9,
marginRight: 16,
background: "#1a1a1a12",
display: "flex",
alignItems: "center",
justifyContent: "center",
flexShrink: 0,
}}
>
<span
style={{
fontFamily: "var(--font-lora), ui-serif, serif",
fontSize: "1.05rem",
fontWeight: 500,
color: "#1a1a1a",
}}
>
{p.productName[0]?.toUpperCase() ?? "P"}
</span>
</div>
{/* Name + vision */}
<div style={{ flex: 1, minWidth: 0 }}>
<div style={{ display: "flex", alignItems: "center", gap: 8, marginBottom: 2 }}>
<span style={{ fontSize: "0.9rem", fontWeight: 600, color: "#1a1a1a" }}>
<div
style={{
display: "flex",
alignItems: "center",
gap: 8,
marginBottom: 2,
}}
>
<span
style={{
fontSize: "0.9rem",
fontWeight: 600,
color: "#1a1a1a",
}}
>
{p.productName}
</span>
<StatusTag status={p.status} />
</div>
{p.productVision && (
<span style={{ fontSize: "0.78rem", color: "#a09a90", display: "block", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>
<span
style={{
fontSize: "0.78rem",
color: "#a09a90",
display: "block",
overflow: "hidden",
textOverflow: "ellipsis",
whiteSpace: "nowrap",
}}
>
{p.productVision}
</span>
)}
</div>
{/* Meta */}
<div style={{ display: "flex", gap: 28, alignItems: "center", flexShrink: 0 }}>
<div
style={{
display: "flex",
gap: 28,
alignItems: "center",
flexShrink: 0,
}}
>
<div style={{ textAlign: "right" }}>
<div style={{ fontSize: "0.62rem", color: "#b5b0a6", textTransform: "uppercase", letterSpacing: "0.06em", marginBottom: 2 }}>
<div
style={{
fontSize: "0.62rem",
color: "#b5b0a6",
textTransform: "uppercase",
letterSpacing: "0.06em",
marginBottom: 2,
}}
>
Last active
</div>
<div style={{ fontSize: "0.78rem", color: "#6b6560" }}>{timeAgo(p.updatedAt)}</div>
<div style={{ fontSize: "0.78rem", color: "#6b6560" }}>
{timeAgo(p.updatedAt)}
</div>
</div>
<div style={{ textAlign: "right" }}>
<div style={{ fontSize: "0.62rem", color: "#b5b0a6", textTransform: "uppercase", letterSpacing: "0.06em", marginBottom: 2 }}>
<div
style={{
fontSize: "0.62rem",
color: "#b5b0a6",
textTransform: "uppercase",
letterSpacing: "0.06em",
marginBottom: 2,
}}
>
Sessions
</div>
<div style={{ fontSize: "0.78rem", color: "#6b6560" }}>{p.stats.sessions}</div>
<div style={{ fontSize: "0.78rem", color: "#6b6560" }}>
{p.stats.sessions}
</div>
</div>
</div>
{/* Delete (visible on row hover) */}
<button
onClick={(e) => { e.preventDefault(); e.stopPropagation(); setProjectToDelete(p); }}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
setProjectToDelete(p);
}}
style={{
marginLeft: 16, padding: "6px 8px", borderRadius: 6,
border: "none", background: "transparent",
color: "#c0bab2", cursor: "pointer",
marginLeft: 16,
padding: "6px 8px",
borderRadius: 6,
border: "none",
background: "transparent",
color: "#c0bab2",
cursor: "pointer",
opacity: hoveredId === p.id ? 1 : 0,
transition: "opacity 0.15s, color 0.15s",
fontFamily: "var(--font-inter), ui-sans-serif, sans-serif", flexShrink: 0,
fontFamily: "var(--font-inter), ui-sans-serif, sans-serif",
flexShrink: 0,
}}
onMouseEnter={(e) => {
e.currentTarget.style.color = "#d32f2f";
}}
onMouseLeave={(e) => {
e.currentTarget.style.color = "#c0bab2";
}}
onMouseEnter={(e) => { e.currentTarget.style.color = "#d32f2f"; }}
onMouseLeave={(e) => { e.currentTarget.style.color = "#c0bab2"; }}
title="Delete project"
>
<Trash2 style={{ width: 14, height: 14 }} />
@@ -307,17 +502,31 @@ export default function ProjectsPage() {
<button
onClick={() => setShowNew(true)}
style={{
width: "100%", display: "flex", alignItems: "center", justifyContent: "center",
padding: "22px", borderRadius: 10,
background: "transparent", border: "1px dashed #d0ccc4",
cursor: "pointer", fontFamily: "var(--font-inter), ui-sans-serif, sans-serif",
color: "#b5b0a6", fontSize: "0.84rem", fontWeight: 500,
width: "100%",
display: "flex",
alignItems: "center",
justifyContent: "center",
padding: "22px",
borderRadius: 10,
background: "transparent",
border: "1px dashed #d0ccc4",
cursor: "pointer",
fontFamily: "var(--font-inter), ui-sans-serif, sans-serif",
color: "#b5b0a6",
fontSize: "0.84rem",
fontWeight: 500,
transition: "all 0.15s",
animationDelay: `${projects.length * 0.05}s`,
}}
className="vibn-enter"
onMouseEnter={(e) => { e.currentTarget.style.borderColor = "#8a8478"; e.currentTarget.style.color = "#6b6560"; }}
onMouseLeave={(e) => { e.currentTarget.style.borderColor = "#d0ccc4"; e.currentTarget.style.color = "#b5b0a6"; }}
onMouseEnter={(e) => {
e.currentTarget.style.borderColor = "#8a8478";
e.currentTarget.style.color = "#6b6560";
}}
onMouseLeave={(e) => {
e.currentTarget.style.borderColor = "#d0ccc4";
e.currentTarget.style.color = "#b5b0a6";
}}
>
+ New project
</button>
@@ -327,19 +536,39 @@ export default function ProjectsPage() {
{/* Empty state */}
{!loading && projects.length === 0 && (
<div style={{ textAlign: "center", paddingTop: 64 }}>
<h3 style={{ fontFamily: "var(--font-lora), ui-serif, serif", fontSize: "1.3rem", fontWeight: 400, color: "#1a1a1a", marginBottom: 8 }}>
<h3
style={{
fontFamily: "var(--font-lora), ui-serif, serif",
fontSize: "1.3rem",
fontWeight: 400,
color: "#1a1a1a",
marginBottom: 8,
}}
>
No projects yet
</h3>
<p style={{ fontSize: "0.82rem", color: "#a09a90", lineHeight: 1.6, marginBottom: 24 }}>
<p
style={{
fontSize: "0.82rem",
color: "#a09a90",
lineHeight: 1.6,
marginBottom: 24,
}}
>
Tell Vibn what you want to build and it will figure out the rest.
</p>
<button
onClick={() => setShowNew(true)}
style={{
padding: "10px 22px", borderRadius: 7,
background: "#1a1a1a", color: "#fff",
border: "none", fontSize: "0.84rem", fontWeight: 600,
fontFamily: "var(--font-inter), ui-sans-serif, sans-serif", cursor: "pointer",
padding: "10px 22px",
borderRadius: 7,
background: "#1a1a1a",
color: "#fff",
border: "none",
fontSize: "0.84rem",
fontWeight: 600,
fontFamily: "var(--font-inter), ui-sans-serif, sans-serif",
cursor: "pointer",
}}
>
Create your first project
@@ -349,17 +578,25 @@ export default function ProjectsPage() {
<ProjectCreationModal
open={showNew}
onOpenChange={(open) => { setShowNew(open); if (!open) fetchProjects(); }}
onOpenChange={(open) => {
setShowNew(open);
if (!open) fetchProjects();
}}
workspace={workspace}
/>
<AlertDialog open={!!projectToDelete} onOpenChange={(open) => !open && setProjectToDelete(null)}>
<AlertDialog
open={!!projectToDelete}
onOpenChange={(open) => !open && setProjectToDelete(null)}
>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Delete &quot;{projectToDelete?.productName}&quot;?</AlertDialogTitle>
<AlertDialogTitle>
Delete &quot;{projectToDelete?.productName}&quot;?
</AlertDialogTitle>
<AlertDialogDescription>
This will remove the project record. Sessions will be preserved but unlinked.
The Gitea repo will not be deleted automatically.
This will remove the project record. Sessions will be preserved
but unlinked. The Gitea repo will not be deleted automatically.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
@@ -369,7 +606,11 @@ export default function ProjectsPage() {
disabled={isDeleting}
className="bg-red-600 hover:bg-red-700"
>
{isDeleting ? <Loader2 className="mr-2 h-4 w-4 animate-spin" /> : <Trash2 className="mr-2 h-4 w-4" />}
{isDeleting ? (
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
) : (
<Trash2 className="mr-2 h-4 w-4" />
)}
Delete Project
</AlertDialogAction>
</AlertDialogFooter>