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
38 changes: 38 additions & 0 deletions apps/portal/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# CleanStart partner portal (clickable mock)

A real Next.js app with seeded mock data and no backend, built so the CleanStart team can walk through the whole partner portal and review it screen by screen. Every name, number and email is made up. Nothing is sent anywhere.

## Run it

```bash
pnpm --filter @cleanstart/portal dev # http://localhost:3004
pnpm --filter @cleanstart/portal build
```

If a build fails with a `next/font/google` import-map error right after using `dev`, delete `apps/portal/.next` and build again.

## How to review

Open the app and use the **Walkthrough** button (bottom left). It lists the steps in order, jumps to the right screen as the right person, and highlights what to look at. Sign-in has two shortcut buttons (partner or CleanStart staff), and the walkthrough panel has links to switch role at any time.

The flows are live, so actions carry across screens: apply as a new company, approve the application as staff, register a deal as a partner, accept it as staff, then see the new status as the partner. Everything you do is also recorded in the Audit log.

State is kept in the browser's local storage so a reload does not lose your place. **Reset demo data** in the walkthrough panel restores the seeds.

## Hosting a review copy (Vercel)

1. Create a Vercel project from this repository with **Root Directory** set to `apps/portal`.
2. Add the environment variable `PORTAL_REVIEW_PASSWORD` (any shared password). With it set, every page asks for it (HTTP Basic, username ignored). Leave it unset for an open copy.
3. Search engines are blocked in three ways: `robots.txt`, an `X-Robots-Tag` header and page metadata.

## Where things live

- `src/components/screens/`: one file per screen, ported from the design boards.
- `src/data/`: seeded mock data, one persisted slice per area (`src/lib/slice.ts`).
- `src/components/shell/`: sidebar, top bar, search (Ctrl K), notifications and help, shared by every signed-in screen.
- `src/tour/`: the walkthrough steps. A screen takes part by carrying a `data-tour` attribute.
- `src/styles/portal.css`: the whole design, taken from the design boards.

## Not in this app

No real authentication, no HubSpot connection, no file storage and no email. Those belong to the real portal build.
22 changes: 22 additions & 0 deletions apps/portal/biome.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"$schema": "https://biomejs.dev/schemas/1.9.4/schema.json",
"extends": [
"../../biome.json"
],
"linter": {
"rules": {
"a11y": {
"noSvgWithoutTitle": "off",
"useKeyWithClickEvents": "off",
"useSemanticElements": "off",
"useFocusableInteractive": "off"
},
"suspicious": {
"noArrayIndexKey": "off"
},
"style": {
"noNonNullAssertion": "off"
}
}
}
}
14 changes: 14 additions & 0 deletions apps/portal/next.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import path from "node:path";
import type { NextConfig } from "next";

const nextConfig: NextConfig = {
// lets a production build run beside a live dev server without sharing .next
distDir: process.env.NEXT_DIST_DIR ?? ".next",
turbopack: { root: path.resolve(process.cwd(), "..", "..") },
poweredByHeader: false,
async headers() {
return [{ source: "/:path*", headers: [{ key: "X-Robots-Tag", value: "noindex, nofollow" }] }];
},
};

export default nextConfig;
27 changes: 27 additions & 0 deletions apps/portal/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
{
"name": "@cleanstart/portal",
"version": "0.0.1",
"description": "CleanStart partner portal: clickable mock with seeded data for team walkthroughs. No backend, no real data.",
"private": true,
"type": "module",
"scripts": {
"dev": "next dev --port 3004",
"build": "next build",
"start": "next start --port 3004",
"lint": "biome lint src",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"driver.js": "^1.3.6",
"next": "16.2.5",
"react": "19.2.4",
"react-dom": "19.2.4"
},
"devDependencies": {
"@cleanstart/config": "workspace:*",
"@types/node": "^22.10.0",
"@types/react": "^19.0.0",
"@types/react-dom": "^19.0.0",
"typescript": "^5.7.3"
}
}
15 changes: 15 additions & 0 deletions apps/portal/public/logo-mark.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 2 additions & 0 deletions apps/portal/public/robots.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
User-agent: *
Disallow: /
8 changes: 8 additions & 0 deletions apps/portal/src/app/(public)/accept-invite/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import type { Metadata } from "next";
import { AcceptInvite } from "@/components/screens/AcceptInvite";

export const metadata: Metadata = { title: "Set up your account" };

export default function Page(): React.JSX.Element {
return <AcceptInvite />;
}
8 changes: 8 additions & 0 deletions apps/portal/src/app/(public)/apply/done/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import type { Metadata } from "next";
import { ApplyDone } from "@/components/screens/ApplyDone";

export const metadata: Metadata = { title: "Application received" };

export default function Page(): React.JSX.Element {
return <ApplyDone />;
}
8 changes: 8 additions & 0 deletions apps/portal/src/app/(public)/apply/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import type { Metadata } from "next";
import { Apply } from "@/components/screens/Apply";

export const metadata: Metadata = { title: "Apply to become a partner" };

export default function Page(): React.JSX.Element {
return <Apply />;
}
3 changes: 3 additions & 0 deletions apps/portal/src/app/(public)/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default function PublicLayout({ children }: { children: React.ReactNode }): React.JSX.Element {
return <div className="public">{children}</div>;
}
8 changes: 8 additions & 0 deletions apps/portal/src/app/(public)/sign-in/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import type { Metadata } from "next";
import { SignIn } from "@/components/screens/SignIn";

export const metadata: Metadata = { title: "Sign in" };

export default function Page(): React.JSX.Element {
return <SignIn />;
}
8 changes: 8 additions & 0 deletions apps/portal/src/app/admin/approvals/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import type { Metadata } from "next";
import { AdminApprovals } from "@/components/screens/AdminApprovals";

export const metadata: Metadata = { title: "Deal approvals" };

export default function Page(): React.JSX.Element {
return <AdminApprovals />;
}
8 changes: 8 additions & 0 deletions apps/portal/src/app/admin/audit/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import type { Metadata } from "next";
import { AdminAudit } from "@/components/screens/AdminAudit";

export const metadata: Metadata = { title: "Audit log" };

export default function Page(): React.JSX.Element {
return <AdminAudit />;
}
8 changes: 8 additions & 0 deletions apps/portal/src/app/admin/documents/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import type { Metadata } from "next";
import { AdminDocuments } from "@/components/screens/AdminDocuments";

export const metadata: Metadata = { title: "Manage documents" };

export default function Page(): React.JSX.Element {
return <AdminDocuments />;
}
5 changes: 5 additions & 0 deletions apps/portal/src/app/admin/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { Shell } from "@/components/shell/shell";

export default function AdminLayout({ children }: { children: React.ReactNode }): React.JSX.Element {
return <Shell kind="admin">{children}</Shell>;
}
5 changes: 5 additions & 0 deletions apps/portal/src/app/admin/loading.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { LoadingSkeleton } from "@/components/screens/loading-skeleton";

export default function Loading(): React.JSX.Element {
return <LoadingSkeleton />;
}
6 changes: 6 additions & 0 deletions apps/portal/src/app/admin/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { redirect } from "next/navigation";
import { ROUTES } from "@/lib/routes";

export default function AdminIndex(): never {
redirect(ROUTES.admin.approvals);
}
13 changes: 13 additions & 0 deletions apps/portal/src/app/admin/partners/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import type { Metadata } from "next";
import { Suspense } from "react";
import { AdminPartners } from "@/components/screens/AdminPartners";

export const metadata: Metadata = { title: "Partners" };

export default function Page(): React.JSX.Element {
return (
<Suspense fallback={null}>
<AdminPartners />
</Suspense>
);
}
8 changes: 8 additions & 0 deletions apps/portal/src/app/admin/reports/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import type { Metadata } from "next";
import { AdminReports } from "@/components/screens/AdminReports";

export const metadata: Metadata = { title: "Reports" };

export default function Page(): React.JSX.Element {
return <AdminReports />;
}
8 changes: 8 additions & 0 deletions apps/portal/src/app/admin/team/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import type { Metadata } from "next";
import { AdminTeam } from "@/components/screens/AdminTeam";

export const metadata: Metadata = { title: "CleanStart team" };

export default function Page(): React.JSX.Element {
return <AdminTeam />;
}
37 changes: 37 additions & 0 deletions apps/portal/src/app/error.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
"use client";

import Link from "next/link";
import { useEffect } from "react";
import { ROUTES } from "@/lib/routes";

export default function ErrorPage({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}): React.JSX.Element {
useEffect(() => {
console.error(error);
}, [error]);

return (
<div className="public state-page">
<div className="state-card">
<span className="state-code">Something went wrong</span>
<h1 className="state-h">This screen could not load</h1>
<p className="state-p">
Try again. If it keeps happening, reset the demo data from the Walkthrough panel or start from sign in.
</p>
<div style={{ display: "flex", gap: 8 }}>
<button type="button" className="btn btn-p" onClick={reset}>
Try again
</button>
<Link href={ROUTES.signIn} className="btn btn-s">
Sign in
</Link>
</div>
</div>
</div>
);
}
15 changes: 15 additions & 0 deletions apps/portal/src/app/icon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
28 changes: 28 additions & 0 deletions apps/portal/src/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import type { Metadata, Viewport } from "next";
import { Manrope } from "next/font/google";
import { ToastProvider } from "@/components/ui/toast";
import { TourProvider } from "@/tour/tour-provider";
import "@/styles/portal.css";

const manrope = Manrope({ subsets: ["latin"], weight: ["400", "500", "600", "700"], variable: "--font-manrope", display: "swap" });

export const metadata: Metadata = {
title: { default: "CleanStart Partner Portal (mock)", template: "%s | CleanStart Partner Portal (mock)" },
description: "Clickable mock of the CleanStart partner portal with made-up data, for team review.",
robots: { index: false, follow: false },
};

export const viewport: Viewport = { width: "device-width", initialScale: 1 };

export default function RootLayout({ children }: { children: React.ReactNode }): React.JSX.Element {
return (
<html lang="en" className={manrope.variable}>
<body>
<ToastProvider>
{children}
<TourProvider />
</ToastProvider>
</body>
</html>
);
}
17 changes: 17 additions & 0 deletions apps/portal/src/app/not-found.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import Link from "next/link";
import { ROUTES } from "@/lib/routes";

export default function NotFound(): React.JSX.Element {
return (
<div className="public state-page">
<div className="state-card">
<span className="state-code">404</span>
<h1 className="state-h">This page does not exist</h1>
<p className="state-p">The link may be old, or the address may have a typo.</p>
<Link href={ROUTES.signIn} className="btn btn-p">
Back to sign in
</Link>
</div>
</div>
);
}
6 changes: 6 additions & 0 deletions apps/portal/src/app/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { redirect } from "next/navigation";
import { ROUTES } from "@/lib/routes";

export default function Home(): never {
redirect(ROUTES.signIn);
}
8 changes: 8 additions & 0 deletions apps/portal/src/app/partner/account/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import type { Metadata } from "next";
import { Account } from "@/components/screens/Account";

export const metadata: Metadata = { title: "Your account" };

export default function Page(): React.JSX.Element {
return <Account />;
}
8 changes: 8 additions & 0 deletions apps/portal/src/app/partner/company/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import type { Metadata } from "next";
import { Company } from "@/components/screens/Company";

export const metadata: Metadata = { title: "Company" };

export default function Page(): React.JSX.Element {
return <Company />;
}
9 changes: 9 additions & 0 deletions apps/portal/src/app/partner/deals/[id]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import type { Metadata } from "next";
import { DealDetail } from "@/components/screens/DealDetail";

export const metadata: Metadata = { title: "Deal detail" };

export default async function Page({ params }: { params: Promise<{ id: string }> }): Promise<React.JSX.Element> {
const { id } = await params;
return <DealDetail id={id} />;
}
8 changes: 8 additions & 0 deletions apps/portal/src/app/partner/deals/new/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import type { Metadata } from "next";
import { RegisterDeal } from "@/components/screens/RegisterDeal";

export const metadata: Metadata = { title: "Register a deal" };

export default function Page(): React.JSX.Element {
return <RegisterDeal />;
}
8 changes: 8 additions & 0 deletions apps/portal/src/app/partner/deals/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import type { Metadata } from "next";
import { Deals } from "@/components/screens/Deals";

export const metadata: Metadata = { title: "Deals" };

export default function Page(): React.JSX.Element {
return <Deals />;
}
Loading
Loading