From 58747992b690f93582a847818625d56565acc3c0 Mon Sep 17 00:00:00 2001 From: Gaurav Jadhav Date: Wed, 11 Nov 2026 16:19:59 +0530 Subject: [PATCH 1/6] feat(portal): clickable partner portal mock with seeded data and guided walkthrough New apps/portal (Next 16, port 3004) so the CleanStart team can review the whole partner portal screen by screen. No backend and no real data. - 19 screens ported from the design boards: sign in, apply, accept invite, partner overview and first login, deals, deal detail, register deal, documents, team, company, account, and the staff screens for approvals, partners, documents, reports, team and audit log. - Seeded, persisted mock data in src/data. Flows are live across roles: apply, approve, register a deal, accept it, and every change lands in the audit log. - Driver.js walkthrough of 20 steps that moves between screens and roles, with a reset for the demo data. - App shell with fixed sidebar, sticky top bar, search palette, notifications, help and a profile menu; per-page titles, loading, error and 404 states. - Optional shared password (PORTAL_REVIEW_PASSWORD) and noindex for a hosted copy. --- apps/portal/README.md | 38 + apps/portal/biome.json | 22 + apps/portal/next.config.ts | 14 + apps/portal/package.json | 27 + apps/portal/public/logo-mark.svg | 15 + apps/portal/public/robots.txt | 2 + .../src/app/(public)/accept-invite/page.tsx | 8 + .../src/app/(public)/apply/done/page.tsx | 8 + apps/portal/src/app/(public)/apply/page.tsx | 8 + apps/portal/src/app/(public)/layout.tsx | 3 + apps/portal/src/app/(public)/sign-in/page.tsx | 8 + apps/portal/src/app/admin/approvals/page.tsx | 8 + apps/portal/src/app/admin/audit/page.tsx | 8 + apps/portal/src/app/admin/documents/page.tsx | 8 + apps/portal/src/app/admin/layout.tsx | 5 + apps/portal/src/app/admin/loading.tsx | 5 + apps/portal/src/app/admin/page.tsx | 6 + apps/portal/src/app/admin/partners/page.tsx | 13 + apps/portal/src/app/admin/reports/page.tsx | 8 + apps/portal/src/app/admin/team/page.tsx | 8 + apps/portal/src/app/error.tsx | 37 + apps/portal/src/app/icon.svg | 15 + apps/portal/src/app/layout.tsx | 28 + apps/portal/src/app/not-found.tsx | 17 + apps/portal/src/app/page.tsx | 6 + apps/portal/src/app/partner/account/page.tsx | 8 + apps/portal/src/app/partner/company/page.tsx | 8 + .../src/app/partner/deals/[id]/page.tsx | 9 + .../portal/src/app/partner/deals/new/page.tsx | 8 + apps/portal/src/app/partner/deals/page.tsx | 8 + .../portal/src/app/partner/documents/page.tsx | 8 + apps/portal/src/app/partner/layout.tsx | 5 + apps/portal/src/app/partner/loading.tsx | 5 + apps/portal/src/app/partner/overview/page.tsx | 8 + apps/portal/src/app/partner/page.tsx | 6 + apps/portal/src/app/partner/team/page.tsx | 8 + apps/portal/src/app/partner/welcome/page.tsx | 8 + .../components/deals/ManagerMessageDrawer.tsx | 88 ++ .../components/deals/RejectedDealDrawer.tsx | 130 +++ apps/portal/src/components/deals/csv.ts | 16 + apps/portal/src/components/deals/dates.ts | 41 + apps/portal/src/components/deals/derive.ts | 142 ++++ apps/portal/src/components/deals/parts.tsx | 56 ++ .../documents/DocumentEditDrawer.tsx | 108 +++ .../components/documents/DocumentPreview.tsx | 84 ++ .../documents/RequestMaterialDialog.tsx | 107 +++ .../documents/UploadDocumentForm.tsx | 245 ++++++ .../src/components/documents/doc-style.ts | 20 + apps/portal/src/components/icon-paths.ts | 36 + apps/portal/src/components/icon.tsx | 20 + .../partners/add-partner-drawer.tsx | 140 ++++ .../partners/applications-table.tsx | 235 ++++++ .../portal/src/components/partners/badges.tsx | 53 ++ apps/portal/src/components/partners/csv.ts | 18 + .../components/partners/message-drawer.tsx | 97 +++ .../components/partners/partner-detail.tsx | 196 +++++ .../components/partners/partner-dialogs.tsx | 71 ++ .../components/partners/partners-table.tsx | 120 +++ .../src/components/partners/public-frame.tsx | 201 +++++ apps/portal/src/components/partners/seg.tsx | 36 + .../src/components/people/AccessRequests.tsx | 100 +++ .../src/components/people/AuditDrawer.tsx | 67 ++ .../src/components/people/InviteCard.tsx | 101 +++ .../src/components/people/PasswordCard.tsx | 127 +++ .../src/components/people/RowAction.tsx | 29 + apps/portal/src/components/people/Seg.tsx | 35 + .../components/people/StaffInviteDrawer.tsx | 140 ++++ .../src/components/people/TwoStepCard.tsx | 254 ++++++ .../src/components/reports/FollowUpList.tsx | 36 + .../components/reports/ProductBreakdown.tsx | 59 ++ .../src/components/reports/download-csv.ts | 10 + .../src/components/reports/partner-ui.ts | 17 + .../src/components/screens/AcceptInvite.tsx | 231 ++++++ .../portal/src/components/screens/Account.tsx | 171 ++++ .../src/components/screens/AdminApprovals.tsx | 501 ++++++++++++ .../src/components/screens/AdminAudit.tsx | 181 +++++ .../src/components/screens/AdminDocuments.tsx | 120 +++ .../src/components/screens/AdminPartners.tsx | 251 ++++++ .../src/components/screens/AdminReports.tsx | 179 ++++ .../src/components/screens/AdminTeam.tsx | 243 ++++++ apps/portal/src/components/screens/Apply.tsx | 307 +++++++ .../src/components/screens/ApplyDone.tsx | 126 +++ .../portal/src/components/screens/Company.tsx | 266 ++++++ .../src/components/screens/DealDetail.tsx | 513 ++++++++++++ apps/portal/src/components/screens/Deals.tsx | 233 ++++++ .../src/components/screens/Documents.tsx | 220 +++++ .../components/screens/PartnerOverview.tsx | 268 ++++++ .../src/components/screens/PartnerWelcome.tsx | 248 ++++++ .../src/components/screens/RegisterDeal.tsx | 481 +++++++++++ apps/portal/src/components/screens/SignIn.tsx | 473 +++++++++++ apps/portal/src/components/screens/Team.tsx | 253 ++++++ .../components/screens/loading-skeleton.tsx | 25 + apps/portal/src/components/shell/personas.ts | 39 + apps/portal/src/components/shell/shell.tsx | 641 +++++++++++++++ apps/portal/src/components/ui/badge.tsx | 86 ++ apps/portal/src/components/ui/dropdown.tsx | 88 ++ apps/portal/src/components/ui/overlay.tsx | 159 ++++ .../src/components/ui/password-field.tsx | 99 +++ apps/portal/src/components/ui/toast.tsx | 35 + apps/portal/src/data/audit.ts | 140 ++++ apps/portal/src/data/deals.ts | 732 +++++++++++++++++ apps/portal/src/data/documents.ts | 373 +++++++++ apps/portal/src/data/partners.ts | 767 ++++++++++++++++++ apps/portal/src/data/reports.ts | 158 ++++ apps/portal/src/data/team.ts | 113 +++ apps/portal/src/lib/demo-date.ts | 3 + apps/portal/src/lib/routes.ts | 55 ++ apps/portal/src/lib/slice.ts | 90 ++ apps/portal/src/proxy.ts | 25 + apps/portal/src/styles/portal.css | 425 ++++++++++ apps/portal/src/tour/steps.ts | 240 ++++++ apps/portal/src/tour/tour-provider.tsx | 185 +++++ apps/portal/tsconfig.json | 10 + pnpm-lock.yaml | 68 ++ 114 files changed, 13470 insertions(+) create mode 100644 apps/portal/README.md create mode 100644 apps/portal/biome.json create mode 100644 apps/portal/next.config.ts create mode 100644 apps/portal/package.json create mode 100644 apps/portal/public/logo-mark.svg create mode 100644 apps/portal/public/robots.txt create mode 100644 apps/portal/src/app/(public)/accept-invite/page.tsx create mode 100644 apps/portal/src/app/(public)/apply/done/page.tsx create mode 100644 apps/portal/src/app/(public)/apply/page.tsx create mode 100644 apps/portal/src/app/(public)/layout.tsx create mode 100644 apps/portal/src/app/(public)/sign-in/page.tsx create mode 100644 apps/portal/src/app/admin/approvals/page.tsx create mode 100644 apps/portal/src/app/admin/audit/page.tsx create mode 100644 apps/portal/src/app/admin/documents/page.tsx create mode 100644 apps/portal/src/app/admin/layout.tsx create mode 100644 apps/portal/src/app/admin/loading.tsx create mode 100644 apps/portal/src/app/admin/page.tsx create mode 100644 apps/portal/src/app/admin/partners/page.tsx create mode 100644 apps/portal/src/app/admin/reports/page.tsx create mode 100644 apps/portal/src/app/admin/team/page.tsx create mode 100644 apps/portal/src/app/error.tsx create mode 100644 apps/portal/src/app/icon.svg create mode 100644 apps/portal/src/app/layout.tsx create mode 100644 apps/portal/src/app/not-found.tsx create mode 100644 apps/portal/src/app/page.tsx create mode 100644 apps/portal/src/app/partner/account/page.tsx create mode 100644 apps/portal/src/app/partner/company/page.tsx create mode 100644 apps/portal/src/app/partner/deals/[id]/page.tsx create mode 100644 apps/portal/src/app/partner/deals/new/page.tsx create mode 100644 apps/portal/src/app/partner/deals/page.tsx create mode 100644 apps/portal/src/app/partner/documents/page.tsx create mode 100644 apps/portal/src/app/partner/layout.tsx create mode 100644 apps/portal/src/app/partner/loading.tsx create mode 100644 apps/portal/src/app/partner/overview/page.tsx create mode 100644 apps/portal/src/app/partner/page.tsx create mode 100644 apps/portal/src/app/partner/team/page.tsx create mode 100644 apps/portal/src/app/partner/welcome/page.tsx create mode 100644 apps/portal/src/components/deals/ManagerMessageDrawer.tsx create mode 100644 apps/portal/src/components/deals/RejectedDealDrawer.tsx create mode 100644 apps/portal/src/components/deals/csv.ts create mode 100644 apps/portal/src/components/deals/dates.ts create mode 100644 apps/portal/src/components/deals/derive.ts create mode 100644 apps/portal/src/components/deals/parts.tsx create mode 100644 apps/portal/src/components/documents/DocumentEditDrawer.tsx create mode 100644 apps/portal/src/components/documents/DocumentPreview.tsx create mode 100644 apps/portal/src/components/documents/RequestMaterialDialog.tsx create mode 100644 apps/portal/src/components/documents/UploadDocumentForm.tsx create mode 100644 apps/portal/src/components/documents/doc-style.ts create mode 100644 apps/portal/src/components/icon-paths.ts create mode 100644 apps/portal/src/components/icon.tsx create mode 100644 apps/portal/src/components/partners/add-partner-drawer.tsx create mode 100644 apps/portal/src/components/partners/applications-table.tsx create mode 100644 apps/portal/src/components/partners/badges.tsx create mode 100644 apps/portal/src/components/partners/csv.ts create mode 100644 apps/portal/src/components/partners/message-drawer.tsx create mode 100644 apps/portal/src/components/partners/partner-detail.tsx create mode 100644 apps/portal/src/components/partners/partner-dialogs.tsx create mode 100644 apps/portal/src/components/partners/partners-table.tsx create mode 100644 apps/portal/src/components/partners/public-frame.tsx create mode 100644 apps/portal/src/components/partners/seg.tsx create mode 100644 apps/portal/src/components/people/AccessRequests.tsx create mode 100644 apps/portal/src/components/people/AuditDrawer.tsx create mode 100644 apps/portal/src/components/people/InviteCard.tsx create mode 100644 apps/portal/src/components/people/PasswordCard.tsx create mode 100644 apps/portal/src/components/people/RowAction.tsx create mode 100644 apps/portal/src/components/people/Seg.tsx create mode 100644 apps/portal/src/components/people/StaffInviteDrawer.tsx create mode 100644 apps/portal/src/components/people/TwoStepCard.tsx create mode 100644 apps/portal/src/components/reports/FollowUpList.tsx create mode 100644 apps/portal/src/components/reports/ProductBreakdown.tsx create mode 100644 apps/portal/src/components/reports/download-csv.ts create mode 100644 apps/portal/src/components/reports/partner-ui.ts create mode 100644 apps/portal/src/components/screens/AcceptInvite.tsx create mode 100644 apps/portal/src/components/screens/Account.tsx create mode 100644 apps/portal/src/components/screens/AdminApprovals.tsx create mode 100644 apps/portal/src/components/screens/AdminAudit.tsx create mode 100644 apps/portal/src/components/screens/AdminDocuments.tsx create mode 100644 apps/portal/src/components/screens/AdminPartners.tsx create mode 100644 apps/portal/src/components/screens/AdminReports.tsx create mode 100644 apps/portal/src/components/screens/AdminTeam.tsx create mode 100644 apps/portal/src/components/screens/Apply.tsx create mode 100644 apps/portal/src/components/screens/ApplyDone.tsx create mode 100644 apps/portal/src/components/screens/Company.tsx create mode 100644 apps/portal/src/components/screens/DealDetail.tsx create mode 100644 apps/portal/src/components/screens/Deals.tsx create mode 100644 apps/portal/src/components/screens/Documents.tsx create mode 100644 apps/portal/src/components/screens/PartnerOverview.tsx create mode 100644 apps/portal/src/components/screens/PartnerWelcome.tsx create mode 100644 apps/portal/src/components/screens/RegisterDeal.tsx create mode 100644 apps/portal/src/components/screens/SignIn.tsx create mode 100644 apps/portal/src/components/screens/Team.tsx create mode 100644 apps/portal/src/components/screens/loading-skeleton.tsx create mode 100644 apps/portal/src/components/shell/personas.ts create mode 100644 apps/portal/src/components/shell/shell.tsx create mode 100644 apps/portal/src/components/ui/badge.tsx create mode 100644 apps/portal/src/components/ui/dropdown.tsx create mode 100644 apps/portal/src/components/ui/overlay.tsx create mode 100644 apps/portal/src/components/ui/password-field.tsx create mode 100644 apps/portal/src/components/ui/toast.tsx create mode 100644 apps/portal/src/data/audit.ts create mode 100644 apps/portal/src/data/deals.ts create mode 100644 apps/portal/src/data/documents.ts create mode 100644 apps/portal/src/data/partners.ts create mode 100644 apps/portal/src/data/reports.ts create mode 100644 apps/portal/src/data/team.ts create mode 100644 apps/portal/src/lib/demo-date.ts create mode 100644 apps/portal/src/lib/routes.ts create mode 100644 apps/portal/src/lib/slice.ts create mode 100644 apps/portal/src/proxy.ts create mode 100644 apps/portal/src/styles/portal.css create mode 100644 apps/portal/src/tour/steps.ts create mode 100644 apps/portal/src/tour/tour-provider.tsx create mode 100644 apps/portal/tsconfig.json diff --git a/apps/portal/README.md b/apps/portal/README.md new file mode 100644 index 000000000..d1399eb23 --- /dev/null +++ b/apps/portal/README.md @@ -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. diff --git a/apps/portal/biome.json b/apps/portal/biome.json new file mode 100644 index 000000000..5409aaba5 --- /dev/null +++ b/apps/portal/biome.json @@ -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" + } + } + } +} diff --git a/apps/portal/next.config.ts b/apps/portal/next.config.ts new file mode 100644 index 000000000..1b744eeeb --- /dev/null +++ b/apps/portal/next.config.ts @@ -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; diff --git a/apps/portal/package.json b/apps/portal/package.json new file mode 100644 index 000000000..34feab15f --- /dev/null +++ b/apps/portal/package.json @@ -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" + } +} diff --git a/apps/portal/public/logo-mark.svg b/apps/portal/public/logo-mark.svg new file mode 100644 index 000000000..a1f43ae73 --- /dev/null +++ b/apps/portal/public/logo-mark.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/apps/portal/public/robots.txt b/apps/portal/public/robots.txt new file mode 100644 index 000000000..1f53798bb --- /dev/null +++ b/apps/portal/public/robots.txt @@ -0,0 +1,2 @@ +User-agent: * +Disallow: / diff --git a/apps/portal/src/app/(public)/accept-invite/page.tsx b/apps/portal/src/app/(public)/accept-invite/page.tsx new file mode 100644 index 000000000..1a34dfe0f --- /dev/null +++ b/apps/portal/src/app/(public)/accept-invite/page.tsx @@ -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 ; +} diff --git a/apps/portal/src/app/(public)/apply/done/page.tsx b/apps/portal/src/app/(public)/apply/done/page.tsx new file mode 100644 index 000000000..50c1bedde --- /dev/null +++ b/apps/portal/src/app/(public)/apply/done/page.tsx @@ -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 ; +} diff --git a/apps/portal/src/app/(public)/apply/page.tsx b/apps/portal/src/app/(public)/apply/page.tsx new file mode 100644 index 000000000..e99fde061 --- /dev/null +++ b/apps/portal/src/app/(public)/apply/page.tsx @@ -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 ; +} diff --git a/apps/portal/src/app/(public)/layout.tsx b/apps/portal/src/app/(public)/layout.tsx new file mode 100644 index 000000000..bb74e47df --- /dev/null +++ b/apps/portal/src/app/(public)/layout.tsx @@ -0,0 +1,3 @@ +export default function PublicLayout({ children }: { children: React.ReactNode }): React.JSX.Element { + return
{children}
; +} diff --git a/apps/portal/src/app/(public)/sign-in/page.tsx b/apps/portal/src/app/(public)/sign-in/page.tsx new file mode 100644 index 000000000..403b49840 --- /dev/null +++ b/apps/portal/src/app/(public)/sign-in/page.tsx @@ -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 ; +} diff --git a/apps/portal/src/app/admin/approvals/page.tsx b/apps/portal/src/app/admin/approvals/page.tsx new file mode 100644 index 000000000..54f05945f --- /dev/null +++ b/apps/portal/src/app/admin/approvals/page.tsx @@ -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 ; +} diff --git a/apps/portal/src/app/admin/audit/page.tsx b/apps/portal/src/app/admin/audit/page.tsx new file mode 100644 index 000000000..1fd6902ef --- /dev/null +++ b/apps/portal/src/app/admin/audit/page.tsx @@ -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 ; +} diff --git a/apps/portal/src/app/admin/documents/page.tsx b/apps/portal/src/app/admin/documents/page.tsx new file mode 100644 index 000000000..a9678d027 --- /dev/null +++ b/apps/portal/src/app/admin/documents/page.tsx @@ -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 ; +} diff --git a/apps/portal/src/app/admin/layout.tsx b/apps/portal/src/app/admin/layout.tsx new file mode 100644 index 000000000..328e9eae6 --- /dev/null +++ b/apps/portal/src/app/admin/layout.tsx @@ -0,0 +1,5 @@ +import { Shell } from "@/components/shell/shell"; + +export default function AdminLayout({ children }: { children: React.ReactNode }): React.JSX.Element { + return {children}; +} diff --git a/apps/portal/src/app/admin/loading.tsx b/apps/portal/src/app/admin/loading.tsx new file mode 100644 index 000000000..3f062b08d --- /dev/null +++ b/apps/portal/src/app/admin/loading.tsx @@ -0,0 +1,5 @@ +import { LoadingSkeleton } from "@/components/screens/loading-skeleton"; + +export default function Loading(): React.JSX.Element { + return ; +} diff --git a/apps/portal/src/app/admin/page.tsx b/apps/portal/src/app/admin/page.tsx new file mode 100644 index 000000000..99295e693 --- /dev/null +++ b/apps/portal/src/app/admin/page.tsx @@ -0,0 +1,6 @@ +import { redirect } from "next/navigation"; +import { ROUTES } from "@/lib/routes"; + +export default function AdminIndex(): never { + redirect(ROUTES.admin.approvals); +} diff --git a/apps/portal/src/app/admin/partners/page.tsx b/apps/portal/src/app/admin/partners/page.tsx new file mode 100644 index 000000000..ec1f66a5f --- /dev/null +++ b/apps/portal/src/app/admin/partners/page.tsx @@ -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 ( + + + + ); +} diff --git a/apps/portal/src/app/admin/reports/page.tsx b/apps/portal/src/app/admin/reports/page.tsx new file mode 100644 index 000000000..507520dcf --- /dev/null +++ b/apps/portal/src/app/admin/reports/page.tsx @@ -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 ; +} diff --git a/apps/portal/src/app/admin/team/page.tsx b/apps/portal/src/app/admin/team/page.tsx new file mode 100644 index 000000000..54b526a01 --- /dev/null +++ b/apps/portal/src/app/admin/team/page.tsx @@ -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 ; +} diff --git a/apps/portal/src/app/error.tsx b/apps/portal/src/app/error.tsx new file mode 100644 index 000000000..84dc2ff79 --- /dev/null +++ b/apps/portal/src/app/error.tsx @@ -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 ( +
+
+ Something went wrong +

This screen could not load

+

+ Try again. If it keeps happening, reset the demo data from the Walkthrough panel or start from sign in. +

+
+ + + Sign in + +
+
+
+ ); +} diff --git a/apps/portal/src/app/icon.svg b/apps/portal/src/app/icon.svg new file mode 100644 index 000000000..a1f43ae73 --- /dev/null +++ b/apps/portal/src/app/icon.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/apps/portal/src/app/layout.tsx b/apps/portal/src/app/layout.tsx new file mode 100644 index 000000000..103384d1c --- /dev/null +++ b/apps/portal/src/app/layout.tsx @@ -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 ( + + + + {children} + + + + + ); +} diff --git a/apps/portal/src/app/not-found.tsx b/apps/portal/src/app/not-found.tsx new file mode 100644 index 000000000..fb9414313 --- /dev/null +++ b/apps/portal/src/app/not-found.tsx @@ -0,0 +1,17 @@ +import Link from "next/link"; +import { ROUTES } from "@/lib/routes"; + +export default function NotFound(): React.JSX.Element { + return ( +
+
+ 404 +

This page does not exist

+

The link may be old, or the address may have a typo.

+ + Back to sign in + +
+
+ ); +} diff --git a/apps/portal/src/app/page.tsx b/apps/portal/src/app/page.tsx new file mode 100644 index 000000000..f9f8bdc95 --- /dev/null +++ b/apps/portal/src/app/page.tsx @@ -0,0 +1,6 @@ +import { redirect } from "next/navigation"; +import { ROUTES } from "@/lib/routes"; + +export default function Home(): never { + redirect(ROUTES.signIn); +} diff --git a/apps/portal/src/app/partner/account/page.tsx b/apps/portal/src/app/partner/account/page.tsx new file mode 100644 index 000000000..763349691 --- /dev/null +++ b/apps/portal/src/app/partner/account/page.tsx @@ -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 ; +} diff --git a/apps/portal/src/app/partner/company/page.tsx b/apps/portal/src/app/partner/company/page.tsx new file mode 100644 index 000000000..181a78f31 --- /dev/null +++ b/apps/portal/src/app/partner/company/page.tsx @@ -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 ; +} diff --git a/apps/portal/src/app/partner/deals/[id]/page.tsx b/apps/portal/src/app/partner/deals/[id]/page.tsx new file mode 100644 index 000000000..722950cba --- /dev/null +++ b/apps/portal/src/app/partner/deals/[id]/page.tsx @@ -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 { + const { id } = await params; + return ; +} diff --git a/apps/portal/src/app/partner/deals/new/page.tsx b/apps/portal/src/app/partner/deals/new/page.tsx new file mode 100644 index 000000000..b16764b96 --- /dev/null +++ b/apps/portal/src/app/partner/deals/new/page.tsx @@ -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 ; +} diff --git a/apps/portal/src/app/partner/deals/page.tsx b/apps/portal/src/app/partner/deals/page.tsx new file mode 100644 index 000000000..b1acc0642 --- /dev/null +++ b/apps/portal/src/app/partner/deals/page.tsx @@ -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 ; +} diff --git a/apps/portal/src/app/partner/documents/page.tsx b/apps/portal/src/app/partner/documents/page.tsx new file mode 100644 index 000000000..1114f6578 --- /dev/null +++ b/apps/portal/src/app/partner/documents/page.tsx @@ -0,0 +1,8 @@ +import type { Metadata } from "next"; +import { Documents } from "@/components/screens/Documents"; + +export const metadata: Metadata = { title: "Documents" }; + +export default function Page(): React.JSX.Element { + return ; +} diff --git a/apps/portal/src/app/partner/layout.tsx b/apps/portal/src/app/partner/layout.tsx new file mode 100644 index 000000000..e9c6a5974 --- /dev/null +++ b/apps/portal/src/app/partner/layout.tsx @@ -0,0 +1,5 @@ +import { Shell } from "@/components/shell/shell"; + +export default function PartnerLayout({ children }: { children: React.ReactNode }): React.JSX.Element { + return {children}; +} diff --git a/apps/portal/src/app/partner/loading.tsx b/apps/portal/src/app/partner/loading.tsx new file mode 100644 index 000000000..3f062b08d --- /dev/null +++ b/apps/portal/src/app/partner/loading.tsx @@ -0,0 +1,5 @@ +import { LoadingSkeleton } from "@/components/screens/loading-skeleton"; + +export default function Loading(): React.JSX.Element { + return ; +} diff --git a/apps/portal/src/app/partner/overview/page.tsx b/apps/portal/src/app/partner/overview/page.tsx new file mode 100644 index 000000000..1ac9cc357 --- /dev/null +++ b/apps/portal/src/app/partner/overview/page.tsx @@ -0,0 +1,8 @@ +import type { Metadata } from "next"; +import { PartnerOverview } from "@/components/screens/PartnerOverview"; + +export const metadata: Metadata = { title: "Overview" }; + +export default function Page(): React.JSX.Element { + return ; +} diff --git a/apps/portal/src/app/partner/page.tsx b/apps/portal/src/app/partner/page.tsx new file mode 100644 index 000000000..d330b49bf --- /dev/null +++ b/apps/portal/src/app/partner/page.tsx @@ -0,0 +1,6 @@ +import { redirect } from "next/navigation"; +import { ROUTES } from "@/lib/routes"; + +export default function PartnerIndex(): never { + redirect(ROUTES.partner.overview); +} diff --git a/apps/portal/src/app/partner/team/page.tsx b/apps/portal/src/app/partner/team/page.tsx new file mode 100644 index 000000000..6d47373f1 --- /dev/null +++ b/apps/portal/src/app/partner/team/page.tsx @@ -0,0 +1,8 @@ +import type { Metadata } from "next"; +import { Team } from "@/components/screens/Team"; + +export const metadata: Metadata = { title: "Team" }; + +export default function Page(): React.JSX.Element { + return ; +} diff --git a/apps/portal/src/app/partner/welcome/page.tsx b/apps/portal/src/app/partner/welcome/page.tsx new file mode 100644 index 000000000..31348786b --- /dev/null +++ b/apps/portal/src/app/partner/welcome/page.tsx @@ -0,0 +1,8 @@ +import type { Metadata } from "next"; +import { PartnerWelcome } from "@/components/screens/PartnerWelcome"; + +export const metadata: Metadata = { title: "Welcome" }; + +export default function Page(): React.JSX.Element { + return ; +} diff --git a/apps/portal/src/components/deals/ManagerMessageDrawer.tsx b/apps/portal/src/components/deals/ManagerMessageDrawer.tsx new file mode 100644 index 000000000..2b269acc8 --- /dev/null +++ b/apps/portal/src/components/deals/ManagerMessageDrawer.tsx @@ -0,0 +1,88 @@ +"use client"; + +import { useState } from "react"; +import { Icon } from "../icon"; +import type { Persona } from "../shell/personas"; +import { Drawer } from "../ui/overlay"; +import { useToast } from "../ui/toast"; + +/** The "Message your partner manager" drawer, opened from a screen with a subject already filled in. */ +export function ManagerMessageDrawer({ + persona, + initialSubject, + onClose, +}: { + persona: Persona; + initialSubject: string; + onClose: () => void; +}): React.JSX.Element | null { + const toast = useToast(); + const [subject, setSubject] = useState(initialSubject); + const [body, setBody] = useState(""); + const [tried, setTried] = useState(false); + const manager = persona.manager; + if (!manager) return null; + const invalid = tried && !body.trim(); + + const send = (event: React.FormEvent): void => { + event.preventDefault(); + if (!body.trim()) { + setTried(true); + return; + } + toast(`Message sent to ${manager.name}`); + onClose(); + }; + + return ( + {manager.initials}} + onSubmit={send} + footer={ + <> + + Sent from {persona.user.toLowerCase().replace(" ", ".")}@{persona.org.toLowerCase().replace(/\s+/g, "")}.com + + + + + } + > +
+ + setSubject(e.target.value)} + /> +
+
+ +