ship-create 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +39 -0
- package/create.mjs +301 -0
- package/package.json +25 -0
- package/templates/.cursorrules +51 -0
- package/templates/.windsurfrules +51 -0
- package/templates/AGENTS.md +51 -0
- package/templates/CLAUDE.md +51 -0
- package/templates/docs/HUMAN_FLOW.md +205 -0
- package/templates/docs/PROJECT.md +154 -0
- package/templates/docs/PROMPTS.md +246 -0
- package/templates/docs/product-types/CRM_TEMPLATE.md +78 -0
- package/templates/docs/product-types/DASHBOARD_TEMPLATE.md +78 -0
- package/templates/docs/product-types/DIRECTORY_TEMPLATE.md +75 -0
- package/templates/docs/product-types/INTERNAL_TOOL_TEMPLATE.md +77 -0
- package/templates/docs/product-types/LEADGEN_TEMPLATE.md +78 -0
- package/templates/docs/product-types/MARKETPLACE_TEMPLATE.md +81 -0
- package/templates/docs/product-types/MEMBERSHIP_TEMPLATE.md +80 -0
- package/templates/docs/product-types/SAAS_TEMPLATE.md +79 -0
- package/templates/starter-kit/README.md +64 -0
- package/templates/starter-kit/app/backoffice/content/page.tsx +93 -0
- package/templates/starter-kit/app/backoffice/layout.tsx +105 -0
- package/templates/starter-kit/app/backoffice/page.tsx +165 -0
- package/templates/starter-kit/app/backoffice/settings/page.tsx +145 -0
- package/templates/starter-kit/app/backoffice/users/page.tsx +134 -0
- package/templates/starter-kit/app/globals.css +141 -0
- package/templates/starter-kit/app/layout.tsx +43 -0
- package/templates/starter-kit/app/member/(app)/billing/page.tsx +137 -0
- package/templates/starter-kit/app/member/(app)/content/page.tsx +111 -0
- package/templates/starter-kit/app/member/(app)/dashboard/page.tsx +129 -0
- package/templates/starter-kit/app/member/(app)/layout.tsx +130 -0
- package/templates/starter-kit/app/member/(app)/settings/page.tsx +96 -0
- package/templates/starter-kit/app/member/login/page.tsx +106 -0
- package/templates/starter-kit/app/member/signup/page.tsx +120 -0
- package/templates/starter-kit/app/page.tsx +82 -0
- package/templates/starter-kit/app/sale/_components/cta-footer.tsx +66 -0
- package/templates/starter-kit/app/sale/_components/faq.tsx +107 -0
- package/templates/starter-kit/app/sale/_components/features.tsx +95 -0
- package/templates/starter-kit/app/sale/_components/hero.tsx +106 -0
- package/templates/starter-kit/app/sale/_components/pricing.tsx +133 -0
- package/templates/starter-kit/app/sale/_components/problem.tsx +59 -0
- package/templates/starter-kit/app/sale/_components/testimonials.tsx +100 -0
- package/templates/starter-kit/app/sale/page.tsx +21 -0
- package/templates/starter-kit/components/ui/avatar.tsx +50 -0
- package/templates/starter-kit/components/ui/badge.tsx +36 -0
- package/templates/starter-kit/components/ui/button.tsx +56 -0
- package/templates/starter-kit/components/ui/card.tsx +78 -0
- package/templates/starter-kit/components/ui/input.tsx +24 -0
- package/templates/starter-kit/components/ui/table.tsx +88 -0
- package/templates/starter-kit/components/ui/tabs.tsx +55 -0
- package/templates/starter-kit/lib/mock-data.ts +118 -0
- package/templates/starter-kit/lib/utils.ts +6 -0
- package/templates/starter-kit/next.config.mjs +6 -0
- package/templates/starter-kit/package.json +36 -0
- package/templates/starter-kit/postcss.config.mjs +9 -0
- package/templates/starter-kit/tailwind.config.ts +83 -0
- package/templates/starter-kit/tsconfig.json +41 -0
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# SHIP Starter Kit
|
|
2
|
+
|
|
3
|
+
This is the code foundation for **The SHIP Method OS** — a Next.js 14 (App
|
|
4
|
+
Router) + TypeScript + Tailwind CSS starter that gives every downstream agent
|
|
5
|
+
a consistent, working UI shell to build on top of.
|
|
6
|
+
|
|
7
|
+
It is currently **mock-data only**. There is no backend wired up — no
|
|
8
|
+
Supabase, no auth, no payments. Every list, table, and metric you see is
|
|
9
|
+
sourced from `lib/mock-data.ts`. That's intentional: the goal of this layer
|
|
10
|
+
is a correct, consistent shared foundation (design tokens, primitives,
|
|
11
|
+
layout) that other agents can build real features on without re-deciding
|
|
12
|
+
button styles or color values per screen.
|
|
13
|
+
|
|
14
|
+
## Running it
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npm install
|
|
18
|
+
npm run dev
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Then open `http://localhost:3000`. The root page (`app/page.tsx`) is a
|
|
22
|
+
"pick your area" index linking to the three route groups below.
|
|
23
|
+
|
|
24
|
+
## Route groups (built by other agents on top of this foundation)
|
|
25
|
+
|
|
26
|
+
- **`/sale`** — the public-facing offer/sale page (headline, pitch, pricing,
|
|
27
|
+
CTA).
|
|
28
|
+
- **`/member`** — the logged-in member area (course access, progress,
|
|
29
|
+
account).
|
|
30
|
+
- **`/backoffice`** — the internal admin console (member management,
|
|
31
|
+
metrics, ops tools).
|
|
32
|
+
|
|
33
|
+
None of these route directories exist yet in this scaffold — they'll be
|
|
34
|
+
added as `app/(sale)/`, `app/(member)/`, `app/(backoffice)/` (or similar)
|
|
35
|
+
route groups by the agents responsible for each area. They should reuse the
|
|
36
|
+
shared primitives in `components/ui/` and the `cn()` helper in
|
|
37
|
+
`lib/utils.ts` rather than introducing new component patterns.
|
|
38
|
+
|
|
39
|
+
## What's already here
|
|
40
|
+
|
|
41
|
+
- `app/` — root layout, global styles (Tailwind + shadcn-style CSS
|
|
42
|
+
variables for light/dark themes), and the index page.
|
|
43
|
+
- `components/ui/` — Button, Card, Input, Badge, Table, Avatar, Tabs —
|
|
44
|
+
shadcn/ui-pattern primitives built on Radix UI + class-variance-authority.
|
|
45
|
+
- `lib/utils.ts` — the standard `cn()` class-merging helper.
|
|
46
|
+
- `lib/mock-data.ts` — typed mock `users`, `members` (course/content), and
|
|
47
|
+
`metrics` arrays shared across all three route groups.
|
|
48
|
+
- Design tokens (colors, radius) in `app/globals.css` and
|
|
49
|
+
`tailwind.config.ts` are pulled from
|
|
50
|
+
`../12-DESIGN-SYSTEM/DESIGN_SYSTEM.md` Section 4 (Color System).
|
|
51
|
+
|
|
52
|
+
## Next step: real data
|
|
53
|
+
|
|
54
|
+
When it's time to move off mock data, replace the contents of
|
|
55
|
+
`lib/mock-data.ts` (or the call sites that import from it) with real
|
|
56
|
+
Supabase queries. Before doing that, read:
|
|
57
|
+
|
|
58
|
+
- `../13-TECH-STACK/TECH_STACK.md` — the chosen stack and why.
|
|
59
|
+
- `../03-INSTRUCTION/DATABASE_SPEC.md` — the database schema this app
|
|
60
|
+
should query against.
|
|
61
|
+
|
|
62
|
+
Keep the typed interfaces (`MockUser`, `MockMemberContent`, `MockMetric`)
|
|
63
|
+
as the contract — swap the data source, not the shape, unless the database
|
|
64
|
+
spec requires otherwise.
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { Plus, Pencil, Trash2 } from "lucide-react";
|
|
4
|
+
|
|
5
|
+
import { Button } from "@/components/ui/button";
|
|
6
|
+
import { Badge } from "@/components/ui/badge";
|
|
7
|
+
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
|
8
|
+
import {
|
|
9
|
+
Table,
|
|
10
|
+
TableBody,
|
|
11
|
+
TableCell,
|
|
12
|
+
TableHead,
|
|
13
|
+
TableHeader,
|
|
14
|
+
TableRow,
|
|
15
|
+
} from "@/components/ui/table";
|
|
16
|
+
import { members } from "@/lib/mock-data";
|
|
17
|
+
|
|
18
|
+
// Fabricated publish status, hardcoded per-row for visual variety (not in mock-data.ts).
|
|
19
|
+
const contentStatus: Record<string, "Published" | "Draft"> = {
|
|
20
|
+
crs_001: "Published",
|
|
21
|
+
crs_002: "Published",
|
|
22
|
+
crs_003: "Draft",
|
|
23
|
+
crs_004: "Draft",
|
|
24
|
+
crs_005: "Published",
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export default function BackofficeContentPage() {
|
|
28
|
+
return (
|
|
29
|
+
<div className="space-y-6">
|
|
30
|
+
<div className="flex items-center justify-between gap-4">
|
|
31
|
+
<div>
|
|
32
|
+
<p className="label-mono text-primary">Library</p>
|
|
33
|
+
<h1 className="font-display text-2xl font-medium tracking-tight text-foreground">
|
|
34
|
+
Content
|
|
35
|
+
</h1>
|
|
36
|
+
<p className="mt-1 text-sm text-muted-foreground">
|
|
37
|
+
Manage courses and modules available to members.
|
|
38
|
+
</p>
|
|
39
|
+
</div>
|
|
40
|
+
<Button onClick={() => {}}>
|
|
41
|
+
<Plus className="h-4 w-4" />
|
|
42
|
+
New Content
|
|
43
|
+
</Button>
|
|
44
|
+
</div>
|
|
45
|
+
|
|
46
|
+
<Card>
|
|
47
|
+
<CardHeader>
|
|
48
|
+
<CardTitle>All Content</CardTitle>
|
|
49
|
+
</CardHeader>
|
|
50
|
+
<CardContent>
|
|
51
|
+
<Table>
|
|
52
|
+
<TableHeader>
|
|
53
|
+
<TableRow>
|
|
54
|
+
<TableHead>Title</TableHead>
|
|
55
|
+
<TableHead>Tier</TableHead>
|
|
56
|
+
<TableHead>Status</TableHead>
|
|
57
|
+
<TableHead className="text-right">Actions</TableHead>
|
|
58
|
+
</TableRow>
|
|
59
|
+
</TableHeader>
|
|
60
|
+
<TableBody>
|
|
61
|
+
{members.map((item) => {
|
|
62
|
+
const status = contentStatus[item.id] ?? "Draft";
|
|
63
|
+
return (
|
|
64
|
+
<TableRow key={item.id}>
|
|
65
|
+
<TableCell className="py-3 font-medium text-foreground">{item.title}</TableCell>
|
|
66
|
+
<TableCell className="py-3">
|
|
67
|
+
<Badge variant="outline">{item.tier}</Badge>
|
|
68
|
+
</TableCell>
|
|
69
|
+
<TableCell className="py-3">
|
|
70
|
+
<Badge variant={status === "Published" ? "default" : "secondary"}>
|
|
71
|
+
{status}
|
|
72
|
+
</Badge>
|
|
73
|
+
</TableCell>
|
|
74
|
+
<TableCell className="flex justify-end gap-2 py-3">
|
|
75
|
+
<Button variant="outline" size="sm" onClick={() => {}}>
|
|
76
|
+
<Pencil className="h-3.5 w-3.5" />
|
|
77
|
+
Edit
|
|
78
|
+
</Button>
|
|
79
|
+
<Button variant="destructive" size="sm" onClick={() => {}}>
|
|
80
|
+
<Trash2 className="h-3.5 w-3.5" />
|
|
81
|
+
Delete
|
|
82
|
+
</Button>
|
|
83
|
+
</TableCell>
|
|
84
|
+
</TableRow>
|
|
85
|
+
);
|
|
86
|
+
})}
|
|
87
|
+
</TableBody>
|
|
88
|
+
</Table>
|
|
89
|
+
</CardContent>
|
|
90
|
+
</Card>
|
|
91
|
+
</div>
|
|
92
|
+
);
|
|
93
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import Link from "next/link";
|
|
2
|
+
import {
|
|
3
|
+
LayoutDashboard,
|
|
4
|
+
Users,
|
|
5
|
+
FileText,
|
|
6
|
+
Settings,
|
|
7
|
+
Search,
|
|
8
|
+
ShieldCheck,
|
|
9
|
+
} from "lucide-react";
|
|
10
|
+
|
|
11
|
+
import { cn } from "@/lib/utils";
|
|
12
|
+
import { Input } from "@/components/ui/input";
|
|
13
|
+
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
|
|
14
|
+
|
|
15
|
+
const navItems = [
|
|
16
|
+
{ label: "Overview", href: "/backoffice", icon: LayoutDashboard },
|
|
17
|
+
{ label: "Users", href: "/backoffice/users", icon: Users },
|
|
18
|
+
{ label: "Content", href: "/backoffice/content", icon: FileText },
|
|
19
|
+
{ label: "Settings", href: "/backoffice/settings", icon: Settings },
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
export default function BackofficeLayout({
|
|
23
|
+
children,
|
|
24
|
+
}: {
|
|
25
|
+
children: React.ReactNode;
|
|
26
|
+
}) {
|
|
27
|
+
return (
|
|
28
|
+
<div className="flex min-h-screen bg-background">
|
|
29
|
+
{/* Sidebar — deeper-than-base ink with a faint grid texture, kept
|
|
30
|
+
visually distinct from the member sidebar (bg-card) while staying
|
|
31
|
+
in the same warm-ink palette. */}
|
|
32
|
+
<aside className="relative flex w-64 flex-col overflow-hidden border-r border-border/80 bg-[hsl(27_20%_5%)]">
|
|
33
|
+
<div className="bg-grid pointer-events-none absolute inset-0 opacity-[0.06]" />
|
|
34
|
+
|
|
35
|
+
<div className="relative z-10 flex items-center gap-3 border-b border-white/5 px-6 py-5">
|
|
36
|
+
<ShieldCheck className="h-5 w-5 text-primary" />
|
|
37
|
+
<div>
|
|
38
|
+
<p className="font-display text-base font-medium leading-none text-foreground">
|
|
39
|
+
SHIP Admin
|
|
40
|
+
</p>
|
|
41
|
+
<p className="label-mono mt-1 text-muted-foreground/70">Admin</p>
|
|
42
|
+
</div>
|
|
43
|
+
</div>
|
|
44
|
+
|
|
45
|
+
<nav className="relative z-10 flex-1 space-y-1 px-3 py-4">
|
|
46
|
+
{navItems.map((item) => {
|
|
47
|
+
const Icon = item.icon;
|
|
48
|
+
const active = item.href === "/backoffice";
|
|
49
|
+
return (
|
|
50
|
+
<Link
|
|
51
|
+
key={item.href}
|
|
52
|
+
href={item.href}
|
|
53
|
+
className={cn(
|
|
54
|
+
"group relative flex items-center gap-3 rounded-full px-3 py-2 text-sm font-medium transition-colors",
|
|
55
|
+
active
|
|
56
|
+
? "bg-primary/15 text-primary"
|
|
57
|
+
: "text-muted-foreground hover:bg-white/5 hover:text-foreground"
|
|
58
|
+
)}
|
|
59
|
+
>
|
|
60
|
+
{active && (
|
|
61
|
+
<span className="absolute -left-3 top-1/2 h-4 w-1 -translate-y-1/2 rounded-full bg-primary" />
|
|
62
|
+
)}
|
|
63
|
+
<Icon className="h-4 w-4" />
|
|
64
|
+
{item.label}
|
|
65
|
+
</Link>
|
|
66
|
+
);
|
|
67
|
+
})}
|
|
68
|
+
</nav>
|
|
69
|
+
|
|
70
|
+
<div className="label-mono relative z-10 border-t border-white/5 px-4 py-4 text-muted-foreground/60">
|
|
71
|
+
Mock-data UI shell — no backend wired up.
|
|
72
|
+
</div>
|
|
73
|
+
</aside>
|
|
74
|
+
|
|
75
|
+
{/* Main column */}
|
|
76
|
+
<div className="flex flex-1 flex-col">
|
|
77
|
+
{/* Top bar */}
|
|
78
|
+
<header className="flex items-center justify-between gap-4 border-b border-border bg-background px-6 py-3.5">
|
|
79
|
+
<div className="relative w-full max-w-sm">
|
|
80
|
+
<Search className="pointer-events-none absolute left-3.5 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
|
|
81
|
+
<Input
|
|
82
|
+
placeholder="Search users, content..."
|
|
83
|
+
className="h-10 pl-10"
|
|
84
|
+
readOnly
|
|
85
|
+
/>
|
|
86
|
+
</div>
|
|
87
|
+
|
|
88
|
+
<div className="flex items-center gap-3">
|
|
89
|
+
<div className="text-right">
|
|
90
|
+
<p className="text-sm font-medium leading-none text-foreground">
|
|
91
|
+
Nuttadech J.
|
|
92
|
+
</p>
|
|
93
|
+
<p className="label-mono mt-1 text-muted-foreground/70">Admin</p>
|
|
94
|
+
</div>
|
|
95
|
+
<Avatar>
|
|
96
|
+
<AvatarFallback>NJ</AvatarFallback>
|
|
97
|
+
</Avatar>
|
|
98
|
+
</div>
|
|
99
|
+
</header>
|
|
100
|
+
|
|
101
|
+
<main className="flex-1 p-6">{children}</main>
|
|
102
|
+
</div>
|
|
103
|
+
</div>
|
|
104
|
+
);
|
|
105
|
+
}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { Users as UsersIcon, DollarSign, Activity, TrendingDown, TrendingUp } from "lucide-react";
|
|
2
|
+
|
|
3
|
+
import { Card, CardContent, CardHeader } from "@/components/ui/card";
|
|
4
|
+
import {
|
|
5
|
+
Table,
|
|
6
|
+
TableBody,
|
|
7
|
+
TableCell,
|
|
8
|
+
TableHead,
|
|
9
|
+
TableHeader,
|
|
10
|
+
TableRow,
|
|
11
|
+
} from "@/components/ui/table";
|
|
12
|
+
import { Badge } from "@/components/ui/badge";
|
|
13
|
+
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
|
|
14
|
+
import { users, metrics } from "@/lib/mock-data";
|
|
15
|
+
|
|
16
|
+
const statusVariant: Record<string, "default" | "secondary" | "outline" | "destructive"> = {
|
|
17
|
+
active: "default",
|
|
18
|
+
trialing: "secondary",
|
|
19
|
+
past_due: "destructive",
|
|
20
|
+
canceled: "outline",
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
function initials(name: string) {
|
|
24
|
+
return name
|
|
25
|
+
.split(" ")
|
|
26
|
+
.map((part) => part[0])
|
|
27
|
+
.join("")
|
|
28
|
+
.slice(0, 2)
|
|
29
|
+
.toUpperCase();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const kpis = [
|
|
33
|
+
{
|
|
34
|
+
label: "Total Users",
|
|
35
|
+
value: String(users.length),
|
|
36
|
+
icon: UsersIcon,
|
|
37
|
+
change: undefined as number | undefined,
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
label: metrics[0].label === "Monthly Recurring Revenue" ? "MRR" : metrics[0].label,
|
|
41
|
+
value: metrics[0].value,
|
|
42
|
+
icon: DollarSign,
|
|
43
|
+
change: metrics[0].change,
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
label: "Active Members",
|
|
47
|
+
value: metrics[1].value,
|
|
48
|
+
icon: Activity,
|
|
49
|
+
change: metrics[1].change,
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
label: "Churn Rate",
|
|
53
|
+
value: metrics[3].value,
|
|
54
|
+
icon: TrendingDown,
|
|
55
|
+
change: metrics[3].change,
|
|
56
|
+
},
|
|
57
|
+
];
|
|
58
|
+
|
|
59
|
+
const recentUsers = [...users]
|
|
60
|
+
.sort((a, b) => new Date(b.joinedAt).getTime() - new Date(a.joinedAt).getTime())
|
|
61
|
+
.slice(0, 5);
|
|
62
|
+
|
|
63
|
+
export default function BackofficeOverviewPage() {
|
|
64
|
+
return (
|
|
65
|
+
<div className="space-y-6">
|
|
66
|
+
<div>
|
|
67
|
+
<p className="label-mono text-primary">Admin</p>
|
|
68
|
+
<h1 className="font-display text-2xl font-medium tracking-tight text-foreground">
|
|
69
|
+
Overview
|
|
70
|
+
</h1>
|
|
71
|
+
<p className="mt-1 text-sm text-muted-foreground">
|
|
72
|
+
Admin-level snapshot of accounts and revenue.
|
|
73
|
+
</p>
|
|
74
|
+
</div>
|
|
75
|
+
|
|
76
|
+
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4">
|
|
77
|
+
{kpis.map((kpi, i) => {
|
|
78
|
+
const Icon = kpi.icon;
|
|
79
|
+
const isPositive = (kpi.change ?? 0) >= 0;
|
|
80
|
+
return (
|
|
81
|
+
<Card
|
|
82
|
+
key={kpi.label}
|
|
83
|
+
className="animate-fade-up"
|
|
84
|
+
style={{ animationDelay: `${i * 80}ms` }}
|
|
85
|
+
>
|
|
86
|
+
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
|
87
|
+
<span className="label-mono text-muted-foreground">{kpi.label}</span>
|
|
88
|
+
<Icon className="h-4 w-4 text-muted-foreground" />
|
|
89
|
+
</CardHeader>
|
|
90
|
+
<CardContent className="flex items-end justify-between gap-2">
|
|
91
|
+
<span className="font-display text-3xl font-medium tracking-tight text-foreground">
|
|
92
|
+
{kpi.value}
|
|
93
|
+
</span>
|
|
94
|
+
{kpi.change !== undefined && (
|
|
95
|
+
<Badge
|
|
96
|
+
variant={isPositive ? "default" : "destructive"}
|
|
97
|
+
className="mb-0.5 gap-1"
|
|
98
|
+
>
|
|
99
|
+
{isPositive ? (
|
|
100
|
+
<TrendingUp className="h-3 w-3" />
|
|
101
|
+
) : (
|
|
102
|
+
<TrendingDown className="h-3 w-3" />
|
|
103
|
+
)}
|
|
104
|
+
{isPositive ? "+" : ""}
|
|
105
|
+
{kpi.change}%
|
|
106
|
+
</Badge>
|
|
107
|
+
)}
|
|
108
|
+
</CardContent>
|
|
109
|
+
</Card>
|
|
110
|
+
);
|
|
111
|
+
})}
|
|
112
|
+
</div>
|
|
113
|
+
|
|
114
|
+
<Card>
|
|
115
|
+
<CardHeader>
|
|
116
|
+
<span className="font-display text-xl font-medium leading-tight tracking-tight text-foreground">
|
|
117
|
+
Recent Signups
|
|
118
|
+
</span>
|
|
119
|
+
</CardHeader>
|
|
120
|
+
<CardContent>
|
|
121
|
+
<Table>
|
|
122
|
+
<TableHeader>
|
|
123
|
+
<TableRow>
|
|
124
|
+
<TableHead>Name</TableHead>
|
|
125
|
+
<TableHead>Email</TableHead>
|
|
126
|
+
<TableHead>Plan</TableHead>
|
|
127
|
+
<TableHead>Status</TableHead>
|
|
128
|
+
<TableHead>Joined</TableHead>
|
|
129
|
+
</TableRow>
|
|
130
|
+
</TableHeader>
|
|
131
|
+
<TableBody>
|
|
132
|
+
{recentUsers.map((user) => (
|
|
133
|
+
<TableRow key={user.id} className="group relative">
|
|
134
|
+
<TableCell className="font-medium">
|
|
135
|
+
<span className="absolute -left-2 top-0 h-full w-0.5 origin-top scale-y-0 bg-primary transition-transform duration-200 group-hover:scale-y-100" />
|
|
136
|
+
<div className="flex items-center gap-3">
|
|
137
|
+
<Avatar className="h-8 w-8">
|
|
138
|
+
<AvatarFallback className="text-xs">
|
|
139
|
+
{initials(user.name)}
|
|
140
|
+
</AvatarFallback>
|
|
141
|
+
</Avatar>
|
|
142
|
+
<span className="text-foreground">{user.name}</span>
|
|
143
|
+
</div>
|
|
144
|
+
</TableCell>
|
|
145
|
+
<TableCell className="text-muted-foreground">{user.email}</TableCell>
|
|
146
|
+
<TableCell>
|
|
147
|
+
<Badge variant="outline">{user.plan}</Badge>
|
|
148
|
+
</TableCell>
|
|
149
|
+
<TableCell>
|
|
150
|
+
<Badge variant={statusVariant[user.status] ?? "outline"}>
|
|
151
|
+
{user.status.replace("_", " ")}
|
|
152
|
+
</Badge>
|
|
153
|
+
</TableCell>
|
|
154
|
+
<TableCell className="text-muted-foreground">
|
|
155
|
+
{new Date(user.joinedAt).toLocaleDateString()}
|
|
156
|
+
</TableCell>
|
|
157
|
+
</TableRow>
|
|
158
|
+
))}
|
|
159
|
+
</TableBody>
|
|
160
|
+
</Table>
|
|
161
|
+
</CardContent>
|
|
162
|
+
</Card>
|
|
163
|
+
</div>
|
|
164
|
+
);
|
|
165
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
|
|
5
|
+
import { Button } from "@/components/ui/button";
|
|
6
|
+
import { Input } from "@/components/ui/input";
|
|
7
|
+
import { Badge } from "@/components/ui/badge";
|
|
8
|
+
import {
|
|
9
|
+
Card,
|
|
10
|
+
CardContent,
|
|
11
|
+
CardDescription,
|
|
12
|
+
CardFooter,
|
|
13
|
+
CardHeader,
|
|
14
|
+
CardTitle,
|
|
15
|
+
} from "@/components/ui/card";
|
|
16
|
+
import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs";
|
|
17
|
+
import {
|
|
18
|
+
Table,
|
|
19
|
+
TableBody,
|
|
20
|
+
TableCell,
|
|
21
|
+
TableHead,
|
|
22
|
+
TableHeader,
|
|
23
|
+
TableRow,
|
|
24
|
+
} from "@/components/ui/table";
|
|
25
|
+
|
|
26
|
+
// Fabricated team roster for the Team tab (not in mock-data.ts).
|
|
27
|
+
const team = [
|
|
28
|
+
{ id: "tm_001", name: "Nuttadech J.", email: "nuttadechnd@gmail.com", role: "Owner" },
|
|
29
|
+
{ id: "tm_002", name: "Pim Anantasak", email: "pim@shipmethod.dev", role: "Admin" },
|
|
30
|
+
{ id: "tm_003", name: "Tum Charoensuk", email: "tum@shipmethod.dev", role: "Support" },
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
const roleVariant: Record<string, "default" | "secondary" | "outline"> = {
|
|
34
|
+
Owner: "default",
|
|
35
|
+
Admin: "secondary",
|
|
36
|
+
Support: "outline",
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export default function BackofficeSettingsPage() {
|
|
40
|
+
return (
|
|
41
|
+
<div className="space-y-6">
|
|
42
|
+
<div>
|
|
43
|
+
<p className="label-mono text-primary">Workspace</p>
|
|
44
|
+
<h1 className="font-display text-2xl font-medium tracking-tight text-foreground">
|
|
45
|
+
Settings
|
|
46
|
+
</h1>
|
|
47
|
+
<p className="mt-1 text-sm text-muted-foreground">
|
|
48
|
+
Manage organization, team access, and billing.
|
|
49
|
+
</p>
|
|
50
|
+
</div>
|
|
51
|
+
|
|
52
|
+
<Tabs defaultValue="general">
|
|
53
|
+
<TabsList>
|
|
54
|
+
<TabsTrigger value="general">General</TabsTrigger>
|
|
55
|
+
<TabsTrigger value="team">Team</TabsTrigger>
|
|
56
|
+
<TabsTrigger value="billing">Billing</TabsTrigger>
|
|
57
|
+
</TabsList>
|
|
58
|
+
|
|
59
|
+
<TabsContent value="general" className="mt-4">
|
|
60
|
+
<Card>
|
|
61
|
+
<CardHeader>
|
|
62
|
+
<CardTitle>Organization</CardTitle>
|
|
63
|
+
<CardDescription>
|
|
64
|
+
Basic details about your workspace.
|
|
65
|
+
</CardDescription>
|
|
66
|
+
</CardHeader>
|
|
67
|
+
<CardContent className="max-w-sm space-y-2">
|
|
68
|
+
<label className="label-mono text-muted-foreground" htmlFor="org-name">
|
|
69
|
+
Organization name
|
|
70
|
+
</label>
|
|
71
|
+
<Input id="org-name" defaultValue="The SHIP Method OS" />
|
|
72
|
+
</CardContent>
|
|
73
|
+
<CardFooter>
|
|
74
|
+
<Button onClick={() => {}}>Save</Button>
|
|
75
|
+
</CardFooter>
|
|
76
|
+
</Card>
|
|
77
|
+
</TabsContent>
|
|
78
|
+
|
|
79
|
+
<TabsContent value="team" className="mt-4">
|
|
80
|
+
<Card>
|
|
81
|
+
<CardHeader>
|
|
82
|
+
<CardTitle>Team Members</CardTitle>
|
|
83
|
+
<CardDescription>People with access to the backoffice.</CardDescription>
|
|
84
|
+
</CardHeader>
|
|
85
|
+
<CardContent>
|
|
86
|
+
<Table>
|
|
87
|
+
<TableHeader>
|
|
88
|
+
<TableRow>
|
|
89
|
+
<TableHead>Name</TableHead>
|
|
90
|
+
<TableHead>Email</TableHead>
|
|
91
|
+
<TableHead>Role</TableHead>
|
|
92
|
+
</TableRow>
|
|
93
|
+
</TableHeader>
|
|
94
|
+
<TableBody>
|
|
95
|
+
{team.map((member) => (
|
|
96
|
+
<TableRow key={member.id}>
|
|
97
|
+
<TableCell className="py-3 font-medium text-foreground">
|
|
98
|
+
{member.name}
|
|
99
|
+
</TableCell>
|
|
100
|
+
<TableCell className="py-3 text-muted-foreground">
|
|
101
|
+
{member.email}
|
|
102
|
+
</TableCell>
|
|
103
|
+
<TableCell className="py-3">
|
|
104
|
+
<Badge variant={roleVariant[member.role] ?? "outline"}>
|
|
105
|
+
{member.role}
|
|
106
|
+
</Badge>
|
|
107
|
+
</TableCell>
|
|
108
|
+
</TableRow>
|
|
109
|
+
))}
|
|
110
|
+
</TableBody>
|
|
111
|
+
</Table>
|
|
112
|
+
</CardContent>
|
|
113
|
+
</Card>
|
|
114
|
+
</TabsContent>
|
|
115
|
+
|
|
116
|
+
<TabsContent value="billing" className="mt-4">
|
|
117
|
+
<Card className="glow-primary border-primary/30">
|
|
118
|
+
<CardHeader>
|
|
119
|
+
<CardTitle>Current Plan</CardTitle>
|
|
120
|
+
<CardDescription>
|
|
121
|
+
Your organization is on the Pro plan, billed monthly.
|
|
122
|
+
</CardDescription>
|
|
123
|
+
</CardHeader>
|
|
124
|
+
<CardContent>
|
|
125
|
+
<div className="flex items-center justify-between rounded-xl border border-primary/20 bg-primary/[0.06] p-4">
|
|
126
|
+
<div>
|
|
127
|
+
<p className="font-display text-lg font-medium text-foreground">
|
|
128
|
+
Pro Plan
|
|
129
|
+
</p>
|
|
130
|
+
<p className="text-sm text-muted-foreground">
|
|
131
|
+
$99/month · renews on the 1st
|
|
132
|
+
</p>
|
|
133
|
+
</div>
|
|
134
|
+
<Badge>Active</Badge>
|
|
135
|
+
</div>
|
|
136
|
+
</CardContent>
|
|
137
|
+
<CardFooter>
|
|
138
|
+
<Button onClick={() => {}}>Upgrade Plan</Button>
|
|
139
|
+
</CardFooter>
|
|
140
|
+
</Card>
|
|
141
|
+
</TabsContent>
|
|
142
|
+
</Tabs>
|
|
143
|
+
</div>
|
|
144
|
+
);
|
|
145
|
+
}
|