feat(frontend): email+password auth, /signin + /signup pages, marketing consolidation, onboarding workspace naming + full data persistence
This commit is contained in:
@@ -24,7 +24,6 @@ interface ProjectData {
|
||||
status?: string;
|
||||
}
|
||||
|
||||
|
||||
// ── Main sidebar ─────────────────────────────────────────────────────────────
|
||||
|
||||
const COLLAPSED_KEY = "vibn_sidebar_collapsed";
|
||||
@@ -42,7 +41,9 @@ export function VIBNSidebar({ workspace, tabs, activeTab }: VIBNSidebarProps) {
|
||||
const [project, setProject] = useState<ProjectData | null>(null);
|
||||
|
||||
// Global projects list (used when NOT inside a project)
|
||||
const [projects, setProjects] = useState<Array<{ id: string; productName: string; status?: string }>>([]);
|
||||
const [projects, setProjects] = useState<
|
||||
Array<{ id: string; productName: string; status?: string }>
|
||||
>([]);
|
||||
|
||||
const activeProjectId = pathname?.match(/\/project\/([^/]+)/)?.[1] ?? null;
|
||||
|
||||
@@ -54,7 +55,7 @@ export function VIBNSidebar({ workspace, tabs, activeTab }: VIBNSidebarProps) {
|
||||
}, []);
|
||||
|
||||
const toggle = () => {
|
||||
setCollapsed(prev => {
|
||||
setCollapsed((prev) => {
|
||||
localStorage.setItem(COLLAPSED_KEY, prev ? "0" : "1");
|
||||
return !prev;
|
||||
});
|
||||
@@ -64,34 +65,55 @@ export function VIBNSidebar({ workspace, tabs, activeTab }: VIBNSidebarProps) {
|
||||
useEffect(() => {
|
||||
if (activeProjectId) return;
|
||||
fetch("/api/projects")
|
||||
.then(r => r.json())
|
||||
.then(d => setProjects(d.projects ?? []))
|
||||
.then((r) => r.json())
|
||||
.then((d) => setProjects(d.projects ?? []))
|
||||
.catch(() => {});
|
||||
}, [activeProjectId]);
|
||||
|
||||
// Fetch project-specific data when inside a project
|
||||
useEffect(() => {
|
||||
if (!activeProjectId) { setProject(null); return; }
|
||||
if (!activeProjectId) {
|
||||
setProject(null);
|
||||
return;
|
||||
}
|
||||
|
||||
fetch(`/api/projects/${activeProjectId}`)
|
||||
.then(r => r.json())
|
||||
.then(d => setProject(d.project ?? null))
|
||||
.then((r) => r.json())
|
||||
.then((d) => setProject(d.project ?? null))
|
||||
.catch(() => {});
|
||||
}, [activeProjectId]);
|
||||
|
||||
const isProjects = !activeProjectId && (pathname?.includes("/projects") || pathname?.includes("/project"));
|
||||
const isProjects =
|
||||
!activeProjectId &&
|
||||
(pathname?.includes("/projects") || pathname?.includes("/project"));
|
||||
const isActivity = !activeProjectId && pathname?.includes("/activity");
|
||||
const isSettings = !activeProjectId && pathname?.includes("/settings");
|
||||
|
||||
const topNavItems = [
|
||||
{ id: "projects", label: "Projects", icon: "⌗", href: `/${workspace}/projects` },
|
||||
{ id: "activity", label: "Activity", icon: "↗", href: `/${workspace}/activity` },
|
||||
{ id: "settings", label: "Settings", icon: "⚙", href: `/${workspace}/settings` },
|
||||
{
|
||||
id: "projects",
|
||||
label: "Projects",
|
||||
icon: "⌗",
|
||||
href: `/${workspace}/projects`,
|
||||
},
|
||||
{
|
||||
id: "activity",
|
||||
label: "Activity",
|
||||
icon: "↗",
|
||||
href: `/${workspace}/activity`,
|
||||
},
|
||||
{
|
||||
id: "settings",
|
||||
label: "Settings",
|
||||
icon: "⚙",
|
||||
href: `/${workspace}/settings`,
|
||||
},
|
||||
];
|
||||
|
||||
const userInitial = session?.user?.name?.[0]?.toUpperCase()
|
||||
?? session?.user?.email?.[0]?.toUpperCase()
|
||||
?? "?";
|
||||
const userInitial =
|
||||
session?.user?.name?.[0]?.toUpperCase() ??
|
||||
session?.user?.email?.[0]?.toUpperCase() ??
|
||||
"?";
|
||||
|
||||
const w = collapsed ? COLLAPSED_W : EXPANDED_W;
|
||||
const transition = mounted ? "width 0.2s cubic-bezier(0.4,0,0.2,1)" : "none";
|
||||
@@ -99,80 +121,215 @@ export function VIBNSidebar({ workspace, tabs, activeTab }: VIBNSidebarProps) {
|
||||
const base = `/${workspace}/project/${activeProjectId}`;
|
||||
|
||||
return (
|
||||
<nav style={{
|
||||
width: w, height: "100vh",
|
||||
background: "#fff", borderRight: "1px solid #e8e4dc",
|
||||
display: "flex", flexDirection: "column",
|
||||
fontFamily: "var(--font-inter), ui-sans-serif, sans-serif",
|
||||
flexShrink: 0, overflow: "hidden",
|
||||
transition, position: "relative",
|
||||
}}>
|
||||
|
||||
<nav
|
||||
style={{
|
||||
width: w,
|
||||
height: "100vh",
|
||||
background: "#fff",
|
||||
borderRight: "1px solid #e8e4dc",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
fontFamily: "var(--font-inter), ui-sans-serif, sans-serif",
|
||||
flexShrink: 0,
|
||||
overflow: "hidden",
|
||||
transition,
|
||||
position: "relative",
|
||||
}}
|
||||
>
|
||||
{/* ── Logo + toggle ── */}
|
||||
{collapsed ? (
|
||||
<div style={{ flexShrink: 0 }}>
|
||||
<div style={{ display: "flex", justifyContent: "center", padding: "14px 0 6px" }}>
|
||||
<Link href={`/${workspace}/projects`} title="VIBN" style={{ textDecoration: "none" }}>
|
||||
<div style={{ width: 26, height: 26, borderRadius: 7, overflow: "hidden" }}>
|
||||
<img src="/vibn-black-circle-logo.png" alt="VIBN" style={{ width: "100%", height: "100%", objectFit: "cover" }} />
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
padding: "14px 0 6px",
|
||||
}}
|
||||
>
|
||||
<Link
|
||||
href={`/${workspace}/projects`}
|
||||
title="VIBN"
|
||||
style={{ textDecoration: "none" }}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
width: 26,
|
||||
height: 26,
|
||||
borderRadius: 7,
|
||||
overflow: "hidden",
|
||||
}}
|
||||
>
|
||||
<img
|
||||
src="/vibn-black-circle-logo.png"
|
||||
alt="VIBN"
|
||||
style={{ width: "100%", height: "100%", objectFit: "cover" }}
|
||||
/>
|
||||
</div>
|
||||
</Link>
|
||||
</div>
|
||||
<div style={{ display: "flex", justifyContent: "center", paddingBottom: 8 }}>
|
||||
<button onClick={toggle} title="Expand sidebar" style={{
|
||||
background: "#f0ece4", border: "none", cursor: "pointer",
|
||||
color: "#6b6560", width: 26, height: 20, borderRadius: 5,
|
||||
display: "flex", alignItems: "center", justifyContent: "center",
|
||||
fontSize: "0.8rem", fontWeight: 700,
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
paddingBottom: 8,
|
||||
}}
|
||||
onMouseEnter={e => { (e.currentTarget.style.background = "#e0dcd4"); }}
|
||||
onMouseLeave={e => { (e.currentTarget.style.background = "#f0ece4"); }}
|
||||
>›</button>
|
||||
>
|
||||
<button
|
||||
onClick={toggle}
|
||||
title="Expand sidebar"
|
||||
style={{
|
||||
background: "#f0ece4",
|
||||
border: "none",
|
||||
cursor: "pointer",
|
||||
color: "#6b6560",
|
||||
width: 26,
|
||||
height: 20,
|
||||
borderRadius: 5,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
fontSize: "0.8rem",
|
||||
fontWeight: 700,
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.background = "#e0dcd4";
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.background = "#f0ece4";
|
||||
}}
|
||||
>
|
||||
›
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div style={{ padding: "14px 10px 14px 16px", display: "flex", alignItems: "center", justifyContent: "space-between", gap: 9, flexShrink: 0 }}>
|
||||
<Link href={`/${workspace}/projects`} style={{ display: "flex", alignItems: "center", gap: 9, textDecoration: "none", minWidth: 0 }}>
|
||||
<div style={{ width: 26, height: 26, borderRadius: 7, overflow: "hidden", flexShrink: 0 }}>
|
||||
<img src="/vibn-black-circle-logo.png" alt="VIBN" style={{ width: "100%", height: "100%", objectFit: "cover" }} />
|
||||
<div
|
||||
style={{
|
||||
padding: "14px 10px 14px 16px",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
gap: 9,
|
||||
flexShrink: 0,
|
||||
}}
|
||||
>
|
||||
<Link
|
||||
href={`/${workspace}/projects`}
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 9,
|
||||
textDecoration: "none",
|
||||
minWidth: 0,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
width: 26,
|
||||
height: 26,
|
||||
borderRadius: 7,
|
||||
overflow: "hidden",
|
||||
flexShrink: 0,
|
||||
}}
|
||||
>
|
||||
<img
|
||||
src="/vibn-black-circle-logo.png"
|
||||
alt="VIBN"
|
||||
style={{ width: "100%", height: "100%", objectFit: "cover" }}
|
||||
/>
|
||||
</div>
|
||||
<span style={{ fontSize: "0.92rem", fontWeight: 600, color: "#1a1a1a", letterSpacing: "-0.03em", fontFamily: "var(--font-lora), ui-serif, serif", whiteSpace: "nowrap" }}>
|
||||
<span
|
||||
style={{
|
||||
fontSize: "0.92rem",
|
||||
fontWeight: 600,
|
||||
color: "#1a1a1a",
|
||||
letterSpacing: "-0.03em",
|
||||
fontFamily: "var(--font-lora), ui-serif, serif",
|
||||
whiteSpace: "nowrap",
|
||||
}}
|
||||
>
|
||||
vibn
|
||||
</span>
|
||||
</Link>
|
||||
<button onClick={toggle} title="Collapse sidebar" style={{
|
||||
background: "#f0ece4", border: "none", cursor: "pointer",
|
||||
color: "#6b6560", width: 24, height: 22, borderRadius: 5,
|
||||
display: "flex", alignItems: "center", justifyContent: "center",
|
||||
fontSize: "0.8rem", fontWeight: 700, flexShrink: 0,
|
||||
}}
|
||||
onMouseEnter={e => { (e.currentTarget.style.background = "#e0dcd4"); }}
|
||||
onMouseLeave={e => { (e.currentTarget.style.background = "#f0ece4"); }}
|
||||
>‹</button>
|
||||
<button
|
||||
onClick={toggle}
|
||||
title="Collapse sidebar"
|
||||
style={{
|
||||
background: "#f0ece4",
|
||||
border: "none",
|
||||
cursor: "pointer",
|
||||
color: "#6b6560",
|
||||
width: 24,
|
||||
height: 22,
|
||||
borderRadius: 5,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
fontSize: "0.8rem",
|
||||
fontWeight: 700,
|
||||
flexShrink: 0,
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.background = "#e0dcd4";
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.background = "#f0ece4";
|
||||
}}
|
||||
>
|
||||
‹
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* ── Top nav ── */}
|
||||
<div style={{ padding: collapsed ? "2px 6px" : "2px 8px", flexShrink: 0 }}>
|
||||
{topNavItems.map(n => {
|
||||
const isActive = n.id === "projects" ? isProjects
|
||||
: n.id === "activity" ? isActivity
|
||||
: isSettings;
|
||||
<div
|
||||
style={{ padding: collapsed ? "2px 6px" : "2px 8px", flexShrink: 0 }}
|
||||
>
|
||||
{topNavItems.map((n) => {
|
||||
const isActive =
|
||||
n.id === "projects"
|
||||
? isProjects
|
||||
: n.id === "activity"
|
||||
? isActivity
|
||||
: isSettings;
|
||||
return (
|
||||
<Link key={n.id} href={n.href} title={collapsed ? n.label : undefined} style={{
|
||||
width: "100%", display: "flex", alignItems: "center",
|
||||
justifyContent: collapsed ? "center" : "flex-start",
|
||||
gap: 8, padding: collapsed ? "8px 0" : "7px 10px",
|
||||
borderRadius: 6,
|
||||
background: isActive ? "#f6f4f0" : "transparent",
|
||||
color: isActive ? "#1a1a1a" : "#6b6560",
|
||||
fontSize: "0.8rem", fontWeight: isActive ? 600 : 500,
|
||||
transition: "background 0.12s", textDecoration: "none",
|
||||
}}
|
||||
onMouseEnter={e => { if (!isActive) (e.currentTarget as HTMLElement).style.background = "#f6f4f0"; }}
|
||||
onMouseLeave={e => { if (!isActive) (e.currentTarget as HTMLElement).style.background = "transparent"; }}
|
||||
<Link
|
||||
key={n.id}
|
||||
href={n.href}
|
||||
title={collapsed ? n.label : undefined}
|
||||
style={{
|
||||
width: "100%",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: collapsed ? "center" : "flex-start",
|
||||
gap: 8,
|
||||
padding: collapsed ? "8px 0" : "7px 10px",
|
||||
borderRadius: 6,
|
||||
background: isActive ? "#f6f4f0" : "transparent",
|
||||
color: isActive ? "#1a1a1a" : "#6b6560",
|
||||
fontSize: "0.8rem",
|
||||
fontWeight: isActive ? 600 : 500,
|
||||
transition: "background 0.12s",
|
||||
textDecoration: "none",
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
if (!isActive)
|
||||
(e.currentTarget as HTMLElement).style.background = "#f6f4f0";
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
if (!isActive)
|
||||
(e.currentTarget as HTMLElement).style.background =
|
||||
"transparent";
|
||||
}}
|
||||
>
|
||||
<span style={{ fontSize: collapsed ? "0.95rem" : "0.78rem", opacity: collapsed ? (isActive ? 0.9 : 0.45) : 0.45, width: collapsed ? "auto" : 16, textAlign: "center" }}>
|
||||
<span
|
||||
style={{
|
||||
fontSize: collapsed ? "0.95rem" : "0.78rem",
|
||||
opacity: collapsed ? (isActive ? 0.9 : 0.45) : 0.45,
|
||||
width: collapsed ? "auto" : 16,
|
||||
textAlign: "center",
|
||||
}}
|
||||
>
|
||||
{n.icon}
|
||||
</span>
|
||||
{!collapsed && n.label}
|
||||
@@ -181,51 +338,101 @@ export function VIBNSidebar({ workspace, tabs, activeTab }: VIBNSidebarProps) {
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div style={{ height: 1, background: "#eae6de", margin: "8px 14px", flexShrink: 0 }} />
|
||||
<div
|
||||
style={{
|
||||
height: 1,
|
||||
background: "#eae6de",
|
||||
margin: "8px 14px",
|
||||
flexShrink: 0,
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* ── Lower section ── */}
|
||||
<div style={{ flex: 1, overflow: "auto", paddingBottom: 8 }}>
|
||||
|
||||
{activeProjectId && project ? (
|
||||
/* ── PROJECT VIEW: name + status + section tabs ── */
|
||||
<>
|
||||
{!collapsed && (
|
||||
<>
|
||||
<div style={{ padding: "6px 12px 8px" }}>
|
||||
<div style={{ fontSize: "0.82rem", fontWeight: 700, color: "#1a1a1a", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>
|
||||
<div
|
||||
style={{
|
||||
fontSize: "0.82rem",
|
||||
fontWeight: 700,
|
||||
color: "#1a1a1a",
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
whiteSpace: "nowrap",
|
||||
}}
|
||||
>
|
||||
{project.productName || project.name || "Project"}
|
||||
</div>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 5, marginTop: 3 }}>
|
||||
<span style={{
|
||||
width: 6, height: 6, borderRadius: "50%", flexShrink: 0, display: "inline-block",
|
||||
background: project.status === "live" ? "#2e7d32"
|
||||
: project.status === "building" ? "#3d5afe"
|
||||
: "#d4a04a",
|
||||
}} />
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 5,
|
||||
marginTop: 3,
|
||||
}}
|
||||
>
|
||||
<span
|
||||
style={{
|
||||
width: 6,
|
||||
height: 6,
|
||||
borderRadius: "50%",
|
||||
flexShrink: 0,
|
||||
display: "inline-block",
|
||||
background:
|
||||
project.status === "live"
|
||||
? "#2e7d32"
|
||||
: project.status === "building"
|
||||
? "#3d5afe"
|
||||
: "#d4a04a",
|
||||
}}
|
||||
/>
|
||||
<span style={{ fontSize: "0.68rem", color: "#8a8478" }}>
|
||||
{project.status === "live" ? "Live" : project.status === "building" ? "Building" : "Defining"}
|
||||
{project.status === "live"
|
||||
? "Live"
|
||||
: project.status === "building"
|
||||
? "Building"
|
||||
: "Defining"}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{tabs && tabs.length > 0 && (
|
||||
<div style={{ padding: "2px 8px" }}>
|
||||
{tabs.map(t => {
|
||||
{tabs.map((t) => {
|
||||
const isActive = activeTab === t.id;
|
||||
return (
|
||||
<Link
|
||||
key={t.id}
|
||||
href={`/${workspace}/project/${activeProjectId}/${t.path}`}
|
||||
style={{
|
||||
width: "100%", display: "flex", alignItems: "center",
|
||||
padding: "7px 10px", borderRadius: 6,
|
||||
width: "100%",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
padding: "7px 10px",
|
||||
borderRadius: 6,
|
||||
background: isActive ? "#f6f4f0" : "transparent",
|
||||
color: isActive ? "#1a1a1a" : "#6b6560",
|
||||
fontSize: "0.8rem", fontWeight: isActive ? 600 : 500,
|
||||
transition: "background 0.12s", textDecoration: "none",
|
||||
fontSize: "0.8rem",
|
||||
fontWeight: isActive ? 600 : 500,
|
||||
transition: "background 0.12s",
|
||||
textDecoration: "none",
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
if (!isActive)
|
||||
(
|
||||
e.currentTarget as HTMLElement
|
||||
).style.background = "#f6f4f0";
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
if (!isActive)
|
||||
(
|
||||
e.currentTarget as HTMLElement
|
||||
).style.background = "transparent";
|
||||
}}
|
||||
onMouseEnter={e => { if (!isActive) (e.currentTarget as HTMLElement).style.background = "#f6f4f0"; }}
|
||||
onMouseLeave={e => { if (!isActive) (e.currentTarget as HTMLElement).style.background = "transparent"; }}
|
||||
>
|
||||
{t.label}
|
||||
</Link>
|
||||
@@ -236,36 +443,69 @@ export function VIBNSidebar({ workspace, tabs, activeTab }: VIBNSidebarProps) {
|
||||
</>
|
||||
)}
|
||||
{collapsed && (
|
||||
<div style={{ display: "flex", flexDirection: "column", alignItems: "center", paddingTop: 8, gap: 6 }}>
|
||||
<span style={{
|
||||
width: 7, height: 7, borderRadius: "50%", display: "inline-block",
|
||||
background: project.status === "live" ? "#2e7d32"
|
||||
: project.status === "building" ? "#3d5afe"
|
||||
: "#d4a04a",
|
||||
}} title={project.productName || project.name} />
|
||||
{tabs && tabs.map(t => {
|
||||
const isActive = activeTab === t.id;
|
||||
return (
|
||||
<Link
|
||||
key={t.id}
|
||||
href={`/${workspace}/project/${activeProjectId}/${t.path}`}
|
||||
title={t.label}
|
||||
style={{
|
||||
width: 28, height: 28, borderRadius: 6, display: "flex",
|
||||
alignItems: "center", justifyContent: "center",
|
||||
background: isActive ? "#f6f4f0" : "transparent",
|
||||
color: isActive ? "#1a1a1a" : "#a09a90",
|
||||
fontSize: "0.6rem", fontWeight: 700, textDecoration: "none",
|
||||
textTransform: "uppercase", letterSpacing: "0.02em",
|
||||
transition: "background 0.12s",
|
||||
}}
|
||||
onMouseEnter={e => { if (!isActive) (e.currentTarget as HTMLElement).style.background = "#f6f4f0"; }}
|
||||
onMouseLeave={e => { if (!isActive) (e.currentTarget as HTMLElement).style.background = "transparent"; }}
|
||||
>
|
||||
{t.label.slice(0, 2)}
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
paddingTop: 8,
|
||||
gap: 6,
|
||||
}}
|
||||
>
|
||||
<span
|
||||
style={{
|
||||
width: 7,
|
||||
height: 7,
|
||||
borderRadius: "50%",
|
||||
display: "inline-block",
|
||||
background:
|
||||
project.status === "live"
|
||||
? "#2e7d32"
|
||||
: project.status === "building"
|
||||
? "#3d5afe"
|
||||
: "#d4a04a",
|
||||
}}
|
||||
title={project.productName || project.name}
|
||||
/>
|
||||
{tabs &&
|
||||
tabs.map((t) => {
|
||||
const isActive = activeTab === t.id;
|
||||
return (
|
||||
<Link
|
||||
key={t.id}
|
||||
href={`/${workspace}/project/${activeProjectId}/${t.path}`}
|
||||
title={t.label}
|
||||
style={{
|
||||
width: 28,
|
||||
height: 28,
|
||||
borderRadius: 6,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
background: isActive ? "#f6f4f0" : "transparent",
|
||||
color: isActive ? "#1a1a1a" : "#a09a90",
|
||||
fontSize: "0.6rem",
|
||||
fontWeight: 700,
|
||||
textDecoration: "none",
|
||||
textTransform: "uppercase",
|
||||
letterSpacing: "0.02em",
|
||||
transition: "background 0.12s",
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
if (!isActive)
|
||||
(e.currentTarget as HTMLElement).style.background =
|
||||
"#f6f4f0";
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
if (!isActive)
|
||||
(e.currentTarget as HTMLElement).style.background =
|
||||
"transparent";
|
||||
}}
|
||||
>
|
||||
{t.label.slice(0, 2)}
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
@@ -273,32 +513,77 @@ export function VIBNSidebar({ workspace, tabs, activeTab }: VIBNSidebarProps) {
|
||||
/* ── GLOBAL VIEW: projects list ── */
|
||||
<div style={{ padding: collapsed ? "2px 6px" : "2px 8px" }}>
|
||||
{!collapsed && (
|
||||
<div style={{ fontSize: "0.58rem", fontWeight: 600, color: "#a09a90", letterSpacing: "0.1em", textTransform: "uppercase", padding: "6px 10px 8px" }}>
|
||||
<div
|
||||
style={{
|
||||
fontSize: "0.58rem",
|
||||
fontWeight: 600,
|
||||
color: "#a09a90",
|
||||
letterSpacing: "0.1em",
|
||||
textTransform: "uppercase",
|
||||
padding: "6px 10px 8px",
|
||||
}}
|
||||
>
|
||||
Projects
|
||||
</div>
|
||||
)}
|
||||
{projects.map(p => {
|
||||
{projects.map((p) => {
|
||||
const isActive = activeProjectId === p.id;
|
||||
const color = p.status === "live" ? "#2e7d32" : p.status === "building" ? "#3d5afe" : "#d4a04a";
|
||||
const color =
|
||||
p.status === "live"
|
||||
? "#2e7d32"
|
||||
: p.status === "building"
|
||||
? "#3d5afe"
|
||||
: "#d4a04a";
|
||||
return (
|
||||
<Link key={p.id} href={`/${workspace}/project/${p.id}`}
|
||||
<Link
|
||||
key={p.id}
|
||||
href={`/${workspace}/project/${p.id}`}
|
||||
title={collapsed ? p.productName : undefined}
|
||||
style={{
|
||||
width: "100%", display: "flex", alignItems: "center",
|
||||
width: "100%",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: collapsed ? "center" : "flex-start",
|
||||
gap: 9, padding: collapsed ? "9px 0" : "7px 10px",
|
||||
gap: 9,
|
||||
padding: collapsed ? "9px 0" : "7px 10px",
|
||||
borderRadius: 6,
|
||||
background: isActive ? "#f6f4f0" : "transparent",
|
||||
color: "#1a1a1a", fontSize: "0.8rem",
|
||||
color: "#1a1a1a",
|
||||
fontSize: "0.8rem",
|
||||
fontWeight: isActive ? 600 : 450,
|
||||
transition: "background 0.12s", textDecoration: "none", overflow: "hidden",
|
||||
transition: "background 0.12s",
|
||||
textDecoration: "none",
|
||||
overflow: "hidden",
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
if (!isActive)
|
||||
(e.currentTarget as HTMLElement).style.background =
|
||||
"#f6f4f0";
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
if (!isActive)
|
||||
(e.currentTarget as HTMLElement).style.background =
|
||||
"transparent";
|
||||
}}
|
||||
onMouseEnter={e => { if (!isActive) (e.currentTarget as HTMLElement).style.background = "#f6f4f0"; }}
|
||||
onMouseLeave={e => { if (!isActive) (e.currentTarget as HTMLElement).style.background = "transparent"; }}
|
||||
>
|
||||
<span style={{ width: 7, height: 7, borderRadius: "50%", background: color, display: "inline-block", flexShrink: 0 }} />
|
||||
<span
|
||||
style={{
|
||||
width: 7,
|
||||
height: 7,
|
||||
borderRadius: "50%",
|
||||
background: color,
|
||||
display: "inline-block",
|
||||
flexShrink: 0,
|
||||
}}
|
||||
/>
|
||||
{!collapsed && (
|
||||
<span style={{ overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>
|
||||
<span
|
||||
style={{
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
whiteSpace: "nowrap",
|
||||
}}
|
||||
>
|
||||
{p.productName}
|
||||
</span>
|
||||
)}
|
||||
@@ -310,32 +595,68 @@ export function VIBNSidebar({ workspace, tabs, activeTab }: VIBNSidebarProps) {
|
||||
</div>
|
||||
|
||||
{/* ── User footer ── */}
|
||||
<div style={{
|
||||
padding: collapsed ? "10px 0" : "12px 14px",
|
||||
borderTop: "1px solid #eae6de",
|
||||
display: "flex", alignItems: "center",
|
||||
justifyContent: collapsed ? "center" : "flex-start",
|
||||
gap: 9, flexShrink: 0,
|
||||
}}>
|
||||
<div title={collapsed ? (session?.user?.name ?? session?.user?.email ?? "Account") : undefined}
|
||||
<div
|
||||
style={{
|
||||
padding: collapsed ? "10px 0" : "12px 14px",
|
||||
borderTop: "1px solid #eae6de",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: collapsed ? "center" : "flex-start",
|
||||
gap: 9,
|
||||
flexShrink: 0,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
title={
|
||||
collapsed
|
||||
? (session?.user?.name ?? session?.user?.email ?? "Account")
|
||||
: undefined
|
||||
}
|
||||
style={{
|
||||
width: 26, height: 26, borderRadius: "50%",
|
||||
background: "#f0ece4", display: "flex", alignItems: "center",
|
||||
justifyContent: "center", fontSize: "0.7rem", fontWeight: 600,
|
||||
color: "#8a8478", flexShrink: 0, cursor: "default",
|
||||
}}>
|
||||
width: 26,
|
||||
height: 26,
|
||||
borderRadius: "50%",
|
||||
background: "#f0ece4",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
fontSize: "0.7rem",
|
||||
fontWeight: 600,
|
||||
color: "#8a8478",
|
||||
flexShrink: 0,
|
||||
cursor: "default",
|
||||
}}
|
||||
>
|
||||
{userInitial}
|
||||
</div>
|
||||
{!collapsed && (
|
||||
<div style={{ flex: 1, minWidth: 0 }}>
|
||||
<div style={{ fontSize: "0.76rem", fontWeight: 500, color: "#1a1a1a", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>
|
||||
{session?.user?.name ?? session?.user?.email?.split("@")[0] ?? "Account"}
|
||||
<div
|
||||
style={{
|
||||
fontSize: "0.76rem",
|
||||
fontWeight: 500,
|
||||
color: "#1a1a1a",
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
whiteSpace: "nowrap",
|
||||
}}
|
||||
>
|
||||
{session?.user?.name ??
|
||||
session?.user?.email?.split("@")[0] ??
|
||||
"Account"}
|
||||
</div>
|
||||
<button onClick={() => signOut({ callbackUrl: "/auth" })} style={{
|
||||
background: "none", border: "none", padding: 0,
|
||||
fontSize: "0.62rem", color: "#a09a90", cursor: "pointer",
|
||||
fontFamily: "var(--font-inter), ui-sans-serif, sans-serif",
|
||||
}}>
|
||||
<button
|
||||
onClick={() => signOut({ callbackUrl: "/signin" })}
|
||||
style={{
|
||||
background: "none",
|
||||
border: "none",
|
||||
padding: 0,
|
||||
fontSize: "0.62rem",
|
||||
color: "#a09a90",
|
||||
cursor: "pointer",
|
||||
fontFamily: "var(--font-inter), ui-sans-serif, sans-serif",
|
||||
}}
|
||||
>
|
||||
Sign out
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -1,205 +0,0 @@
|
||||
/**
|
||||
* Marketing content for the homepage
|
||||
* Centralized location for all copy to make updates easier
|
||||
*/
|
||||
|
||||
export const homepage = {
|
||||
hero: {
|
||||
title: "AI Coding Tools for Vibe Coders.",
|
||||
subtitle: "We take you from idea to market and beyond — so you can finally finish what you start and share it with the world.",
|
||||
cta: {
|
||||
primary: "Start Your Idea",
|
||||
secondary: "See How It Works",
|
||||
},
|
||||
},
|
||||
|
||||
emotionalHook: {
|
||||
title: "You've been waiting",
|
||||
subtitle: "Now you can",
|
||||
description: "You've had the ideas. You've started the projects. But somewhere between no-code limitations and overwhelming dev tools, momentum stalls. Not anymore.",
|
||||
},
|
||||
|
||||
whoItsFor: {
|
||||
title: "Creators stuck between ideas and code",
|
||||
subtitle: "You're a Vibe Coder",
|
||||
description: "You're ambitious. You have vision. You know what you want to build. But you're not fluent in code yet, and no-code tools feel limiting. You're stuck in the gap — and we built Vibn to meet you there.",
|
||||
traits: [
|
||||
"You have big ideas but hit walls with no-code tools",
|
||||
"You want to learn to code, but need to ship while you learn",
|
||||
"You're tired of starting projects that never launch",
|
||||
"You need guidance, not just more tutorials",
|
||||
"You want to build real products, not just prototypes",
|
||||
],
|
||||
},
|
||||
|
||||
transformation: {
|
||||
title: "From stalled chaos to unstoppable momentum",
|
||||
description: "When you join Vibn, something shifts. The overwhelm fades. The path becomes clear. You're not just learning — you're building. You're not just building — you're shipping. And you're not alone.",
|
||||
outcomes: [
|
||||
"Clarity on what to build next",
|
||||
"Confidence in your technical decisions",
|
||||
"Momentum that carries you forward",
|
||||
"A product you're proud to share",
|
||||
],
|
||||
},
|
||||
|
||||
features: {
|
||||
title: "Everything you need in one calm, guided flow",
|
||||
description: "Vibn gives you structure without rigidity, guidance without hand-holding, and momentum without overwhelm.",
|
||||
list: [
|
||||
{
|
||||
title: "Project Home",
|
||||
description: "One central place for your vision, progress, and next steps.",
|
||||
},
|
||||
{
|
||||
title: "Scope & Roadmap",
|
||||
description: "Define what you're building and break it into achievable milestones.",
|
||||
},
|
||||
{
|
||||
title: "AI Guardrails",
|
||||
description: "Keep AI coding assistants on track with your project's scope and standards.",
|
||||
},
|
||||
{
|
||||
title: "Build, Don't Babysit",
|
||||
description: "Track your coding sessions, costs, and progress automatically.",
|
||||
},
|
||||
{
|
||||
title: "Backend & Hosting Simplified",
|
||||
description: "Deploy with confidence. No DevOps degree required.",
|
||||
},
|
||||
{
|
||||
title: "v0 Integration",
|
||||
description: "Generate beautiful UI components and iterate visually.",
|
||||
},
|
||||
{
|
||||
title: "Collaboration & Chat",
|
||||
description: "Get unstuck with AI and human support that understands your project.",
|
||||
},
|
||||
{
|
||||
title: "Launch Suite",
|
||||
description: "Go from localhost to production with guided deployment.",
|
||||
},
|
||||
{
|
||||
title: "Market & Grow",
|
||||
description: "Tools to help you share your product and find your first users.",
|
||||
},
|
||||
{
|
||||
title: "Progress & Cost Tracking",
|
||||
description: "Stay on budget. Stay motivated. See how far you've come.",
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
howItWorks: {
|
||||
title: "Plan. Build. Launch. Grow",
|
||||
description: "Vibn guides you from idea to market, one clear step at a time.",
|
||||
steps: [
|
||||
{
|
||||
number: 1,
|
||||
title: "Start with your idea",
|
||||
description: "Tell us what you want to build. We'll help you define scope and create a roadmap that feels achievable.",
|
||||
},
|
||||
{
|
||||
number: 2,
|
||||
title: "Build with AI assistance",
|
||||
description: "Use Cursor, ChatGPT, and other AI tools — but with guardrails that keep you on track and on budget.",
|
||||
},
|
||||
{
|
||||
number: 3,
|
||||
title: "Launch with confidence",
|
||||
description: "Deploy your product with guided setup for hosting, domains, and everything you need to go live.",
|
||||
},
|
||||
{
|
||||
number: 4,
|
||||
title: "Grow your product",
|
||||
description: "Market your launch, gather feedback, and iterate with the same support that got you here.",
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
pricing: {
|
||||
title: "Simple, fair, transparent",
|
||||
description: "Pay for what you use. No surprises. No hidden fees.",
|
||||
tiers: [
|
||||
{
|
||||
name: "Starter",
|
||||
price: "Free",
|
||||
description: "For exploring and getting started",
|
||||
features: [
|
||||
"1 active project",
|
||||
"Basic AI tracking",
|
||||
"Community support",
|
||||
"7-day history",
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Creator",
|
||||
price: "$29/mo",
|
||||
description: "For builders shipping real products",
|
||||
features: [
|
||||
"5 active projects",
|
||||
"Full AI tracking & analytics",
|
||||
"Priority support",
|
||||
"Unlimited history",
|
||||
"Deployment guides",
|
||||
"v0 integration",
|
||||
],
|
||||
highlighted: true,
|
||||
},
|
||||
{
|
||||
name: "Studio",
|
||||
price: "$99/mo",
|
||||
description: "For teams and agencies",
|
||||
features: [
|
||||
"Unlimited projects",
|
||||
"Team collaboration",
|
||||
"White-label options",
|
||||
"Custom integrations",
|
||||
"Dedicated support",
|
||||
"SLA guarantee",
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
finalCTA: {
|
||||
title: "Start your idea — we'll meet you where you are and help you finish what you start.",
|
||||
cta: {
|
||||
primary: "Get Started Free",
|
||||
secondary: "Book a Demo",
|
||||
},
|
||||
},
|
||||
|
||||
meta: {
|
||||
title: "VIBN - From Ideas to Market for Vibe Coders",
|
||||
description: "AI coding tools aren't made for you. We are. Take your product from idea to market with guidance, structure, and momentum.",
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Future content sections (from PROJECT_INSTRUCTIONS.md)
|
||||
* TODO: Implement these sections
|
||||
*/
|
||||
export const futureContent = {
|
||||
emotionalHook: {
|
||||
title: "You've been waiting. Now you can.",
|
||||
// Short, empathetic copy about frustration and transition
|
||||
},
|
||||
whoItsFor: {
|
||||
title: "Creators stuck between ideas and code",
|
||||
// Describes Vibe Coders: ambitious, not yet fluent in code
|
||||
},
|
||||
transformation: {
|
||||
title: "From stalled chaos to unstoppable momentum",
|
||||
// Emotional shift when users join VIBN
|
||||
},
|
||||
howItWorks: {
|
||||
title: "Plan. Build. Launch. Grow.",
|
||||
// Step-based explanation
|
||||
},
|
||||
pricing: {
|
||||
title: "Simple, fair, transparent",
|
||||
// Pricing tiers
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
// Public components exports from subdirectories when needed
|
||||
@@ -1,21 +0,0 @@
|
||||
import Link from "next/link";
|
||||
|
||||
/** Compact nav from justine/02_signup.html — use inside [data-justine-auth] + 02-signup.css */
|
||||
export function JustineAuthShell({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<>
|
||||
<nav className="justine-auth-nav" aria-label="Auth">
|
||||
<Link href="/" className="justine-auth-nav-brand">
|
||||
<div className="justine-auth-nav-logo">
|
||||
<span className="f">V</span>
|
||||
</div>
|
||||
<span className="justine-auth-nav-wordmark f">vibn</span>
|
||||
</Link>
|
||||
<span className="justine-auth-nav-aside">
|
||||
New to vibn? <Link href="/">View homepage</Link>
|
||||
</span>
|
||||
</nav>
|
||||
<div className="justine-auth-main">{children}</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
import Link from "next/link";
|
||||
|
||||
/** Footer from justine/01_homepage.html */
|
||||
export function JustineFooter() {
|
||||
return (
|
||||
<footer>
|
||||
<div>
|
||||
<span className="f" style={{ fontSize: 16, fontWeight: 700, color: "var(--ink)" }}>
|
||||
vibn
|
||||
</span>
|
||||
<span className="footer-tagline">The fastest way from idea to product.</span>
|
||||
</div>
|
||||
<div className="footer-links">
|
||||
<Link href="/#how-it-works" style={{ fontSize: 13, color: "var(--muted)", textDecoration: "none" }}>
|
||||
How it works
|
||||
</Link>
|
||||
<Link href="/pricing" style={{ fontSize: 13, color: "var(--muted)", textDecoration: "none" }}>
|
||||
Pricing
|
||||
</Link>
|
||||
<Link href="/privacy" style={{ fontSize: 13, color: "var(--muted)", textDecoration: "none" }}>
|
||||
Privacy
|
||||
</Link>
|
||||
<Link href="/terms" style={{ fontSize: 13, color: "var(--muted)", textDecoration: "none" }}>
|
||||
Terms
|
||||
</Link>
|
||||
</div>
|
||||
<span style={{ fontSize: 12.5, color: "var(--muted)", textAlign: "right", display: "block" }}>
|
||||
© {new Date().getFullYear()} vibn
|
||||
</span>
|
||||
</footer>
|
||||
);
|
||||
}
|
||||
@@ -1,518 +0,0 @@
|
||||
import type { CSSProperties } from "react";
|
||||
import Link from "next/link";
|
||||
|
||||
/**
|
||||
* Body sections from justine/01_homepage.html — inline styles + classes match the source file.
|
||||
* Lives under [data-justine]; tokens are --ink, --mid, --muted, --border, --white (see 01-homepage.css).
|
||||
*/
|
||||
export function JustineHomePage() {
|
||||
return (
|
||||
<div className="justine-home-page">
|
||||
<section
|
||||
className="hero-section"
|
||||
style={{ maxWidth: 980, margin: "0 auto", padding: "88px 52px 72px" }}
|
||||
>
|
||||
<div className="hero-grid">
|
||||
<div>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 11,
|
||||
fontWeight: 600,
|
||||
letterSpacing: "0.13em",
|
||||
textTransform: "uppercase",
|
||||
color: "var(--muted)",
|
||||
marginBottom: 22,
|
||||
}}
|
||||
>
|
||||
For non-technical founders
|
||||
</div>
|
||||
<h1
|
||||
className="f hero-h1"
|
||||
style={{
|
||||
fontSize: 58,
|
||||
fontWeight: 700,
|
||||
color: "var(--ink)",
|
||||
letterSpacing: "-0.03em",
|
||||
lineHeight: 1.06,
|
||||
marginBottom: 28,
|
||||
}}
|
||||
>
|
||||
You have the idea.
|
||||
<br />
|
||||
We handle
|
||||
<br />
|
||||
<em className="gradient-em">everything else.</em>
|
||||
</h1>
|
||||
<p className="hero-sub" style={{ fontSize: 17, color: "var(--mid)", lineHeight: 1.75 }}>
|
||||
You describe it. Vibn builds it, launches it, and markets it. From idea to{" "}
|
||||
<strong style={{ color: "var(--ink)" }}>live</strong> product in{" "}
|
||||
<strong style={{ color: "var(--ink)" }}>72 hours</strong> — no code, no agencies, no
|
||||
waiting.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div style={{ flexShrink: 0 }}>
|
||||
<div
|
||||
style={{
|
||||
background: "var(--white)",
|
||||
border: "1px solid var(--border)",
|
||||
borderRadius: 16,
|
||||
overflow: "hidden",
|
||||
boxShadow: "0 20px 60px rgba(30,27,75,0.05)",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
padding: "24px 26px 20px",
|
||||
background: "#FCFCFF",
|
||||
borderBottom: "1px solid var(--border)",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 10,
|
||||
fontWeight: 600,
|
||||
letterSpacing: "0.12em",
|
||||
textTransform: "uppercase",
|
||||
color: "var(--muted)",
|
||||
marginBottom: 12,
|
||||
}}
|
||||
>
|
||||
Your idea
|
||||
</div>
|
||||
<p
|
||||
className="f"
|
||||
style={{
|
||||
fontSize: 15,
|
||||
fontStyle: "italic",
|
||||
color: "var(--ink)",
|
||||
lineHeight: 1.65,
|
||||
marginBottom: 14,
|
||||
}}
|
||||
>
|
||||
"I want to build a booking tool for independent personal trainers."
|
||||
</p>
|
||||
<div style={{ display: "flex", justifyContent: "flex-end" }}>
|
||||
<span
|
||||
style={{
|
||||
fontSize: 11,
|
||||
color: "var(--muted)",
|
||||
background: "var(--white)",
|
||||
border: "1px solid var(--border)",
|
||||
borderRadius: 5,
|
||||
padding: "3px 9px",
|
||||
letterSpacing: "0.04em",
|
||||
}}
|
||||
>
|
||||
↵ Enter
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ padding: "20px 26px 24px", background: "var(--white)" }}>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 10,
|
||||
fontWeight: 600,
|
||||
letterSpacing: "0.12em",
|
||||
textTransform: "uppercase",
|
||||
color: "var(--muted)",
|
||||
marginBottom: 16,
|
||||
}}
|
||||
>
|
||||
vibn generated
|
||||
</div>
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 0 }}>
|
||||
{[
|
||||
["Pages", "Landing, Dashboard, Booking, Payments"],
|
||||
["Stack", "Auth, database, payments — handled"],
|
||||
["Revenue", "Subscription · $29 / mo"],
|
||||
].map(([k, v]) => (
|
||||
<div
|
||||
key={k}
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "baseline",
|
||||
padding: "10px 0",
|
||||
borderBottom: "1px solid var(--border)",
|
||||
}}
|
||||
>
|
||||
<span style={{ fontSize: 12, color: "var(--muted)", fontWeight: 500 }}>{k}</span>
|
||||
<span style={{ fontSize: 13, color: "var(--ink)", fontWeight: 600 }}>{v}</span>
|
||||
</div>
|
||||
))}
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "baseline",
|
||||
padding: "10px 0",
|
||||
}}
|
||||
>
|
||||
<span style={{ fontSize: 12, color: "var(--muted)", fontWeight: 500 }}>Status</span>
|
||||
<span style={{ fontSize: 13, fontWeight: 600, color: "#6366F1" }}>
|
||||
⬤ Ready to build
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
textAlign: "center",
|
||||
gap: 10,
|
||||
marginTop: 52,
|
||||
}}
|
||||
>
|
||||
<Link href="/auth?new=1">
|
||||
<button type="button" className="btn-ink-lg">
|
||||
Start free — no code needed
|
||||
</button>
|
||||
</Link>
|
||||
<div>
|
||||
<span style={{ fontSize: 13.5, color: "#818CF8" }}>★★★★★</span>
|
||||
<span style={{ fontSize: 13.5, color: "var(--stone)" }}>
|
||||
280 founders launched
|
||||
</span>
|
||||
</div>
|
||||
<p style={{ fontSize: 12, color: "#9CA3AF" }}>No credit card required · Free forever plan</p>
|
||||
<Link
|
||||
href="/#how-it-works"
|
||||
style={{
|
||||
fontSize: 13.5,
|
||||
color: "#6366F1",
|
||||
textDecoration: "none",
|
||||
fontWeight: 500,
|
||||
marginTop: 4,
|
||||
}}
|
||||
>
|
||||
See how it works →
|
||||
</Link>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section
|
||||
className="empathy-section"
|
||||
style={{ borderTop: "1px solid var(--border)", borderBottom: "1px solid var(--border)", padding: "80px 52px" }}
|
||||
>
|
||||
<div style={{ maxWidth: 980, margin: "0 auto" }}>
|
||||
<div className="empathy-grid">
|
||||
<div>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 11,
|
||||
fontWeight: 600,
|
||||
letterSpacing: "0.13em",
|
||||
textTransform: "uppercase",
|
||||
color: "var(--muted)",
|
||||
marginBottom: 18,
|
||||
}}
|
||||
>
|
||||
Sound familiar?
|
||||
</div>
|
||||
<h2
|
||||
className="f"
|
||||
style={{
|
||||
fontSize: 36,
|
||||
fontWeight: 700,
|
||||
color: "#1A1A1A",
|
||||
lineHeight: 1.18,
|
||||
marginBottom: 24,
|
||||
letterSpacing: "-0.02em",
|
||||
}}
|
||||
>
|
||||
The idea is the hard part.{" "}
|
||||
<span className="gradient-text">Everything else shouldn't be.</span>
|
||||
</h2>
|
||||
<p style={{ fontSize: 15, color: "var(--mid)", lineHeight: 1.82, marginBottom: 20 }}>
|
||||
You know exactly what you want to build and who it's for. But the moment you
|
||||
think about servers, databases, deployment pipelines, SEO — the whole thing stalls.
|
||||
</p>
|
||||
<p style={{ fontSize: 15, color: "var(--mid)", lineHeight: 1.82 }}>
|
||||
vibn exists to remove all of that. Not abstract it —{" "}
|
||||
<em className="f" style={{ fontStyle: "italic" }}>
|
||||
remove it entirely.
|
||||
</em>
|
||||
</p>
|
||||
</div>
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 14 }}>
|
||||
{[
|
||||
{
|
||||
t: `No more "I need to hire a developer first"`,
|
||||
d: "vibn is your developer. Start building the moment you have an idea.",
|
||||
},
|
||||
{
|
||||
t: "No more staring at a blank marketing calendar",
|
||||
d: "AI generates and publishes your content every single week.",
|
||||
},
|
||||
{
|
||||
t: `No more "I'll launch when it's ready"`,
|
||||
d: "Most founders ship their first version in under 72 hours.",
|
||||
},
|
||||
].map((row) => (
|
||||
<div key={row.t} className="empathy-card">
|
||||
<div
|
||||
style={{
|
||||
width: 20,
|
||||
height: 20,
|
||||
borderRadius: "50%",
|
||||
border: "1.5px solid rgba(99,102,241,0.4)",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
flexShrink: 0,
|
||||
marginTop: 2,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
width: 7,
|
||||
height: 7,
|
||||
borderRadius: "50%",
|
||||
background: "#6366F1",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<div className="f" style={{ fontSize: 14, fontWeight: 600, color: "#1A1A1A", marginBottom: 4 }}>
|
||||
{row.t}
|
||||
</div>
|
||||
<div style={{ fontSize: 13, color: "var(--mid)", lineHeight: 1.7 }}>{row.d}</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="how-it-works" className="how-section" style={{ maxWidth: 980, margin: "0 auto", padding: "84px 52px" }}>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 11,
|
||||
fontWeight: 600,
|
||||
letterSpacing: "0.13em",
|
||||
textTransform: "uppercase",
|
||||
color: "var(--muted)",
|
||||
marginBottom: 16,
|
||||
}}
|
||||
>
|
||||
How it works
|
||||
</div>
|
||||
<h2
|
||||
className="f"
|
||||
style={{
|
||||
fontSize: 42,
|
||||
fontWeight: 700,
|
||||
color: "#1A1A1A",
|
||||
letterSpacing: "-0.02em",
|
||||
marginBottom: 54,
|
||||
maxWidth: 480,
|
||||
lineHeight: 1.15,
|
||||
}}
|
||||
>
|
||||
Four phases. One <span className="gradient-text">complete</span> product.
|
||||
</h2>
|
||||
<div className="phase-grid">
|
||||
{[
|
||||
{
|
||||
k: "01 — Discover",
|
||||
t: "Define your idea",
|
||||
p: "Six guided questions turn a rough idea into a full product plan — pages, architecture, revenue model. No jargon.",
|
||||
style: {
|
||||
padding: "40px 44px",
|
||||
background: "var(--white)",
|
||||
borderRight: "1px solid rgba(99,102,241,0.2)",
|
||||
borderBottom: "1px solid rgba(99,102,241,0.2)",
|
||||
} satisfies CSSProperties,
|
||||
},
|
||||
{
|
||||
k: "02 — Design",
|
||||
t: "Choose your style",
|
||||
p: "Pick a visual style and see your exact site and emails live before a single line of code is written.",
|
||||
style: {
|
||||
padding: "40px 44px",
|
||||
background: "var(--white)",
|
||||
borderBottom: "1px solid rgba(99,102,241,0.2)",
|
||||
} satisfies CSSProperties,
|
||||
},
|
||||
{
|
||||
k: "03 — Build",
|
||||
t: "Your app, live",
|
||||
p: "AI writes every line. Auth, database, payments, all pages — deployed and live. Describe changes in plain English.",
|
||||
style: {
|
||||
padding: "40px 44px",
|
||||
background: "var(--white)",
|
||||
borderRight: "1px solid rgba(99,102,241,0.2)",
|
||||
} satisfies CSSProperties,
|
||||
},
|
||||
{
|
||||
k: "04 — Grow",
|
||||
t: "Market & automate",
|
||||
p: "AI generates your blog, emails, and social schedule — publishing on autopilot so you can focus on users.",
|
||||
style: { padding: "40px 44px", background: "var(--white)" } satisfies CSSProperties,
|
||||
},
|
||||
].map((cell) => (
|
||||
<div key={cell.k} style={cell.style}>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 11,
|
||||
fontWeight: 600,
|
||||
letterSpacing: "0.1em",
|
||||
textTransform: "uppercase",
|
||||
color: "rgba(99,102,241,0.6)",
|
||||
marginBottom: 14,
|
||||
}}
|
||||
>
|
||||
{cell.k}
|
||||
</div>
|
||||
<div className="f" style={{ fontSize: 22, fontWeight: 700, color: "#1A1A1A", marginBottom: 10 }}>
|
||||
{cell.t}
|
||||
</div>
|
||||
<p style={{ fontSize: 13.5, color: "var(--mid)", lineHeight: 1.72 }}>{cell.p}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section style={{ background: "var(--white)", borderTop: "1px solid var(--border)", borderBottom: "1px solid var(--border)" }}>
|
||||
<div className="wyg-grid wyg-section" style={{ maxWidth: 980, margin: "0 auto", padding: "0 52px" }}>
|
||||
<div style={{ padding: "44px 40px 44px 0", borderRight: "1px solid var(--border)" }}>
|
||||
<div style={{ fontSize: 13, fontWeight: 700, color: "#6366F1", marginBottom: 12, textAlign: "center" }}>✦</div>
|
||||
<div className="f" style={{ fontSize: 17, fontWeight: 700, color: "#1A1A1A", marginBottom: 8, textAlign: "center" }}>
|
||||
A live, working product
|
||||
</div>
|
||||
<p style={{ fontSize: 13.5, color: "var(--mid)", lineHeight: 1.7, textAlign: "center" }}>
|
||||
Not a prototype. Real auth, real payments, real database — on your own URL from day one.
|
||||
</p>
|
||||
<p style={{ fontSize: 12, color: "var(--muted)", lineHeight: 1.6, textAlign: "center", marginTop: 10 }}>
|
||||
Runs on your own servers — your data, your infrastructure, no lock-in.
|
||||
</p>
|
||||
</div>
|
||||
<div style={{ padding: "44px 40px", borderRight: "1px solid var(--border)" }}>
|
||||
<div style={{ fontSize: 13, fontWeight: 700, color: "#6366F1", marginBottom: 12, textAlign: "center" }}>✦</div>
|
||||
<div className="f" style={{ fontSize: 17, fontWeight: 700, color: "#1A1A1A", marginBottom: 8, textAlign: "center" }}>
|
||||
A full marketing engine
|
||||
</div>
|
||||
<p style={{ fontSize: 13.5, color: "var(--mid)", lineHeight: 1.7, textAlign: "center" }}>
|
||||
Blog posts, onboarding emails, and social content — written and published automatically every week.
|
||||
</p>
|
||||
</div>
|
||||
<div style={{ padding: "44px 0 44px 40px" }}>
|
||||
<div style={{ fontSize: 13, fontWeight: 700, color: "#6366F1", marginBottom: 12, textAlign: "center" }}>✦</div>
|
||||
<div className="f" style={{ fontSize: 17, fontWeight: 700, color: "#1A1A1A", marginBottom: 8, textAlign: "center" }}>
|
||||
A product that evolves
|
||||
</div>
|
||||
<p style={{ fontSize: 13.5, color: "var(--mid)", lineHeight: 1.7, textAlign: "center" }}>
|
||||
Describe changes in plain English. Vibn handles the code so your product grows as fast as your ideas.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="quote-section" style={{ background: "#1A1A1A", padding: "32px 52px 28px" }}>
|
||||
<div style={{ maxWidth: 980, margin: "0 auto" }}>
|
||||
<div className="quote-grid">
|
||||
<div className="quote-side" style={{ display: "flex", gap: 14, opacity: 0.85 }}>
|
||||
<div style={{ width: 2, background: "#6366F1", borderRadius: 2, flexShrink: 0 }} />
|
||||
<div>
|
||||
<p className="f" style={{ fontSize: 12.5, color: "#FFFFFF", lineHeight: 1.65, fontStyle: "italic", marginBottom: 8 }}>
|
||||
"I had the idea for 2 years. The backend terrified me. vibn shipped it in 4 days and handles all my marketing."
|
||||
</p>
|
||||
<span style={{ fontSize: 10.5, color: "var(--muted)", fontWeight: 600 }}>— Alex K., founder of Taskly</span>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ background: "rgba(255,255,255,0.05)", borderRadius: 12, padding: "22px 26px" }}>
|
||||
<div style={{ width: 3, height: 16, background: "#6366F1", borderRadius: 2, marginBottom: 12, opacity: 0.7 }} />
|
||||
<p className="f" style={{ fontSize: 16, color: "#FFFFFF", lineHeight: 1.7, fontStyle: "italic", marginBottom: 12 }}>
|
||||
"I have zero coding experience. Three weeks in, I have 300 paying users. That's entirely because of vibn."
|
||||
</p>
|
||||
<span style={{ fontSize: 11, color: "var(--muted)", fontWeight: 600 }}>— Marcus L., founder of Flowmatic</span>
|
||||
</div>
|
||||
<div className="quote-side" style={{ display: "flex", gap: 14, opacity: 0.85 }}>
|
||||
<div style={{ width: 2, background: "#6366F1", borderRadius: 2, flexShrink: 0 }} />
|
||||
<div>
|
||||
<p className="f" style={{ fontSize: 12.5, color: "#FFFFFF", lineHeight: 1.65, fontStyle: "italic", marginBottom: 8 }}>
|
||||
"The marketing autopilot saved me ten hours a week. My blog runs itself. I just focus on product."
|
||||
</p>
|
||||
<span style={{ fontSize: 10.5, color: "var(--muted)", fontWeight: 600 }}>— Sara R., founder of Nudge</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ display: "flex", justifyContent: "center", gap: 7 }}>
|
||||
<div style={{ width: 5, height: 5, borderRadius: "50%", background: "rgba(255,255,255,0.3)" }} />
|
||||
<div style={{ width: 16, height: 5, borderRadius: 3, background: "#FFFFFF" }} />
|
||||
<div style={{ width: 5, height: 5, borderRadius: "50%", background: "rgba(255,255,255,0.3)" }} />
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section style={{ background: "var(--white)", borderTop: "1px solid var(--border)", borderBottom: "1px solid var(--border)" }}>
|
||||
<div className="stats-grid stats-section" style={{ maxWidth: 980, margin: "0 auto", padding: "0 52px" }}>
|
||||
{[
|
||||
{ num: "280+", label: "founders launched", pl: 0, pr: true },
|
||||
{ num: "72h", label: "average time to first version", pl: 36, pr: true },
|
||||
{ num: "4.9★", label: "average rating", pl: 36, pr: true },
|
||||
{ num: "3×", label: "faster than hiring a developer", pl: 36, pr: false },
|
||||
].map((row) => (
|
||||
<div
|
||||
key={row.label}
|
||||
style={{
|
||||
padding: row.pl ? `40px 0 40px ${row.pl}px` : "40px 0",
|
||||
borderRight: row.pr ? "1px solid var(--border)" : undefined,
|
||||
}}
|
||||
>
|
||||
<div className="f gradient-num" style={{ fontSize: 40, fontWeight: 700, letterSpacing: "-0.03em", marginBottom: 6 }}>
|
||||
{row.num}
|
||||
</div>
|
||||
<div style={{ fontSize: 13, color: "var(--muted)" }}>{row.label}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="cta-section" style={{ padding: "80px 52px", textAlign: "center" }}>
|
||||
<div
|
||||
className="cta-card"
|
||||
style={{
|
||||
maxWidth: 680,
|
||||
margin: "0 auto",
|
||||
background: "#FFFFFF",
|
||||
borderRadius: 20,
|
||||
padding: "64px 52px",
|
||||
boxShadow: "0 0 0 1px rgba(99,102,241,0.15),0 20px 60px rgba(30,27,75,0.08)",
|
||||
}}
|
||||
>
|
||||
<h2
|
||||
className="f"
|
||||
style={{
|
||||
fontSize: 48,
|
||||
fontWeight: 700,
|
||||
color: "var(--ink)",
|
||||
letterSpacing: "-0.03em",
|
||||
lineHeight: 1.1,
|
||||
marginBottom: 20,
|
||||
}}
|
||||
>
|
||||
Your idea deserves to exist.
|
||||
</h2>
|
||||
<p style={{ fontSize: 16, color: "var(--mid)", lineHeight: 1.75, marginBottom: 38 }}>
|
||||
Thousands of ideas never make it past a spreadsheet. Yours doesn't have to be one of them.
|
||||
</p>
|
||||
<Link href="/auth?new=1">
|
||||
<button type="button" className="btn-ink-lg" style={{ marginBottom: 16 }}>
|
||||
Build my product — free
|
||||
</button>
|
||||
</Link>
|
||||
<div style={{ fontSize: 12.5, color: "var(--muted)" }}>Joins 280+ non-technical founders already live</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,115 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
|
||||
/** Nav from justine/01_homepage.html — classes defined in app/styles/justine/01-homepage.css */
|
||||
export function JustineNav() {
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const close = useCallback(() => {
|
||||
setOpen(false);
|
||||
document.body.style.overflow = "";
|
||||
}, []);
|
||||
|
||||
const toggle = useCallback(() => {
|
||||
setOpen((o) => {
|
||||
const next = !o;
|
||||
document.body.style.overflow = next ? "hidden" : "";
|
||||
return next;
|
||||
});
|
||||
}, []);
|
||||
|
||||
useEffect(() => () => {
|
||||
document.body.style.overflow = "";
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<nav>
|
||||
<Link href="/" style={{ display: "flex", alignItems: "center", gap: 10, textDecoration: "none" }}>
|
||||
<div
|
||||
className="logo-box"
|
||||
style={{
|
||||
width: 30,
|
||||
height: 30,
|
||||
background: "linear-gradient(135deg,#2E2A5E,#4338CA)",
|
||||
borderRadius: 7,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
}}
|
||||
>
|
||||
<span className="f" style={{ fontSize: 15, fontWeight: 700, color: "#FFFFFF" }}>
|
||||
V
|
||||
</span>
|
||||
</div>
|
||||
<span className="f" style={{ fontSize: 19, fontWeight: 700, color: "var(--ink)", letterSpacing: "-0.02em" }}>
|
||||
vibn
|
||||
</span>
|
||||
</Link>
|
||||
|
||||
<div className="nav-links">
|
||||
<Link href="/#how-it-works" style={{ fontSize: 14, color: "var(--muted)", textDecoration: "none" }}>
|
||||
How it works
|
||||
</Link>
|
||||
<Link href="/pricing" style={{ fontSize: 14, color: "var(--muted)", textDecoration: "none" }}>
|
||||
Pricing
|
||||
</Link>
|
||||
<Link href="/stories" style={{ fontSize: 14, color: "var(--muted)", textDecoration: "none" }}>
|
||||
Stories
|
||||
</Link>
|
||||
<span style={{ fontSize: 14, color: "var(--muted)" }}>Blog</span>
|
||||
</div>
|
||||
|
||||
<div className="nav-right-btns" style={{ display: "flex", alignItems: "center", gap: 12 }}>
|
||||
<Link href="/auth" style={{ fontSize: 14, color: "#6366F1", fontWeight: 600, textDecoration: "none" }}>
|
||||
Log in
|
||||
</Link>
|
||||
<Link href="/auth?new=1">
|
||||
<button type="button" className="btn-ink">
|
||||
Get started free
|
||||
</button>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className={`hamburger ${open ? "open" : ""}`}
|
||||
aria-label={open ? "Close menu" : "Open menu"}
|
||||
aria-expanded={open}
|
||||
onClick={toggle}
|
||||
>
|
||||
<span />
|
||||
<span />
|
||||
<span />
|
||||
</button>
|
||||
</nav>
|
||||
|
||||
<div className={`mobile-menu ${open ? "open" : ""}`}>
|
||||
<Link href="/#how-it-works" onClick={close}>
|
||||
How it works
|
||||
</Link>
|
||||
<Link href="/pricing" onClick={close}>
|
||||
Pricing
|
||||
</Link>
|
||||
<Link href="/stories" onClick={close}>
|
||||
Stories
|
||||
</Link>
|
||||
<Link href="#" onClick={(e) => { e.preventDefault(); close(); }}>
|
||||
Blog
|
||||
</Link>
|
||||
<Link href="/auth" style={{ color: "#6366F1", fontWeight: 600 }} onClick={close}>
|
||||
Log in
|
||||
</Link>
|
||||
<div className="mobile-menu-cta">
|
||||
<Link href="/auth?new=1" onClick={close} style={{ display: "block", width: "100%" }}>
|
||||
<button type="button" className="btn-ink-lg" style={{ width: "100%" }}>
|
||||
Get started free
|
||||
</button>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
export { JustineNav } from "./JustineNav";
|
||||
export { JustineFooter } from "./JustineFooter";
|
||||
export { JustineHomePage } from "./JustineHomePage";
|
||||
export { JustineAuthShell } from "./JustineAuthShell";
|
||||
File diff suppressed because it is too large
Load Diff
@@ -2163,8 +2163,8 @@ export function ChatPanel({
|
||||
);
|
||||
|
||||
const authHref = pathname
|
||||
? `/auth?callbackUrl=${encodeURIComponent(pathname)}`
|
||||
: "/auth";
|
||||
? `/signin?callbackUrl=${encodeURIComponent(pathname)}`
|
||||
: "/signin";
|
||||
|
||||
const structuralChatSignedOutColumn = (
|
||||
<div
|
||||
|
||||
Reference in New Issue
Block a user