feat: complete MVP foundation auth and dashboard

This commit is contained in:
2026-06-04 09:05:40 +02:00
parent 20615e12a1
commit df7a955736
32 changed files with 880 additions and 139 deletions

30
app/actions/auth.ts Normal file
View File

@@ -0,0 +1,30 @@
"use server";
import { cookies } from "next/headers";
import { redirect } from "next/navigation";
import {
createClearedMockSessionCookie,
createMockSessionCookie,
} from "@/lib/mock-auth";
export async function signInMock() {
const cookieStore = await cookies();
cookieStore.set(createMockSessionCookie());
redirect("/dashboard");
}
export async function signUpMock() {
const cookieStore = await cookies();
cookieStore.set(createMockSessionCookie());
redirect("/dashboard");
}
export async function signOutMock() {
const cookieStore = await cookies();
cookieStore.set(createClearedMockSessionCookie());
redirect("/");
}

View File

@@ -0,0 +1,10 @@
import { DashboardPlaceholderPage } from "@/components/dashboard-placeholder-page";
export default function AnalyticsPage() {
return (
<DashboardPlaceholderPage
description="Kampagnenmetriken und Rybbit-Daten folgen in TASK-17 und TASK-19."
title="Analytics"
/>
);
}

View File

@@ -0,0 +1,10 @@
import { DashboardPlaceholderPage } from "@/components/dashboard-placeholder-page";
export default function AuditsPage() {
return (
<DashboardPlaceholderPage
description="Audit-Review, Screenshots und oeffentliche Freigaben folgen in TASK-12 und TASK-13."
title="Audits"
/>
);
}

View File

@@ -0,0 +1,10 @@
import { DashboardPlaceholderPage } from "@/components/dashboard-placeholder-page";
export default function BlacklistPage() {
return (
<DashboardPlaceholderPage
description="Sperrlisten fuer Domains, E-Mails, Telefonnummern, Firmennamen und Place IDs folgen nach den Datenmodellen."
title="Blacklist"
/>
);
}

View File

@@ -0,0 +1,10 @@
import { DashboardPlaceholderPage } from "@/components/dashboard-placeholder-page";
export default function CampaignsPage() {
return (
<DashboardPlaceholderPage
description="Kampagnen-Konfiguration, PLZ, Radius, Limits und Laufplanung folgen in TASK-5."
title="Campaigns"
/>
);
}

25
app/dashboard/layout.tsx Normal file
View File

@@ -0,0 +1,25 @@
import { redirect } from "next/navigation";
import { DashboardSidebar } from "@/components/dashboard-sidebar";
import { getCurrentMockSession } from "@/lib/mock-session";
import { getDashboardRedirectPath } from "@/lib/route-guards";
export default async function DashboardLayout({
children,
}: {
children: React.ReactNode;
}) {
const session = await getCurrentMockSession();
const redirectPath = getDashboardRedirectPath(session);
if (redirectPath || !session) {
redirect(redirectPath ?? "/");
}
return (
<div className="min-h-dvh bg-background md:flex">
<DashboardSidebar session={session} />
<div className="min-w-0 flex-1">{children}</div>
</div>
);
}

View File

@@ -0,0 +1,10 @@
import { DashboardPlaceholderPage } from "@/components/dashboard-placeholder-page";
export default function LeadsPage() {
return (
<DashboardPlaceholderPage
description="Lead-Qualifikation, Dubletten und fehlende Kontaktdaten folgen in TASK-7."
title="Leads"
/>
);
}

View File

@@ -0,0 +1,10 @@
import { DashboardPlaceholderPage } from "@/components/dashboard-placeholder-page";
export default function OutreachPage() {
return (
<DashboardPlaceholderPage
description="E-Mail-Entwuerfe, Telefon-Skripte und manuelle Versandfreigaben folgen in TASK-13 und TASK-14."
title="Outreach"
/>
);
}

View File

@@ -1,63 +1,130 @@
import { FileSearch, MailCheck, MapPinned, ShieldCheck } from "lucide-react";
const pipelineSteps = [
{
title: "Kampagnen",
description: "Kategorie, PLZ, Radius und Lauf-Limits vorbereiten.",
icon: MapPinned,
},
{
title: "Lead-Audit",
description: "Website-Potenzial, Screenshots und Quellen buendeln.",
icon: FileSearch,
},
{
title: "Freigabe",
description: "Audit-Seite, E-Mail und Telefon-Skript manuell pruefen.",
icon: ShieldCheck,
},
{
title: "Outreach",
description: "Freigegebene Kontakte senden und Antworten nachhalten.",
icon: MailCheck,
},
];
import {
dashboardKpis,
pipelineHealth,
pipelineStages,
reviewQueue,
} from "@/lib/dashboard-model";
export default function DashboardPage() {
return (
<main className="min-h-dvh bg-background px-6 py-8">
<div className="mx-auto flex w-full max-w-6xl flex-col gap-8">
<header className="flex flex-col gap-2 border-b pb-6">
<main className="px-4 py-5 sm:px-6 lg:px-8">
<div className="mx-auto flex w-full max-w-7xl flex-col gap-6">
<header className="flex flex-col gap-3 border-b pb-5 lg:flex-row lg:items-end lg:justify-between">
<div>
<p className="text-sm font-medium text-muted-foreground">
Interner Arbeitsbereich
</p>
<h1 className="mt-2 text-3xl font-semibold tracking-normal">
Pipeline-Uebersicht
</h1>
<p className="mt-2 max-w-2xl text-sm leading-6 text-muted-foreground">
Recherche, Audit-Freigabe und Outreach bleiben eng gekoppelt:
wenige gute Leads, manuelle Pruefung, kein automatischer Versand.
</p>
</div>
<p className="text-sm font-medium text-muted-foreground">
Interner Arbeitsbereich
</p>
<h1 className="text-3xl font-semibold tracking-normal">
Dashboard-Platzhalter
</h1>
<p className="max-w-2xl text-sm leading-6 text-muted-foreground">
Hier entsteht der scanbare Funnel fuer Recherche, Audit, Review,
Versand und Follow-up.
Mock-Session aktiv
</p>
</header>
<section className="grid gap-3 md:grid-cols-4">
{pipelineSteps.map((step) => {
const Icon = step.icon;
<section className="grid gap-3 sm:grid-cols-2 xl:grid-cols-4">
{dashboardKpis.map((kpi) => (
<article
className="rounded-lg border bg-card p-4 text-card-foreground"
key={kpi.label}
>
<p className="text-sm text-muted-foreground">{kpi.label}</p>
<p className="mt-3 text-3xl font-semibold tracking-normal">
{kpi.value}
</p>
<p className="mt-2 text-sm leading-5 text-muted-foreground">
{kpi.detail}
</p>
</article>
))}
</section>
<section className="grid gap-3 xl:grid-cols-4">
{pipelineStages.map((stage) => {
const Icon = stage.icon;
return (
<article
className="rounded-lg border bg-card p-4 text-card-foreground"
key={step.title}
key={stage.title}
>
<Icon className="mb-4 size-5 text-muted-foreground" />
<h2 className="text-sm font-medium">{step.title}</h2>
<div className="flex items-center justify-between gap-4">
<Icon className="size-5 text-muted-foreground" />
<span className="text-2xl font-semibold">{stage.count}</span>
</div>
<h2 className="mt-4 text-sm font-medium">{stage.title}</h2>
<p className="mt-2 text-sm leading-6 text-muted-foreground">
{step.description}
{stage.description}
</p>
<p className="mt-4 rounded-md bg-muted px-2 py-1 text-xs text-muted-foreground">
{stage.meta}
</p>
</article>
);
})}
</section>
<section className="grid gap-3 lg:grid-cols-[1.45fr_0.55fr]">
<div className="rounded-lg border bg-card text-card-foreground">
<div className="border-b p-4">
<h2 className="text-base font-semibold tracking-normal">
Naechste Review-Schritte
</h2>
<p className="mt-1 text-sm leading-6 text-muted-foreground">
Alles bleibt an manuelle Freigabe gekoppelt.
</p>
</div>
<div className="divide-y">
{reviewQueue.map((item) => (
<article
className="grid gap-2 p-4 sm:grid-cols-[1fr_auto]"
key={`${item.title}-${item.company}`}
>
<div>
<h3 className="text-sm font-medium">{item.title}</h3>
<p className="mt-1 text-sm text-muted-foreground">
{item.company}
</p>
</div>
<p className="max-w-sm text-sm leading-6 text-muted-foreground sm:text-right">
{item.detail}
</p>
</article>
))}
</div>
</div>
<div className="rounded-lg border bg-card p-4 text-card-foreground">
<h2 className="text-base font-semibold tracking-normal">
Betriebsmodus
</h2>
<div className="mt-4 grid gap-3">
{pipelineHealth.map((item) => {
const Icon = item.icon;
return (
<div
className="flex items-center justify-between gap-3 rounded-lg border bg-background p-3"
key={item.label}
>
<span className="inline-flex items-center gap-2 text-sm font-medium">
<Icon className="size-4 text-muted-foreground" />
{item.label}
</span>
<span className="text-right text-sm text-muted-foreground">
{item.value}
</span>
</div>
);
})}
</div>
</div>
</section>
</div>
</main>
);

View File

@@ -0,0 +1,10 @@
import { DashboardPlaceholderPage } from "@/components/dashboard-placeholder-page";
export default function SettingsPage() {
return (
<DashboardPlaceholderPage
description="Provider-Status, Secrets-Hinweise und Workspace-Einstellungen folgen mit den Integrationen."
title="Settings"
/>
);
}

View File

@@ -1,19 +1,14 @@
import { LockKeyhole } from "lucide-react";
import { redirect } from "next/navigation";
export default function LoginPage() {
return (
<main className="flex min-h-dvh items-center justify-center bg-background px-6 py-12">
<section className="w-full max-w-md rounded-lg border bg-card p-6 text-card-foreground">
<LockKeyhole className="mb-5 size-6 text-muted-foreground" />
<h1 className="text-2xl font-semibold tracking-normal">
Login-Platzhalter
</h1>
<p className="mt-3 text-sm leading-6 text-muted-foreground">
Better Auth wird in einem spaeteren Task angebunden. Bis dahin bleibt
diese Route als definierter Einstiegspunkt fuer den Admin-Login
bestehen.
</p>
</section>
</main>
);
import { AuthEntry } from "@/components/auth-entry";
import { getCurrentMockSession } from "@/lib/mock-session";
export default async function LoginPage() {
const session = await getCurrentMockSession();
if (session) {
redirect("/dashboard");
}
return <AuthEntry />;
}

View File

@@ -1,81 +1,14 @@
import Link from "next/link";
import { ArrowRight, FileText, LayoutDashboard, LockKeyhole } from "lucide-react";
import { redirect } from "next/navigation";
import { Button } from "@/components/ui/button";
import { AuthEntry } from "@/components/auth-entry";
import { getCurrentMockSession } from "@/lib/mock-session";
export default function Home() {
return (
<main className="flex flex-1 bg-background">
<section className="mx-auto flex min-h-dvh w-full max-w-5xl flex-col justify-center gap-10 px-6 py-12">
<div className="max-w-3xl space-y-5">
<p className="text-sm font-medium text-muted-foreground">
WebDev Pipeline MVP
</p>
<h1 className="text-4xl font-semibold tracking-normal text-foreground sm:text-5xl">
Lokale Webdesign-Leads recherchieren, auditieren und respektvoll
kontaktieren.
</h1>
<p className="max-w-2xl text-base leading-7 text-muted-foreground sm:text-lg">
Diese Foundation setzt die ersten App-Routen fuer Dashboard,
Anmeldung und oeffentliche Audit-Seiten auf. Die Integrationen
folgen in den naechsten Backlog-Tasks.
</p>
</div>
export default async function Home() {
const session = await getCurrentMockSession();
<div className="grid gap-3 sm:grid-cols-3">
<Button asChild size="lg" className="justify-between">
<Link href="/dashboard">
<span className="inline-flex items-center gap-2">
<LayoutDashboard />
Dashboard
</span>
<ArrowRight />
</Link>
</Button>
<Button asChild size="lg" variant="outline" className="justify-between">
<Link href="/login">
<span className="inline-flex items-center gap-2">
<LockKeyhole />
Login
</span>
<ArrowRight />
</Link>
</Button>
<Button asChild size="lg" variant="outline" className="justify-between">
<Link href="/audit/example">
<span className="inline-flex items-center gap-2">
<FileText />
Audit
</span>
<ArrowRight />
</Link>
</Button>
</div>
if (session) {
redirect("/dashboard");
}
<dl className="grid gap-4 border-t pt-8 sm:grid-cols-3">
<div>
<dt className="text-sm font-medium text-foreground">Recherche</dt>
<dd className="mt-2 text-sm leading-6 text-muted-foreground">
Kampagnen, Places-Quellen und Lead-Qualitaet werden spaeter im
Dashboard gebuendelt.
</dd>
</div>
<div>
<dt className="text-sm font-medium text-foreground">Audit</dt>
<dd className="mt-2 text-sm leading-6 text-muted-foreground">
Oeffentliche Audit-Seiten starten als sichere Platzhalter ohne
freigegebene Inhalte.
</dd>
</div>
<div>
<dt className="text-sm font-medium text-foreground">Outreach</dt>
<dd className="mt-2 text-sm leading-6 text-muted-foreground">
Versand bleibt im MVP an manuelle Pruefung und Freigabe
gekoppelt.
</dd>
</div>
</dl>
</section>
</main>
);
return <AuthEntry />;
}