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.
Files changed (56) hide show
  1. package/README.md +39 -0
  2. package/create.mjs +301 -0
  3. package/package.json +25 -0
  4. package/templates/.cursorrules +51 -0
  5. package/templates/.windsurfrules +51 -0
  6. package/templates/AGENTS.md +51 -0
  7. package/templates/CLAUDE.md +51 -0
  8. package/templates/docs/HUMAN_FLOW.md +205 -0
  9. package/templates/docs/PROJECT.md +154 -0
  10. package/templates/docs/PROMPTS.md +246 -0
  11. package/templates/docs/product-types/CRM_TEMPLATE.md +78 -0
  12. package/templates/docs/product-types/DASHBOARD_TEMPLATE.md +78 -0
  13. package/templates/docs/product-types/DIRECTORY_TEMPLATE.md +75 -0
  14. package/templates/docs/product-types/INTERNAL_TOOL_TEMPLATE.md +77 -0
  15. package/templates/docs/product-types/LEADGEN_TEMPLATE.md +78 -0
  16. package/templates/docs/product-types/MARKETPLACE_TEMPLATE.md +81 -0
  17. package/templates/docs/product-types/MEMBERSHIP_TEMPLATE.md +80 -0
  18. package/templates/docs/product-types/SAAS_TEMPLATE.md +79 -0
  19. package/templates/starter-kit/README.md +64 -0
  20. package/templates/starter-kit/app/backoffice/content/page.tsx +93 -0
  21. package/templates/starter-kit/app/backoffice/layout.tsx +105 -0
  22. package/templates/starter-kit/app/backoffice/page.tsx +165 -0
  23. package/templates/starter-kit/app/backoffice/settings/page.tsx +145 -0
  24. package/templates/starter-kit/app/backoffice/users/page.tsx +134 -0
  25. package/templates/starter-kit/app/globals.css +141 -0
  26. package/templates/starter-kit/app/layout.tsx +43 -0
  27. package/templates/starter-kit/app/member/(app)/billing/page.tsx +137 -0
  28. package/templates/starter-kit/app/member/(app)/content/page.tsx +111 -0
  29. package/templates/starter-kit/app/member/(app)/dashboard/page.tsx +129 -0
  30. package/templates/starter-kit/app/member/(app)/layout.tsx +130 -0
  31. package/templates/starter-kit/app/member/(app)/settings/page.tsx +96 -0
  32. package/templates/starter-kit/app/member/login/page.tsx +106 -0
  33. package/templates/starter-kit/app/member/signup/page.tsx +120 -0
  34. package/templates/starter-kit/app/page.tsx +82 -0
  35. package/templates/starter-kit/app/sale/_components/cta-footer.tsx +66 -0
  36. package/templates/starter-kit/app/sale/_components/faq.tsx +107 -0
  37. package/templates/starter-kit/app/sale/_components/features.tsx +95 -0
  38. package/templates/starter-kit/app/sale/_components/hero.tsx +106 -0
  39. package/templates/starter-kit/app/sale/_components/pricing.tsx +133 -0
  40. package/templates/starter-kit/app/sale/_components/problem.tsx +59 -0
  41. package/templates/starter-kit/app/sale/_components/testimonials.tsx +100 -0
  42. package/templates/starter-kit/app/sale/page.tsx +21 -0
  43. package/templates/starter-kit/components/ui/avatar.tsx +50 -0
  44. package/templates/starter-kit/components/ui/badge.tsx +36 -0
  45. package/templates/starter-kit/components/ui/button.tsx +56 -0
  46. package/templates/starter-kit/components/ui/card.tsx +78 -0
  47. package/templates/starter-kit/components/ui/input.tsx +24 -0
  48. package/templates/starter-kit/components/ui/table.tsx +88 -0
  49. package/templates/starter-kit/components/ui/tabs.tsx +55 -0
  50. package/templates/starter-kit/lib/mock-data.ts +118 -0
  51. package/templates/starter-kit/lib/utils.ts +6 -0
  52. package/templates/starter-kit/next.config.mjs +6 -0
  53. package/templates/starter-kit/package.json +36 -0
  54. package/templates/starter-kit/postcss.config.mjs +9 -0
  55. package/templates/starter-kit/tailwind.config.ts +83 -0
  56. package/templates/starter-kit/tsconfig.json +41 -0
@@ -0,0 +1,134 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import { UserPlus } from "lucide-react";
5
+
6
+ import { Button } from "@/components/ui/button";
7
+ import { Badge } from "@/components/ui/badge";
8
+ import { Avatar, AvatarFallback } from "@/components/ui/avatar";
9
+ import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs";
10
+ import {
11
+ Table,
12
+ TableBody,
13
+ TableCell,
14
+ TableHead,
15
+ TableHeader,
16
+ TableRow,
17
+ } from "@/components/ui/table";
18
+ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
19
+ import { users, type MockUser } from "@/lib/mock-data";
20
+
21
+ const statusVariant: Record<string, "default" | "secondary" | "outline" | "destructive"> = {
22
+ active: "default",
23
+ trialing: "secondary",
24
+ past_due: "destructive",
25
+ canceled: "outline",
26
+ };
27
+
28
+ function initials(name: string) {
29
+ return name
30
+ .split(" ")
31
+ .map((part) => part[0])
32
+ .join("")
33
+ .slice(0, 2)
34
+ .toUpperCase();
35
+ }
36
+
37
+ function UsersTable({ data }: { data: MockUser[] }) {
38
+ return (
39
+ <Table>
40
+ <TableHeader>
41
+ <TableRow>
42
+ <TableHead>Name</TableHead>
43
+ <TableHead>Email</TableHead>
44
+ <TableHead>Plan</TableHead>
45
+ <TableHead>Status</TableHead>
46
+ <TableHead>Joined</TableHead>
47
+ <TableHead className="text-right">Actions</TableHead>
48
+ </TableRow>
49
+ </TableHeader>
50
+ <TableBody>
51
+ {data.map((user) => (
52
+ <TableRow key={user.id}>
53
+ <TableCell className="py-3 font-medium">
54
+ <div className="flex items-center gap-3">
55
+ <Avatar className="h-8 w-8">
56
+ <AvatarFallback className="text-xs">
57
+ {initials(user.name)}
58
+ </AvatarFallback>
59
+ </Avatar>
60
+ <span className="text-foreground">{user.name}</span>
61
+ </div>
62
+ </TableCell>
63
+ <TableCell className="py-3 text-muted-foreground">{user.email}</TableCell>
64
+ <TableCell className="py-3">
65
+ <Badge variant="outline">{user.plan}</Badge>
66
+ </TableCell>
67
+ <TableCell className="py-3">
68
+ <Badge variant={statusVariant[user.status] ?? "outline"}>
69
+ {user.status.replace("_", " ")}
70
+ </Badge>
71
+ </TableCell>
72
+ <TableCell className="py-3 text-muted-foreground">
73
+ {new Date(user.joinedAt).toLocaleDateString()}
74
+ </TableCell>
75
+ <TableCell className="py-3 text-right">
76
+ <Button variant="outline" size="sm" onClick={() => {}}>
77
+ View
78
+ </Button>
79
+ </TableCell>
80
+ </TableRow>
81
+ ))}
82
+ </TableBody>
83
+ </Table>
84
+ );
85
+ }
86
+
87
+ export default function BackofficeUsersPage() {
88
+ const active = users.filter((u) => u.status === "active");
89
+ const inactive = users.filter((u) => u.status !== "active");
90
+
91
+ return (
92
+ <div className="space-y-6">
93
+ <div className="flex items-center justify-between gap-4">
94
+ <div>
95
+ <p className="label-mono text-primary">Accounts</p>
96
+ <h1 className="font-display text-2xl font-medium tracking-tight text-foreground">
97
+ Users
98
+ </h1>
99
+ <p className="mt-1 text-sm text-muted-foreground">
100
+ Manage all registered accounts.
101
+ </p>
102
+ </div>
103
+ <Button onClick={() => {}}>
104
+ <UserPlus className="h-4 w-4" />
105
+ Invite User
106
+ </Button>
107
+ </div>
108
+
109
+ <Card>
110
+ <CardHeader>
111
+ <CardTitle>All Users</CardTitle>
112
+ </CardHeader>
113
+ <CardContent>
114
+ <Tabs defaultValue="all">
115
+ <TabsList>
116
+ <TabsTrigger value="all">All ({users.length})</TabsTrigger>
117
+ <TabsTrigger value="active">Active ({active.length})</TabsTrigger>
118
+ <TabsTrigger value="inactive">Inactive ({inactive.length})</TabsTrigger>
119
+ </TabsList>
120
+ <TabsContent value="all">
121
+ <UsersTable data={users} />
122
+ </TabsContent>
123
+ <TabsContent value="active">
124
+ <UsersTable data={active} />
125
+ </TabsContent>
126
+ <TabsContent value="inactive">
127
+ <UsersTable data={inactive} />
128
+ </TabsContent>
129
+ </Tabs>
130
+ </CardContent>
131
+ </Card>
132
+ </div>
133
+ );
134
+ }
@@ -0,0 +1,141 @@
1
+ @tailwind base;
2
+ @tailwind components;
3
+ @tailwind utilities;
4
+
5
+ /*
6
+ "Ember Editorial" — the SHIP Starter Kit's 2026 visual direction.
7
+ A warm-ink dark theme (not slate/blue-black) with a single confident
8
+ ember-coral accent, replacing the generic purple-gradient-on-white default.
9
+ Values are HSL triplets so Tailwind's hsl(var(--x)) usage works.
10
+ */
11
+
12
+ @layer base {
13
+ :root {
14
+ --background: 27 18% 8%; /* warm espresso ink, not cold slate */
15
+ --foreground: 39 38% 93%; /* warm parchment white */
16
+
17
+ --card: 26 16% 11%;
18
+ --card-foreground: 39 38% 93%;
19
+
20
+ --primary: 14 92% 58%; /* ember coral */
21
+ --primary-foreground: 27 30% 8%;
22
+
23
+ --secondary: 84 22% 52%; /* moss green, the cool counterweight */
24
+ --secondary-foreground: 27 30% 8%;
25
+
26
+ --accent: 38 78% 62%; /* warm gold for highlights / eyebrow text */
27
+ --accent-foreground: 27 30% 8%;
28
+
29
+ --muted: 27 14% 16%;
30
+ --muted-foreground: 36 16% 65%;
31
+
32
+ --destructive: 6 78% 58%;
33
+ --destructive-foreground: 39 38% 93%;
34
+
35
+ --success: 84 30% 48%;
36
+ --success-foreground: 27 30% 8%;
37
+
38
+ --border: 27 14% 19%;
39
+ --input: 27 14% 19%;
40
+ --ring: 14 92% 58%;
41
+
42
+ --radius: 0.85rem;
43
+ }
44
+
45
+ /* .dark is identical to :root — this kit commits to one confident theme
46
+ rather than a light/dark toggle. Kept for component compatibility. */
47
+ .dark {
48
+ --background: 27 18% 8%;
49
+ --foreground: 39 38% 93%;
50
+ --card: 26 16% 11%;
51
+ --card-foreground: 39 38% 93%;
52
+ --primary: 14 92% 58%;
53
+ --primary-foreground: 27 30% 8%;
54
+ --secondary: 84 22% 52%;
55
+ --secondary-foreground: 27 30% 8%;
56
+ --accent: 38 78% 62%;
57
+ --accent-foreground: 27 30% 8%;
58
+ --muted: 27 14% 16%;
59
+ --muted-foreground: 36 16% 65%;
60
+ --destructive: 6 78% 58%;
61
+ --destructive-foreground: 39 38% 93%;
62
+ --success: 84 30% 48%;
63
+ --success-foreground: 27 30% 8%;
64
+ --border: 27 14% 19%;
65
+ --input: 27 14% 19%;
66
+ --ring: 14 92% 58%;
67
+ }
68
+ }
69
+
70
+ @layer base {
71
+ * {
72
+ @apply border-border;
73
+ }
74
+
75
+ html {
76
+ color-scheme: dark;
77
+ }
78
+
79
+ body {
80
+ @apply bg-background text-foreground font-sans;
81
+ font-feature-settings: "ss01", "ss02";
82
+ }
83
+
84
+ /* Grain overlay — gives the warm-ink background depth instead of flat color. */
85
+ body::before {
86
+ content: "";
87
+ position: fixed;
88
+ inset: 0;
89
+ z-index: 60;
90
+ pointer-events: none;
91
+ opacity: 0.05;
92
+ mix-blend-mode: overlay;
93
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
94
+ }
95
+
96
+ ::selection {
97
+ background-color: hsl(var(--primary) / 0.35);
98
+ color: hsl(var(--foreground));
99
+ }
100
+ }
101
+
102
+ @layer utilities {
103
+ .text-balance {
104
+ text-wrap: balance;
105
+ }
106
+
107
+ /* Eyebrow / label style used across the kit for mono uppercase tags. */
108
+ .label-mono {
109
+ font-family: var(--font-mono);
110
+ text-transform: uppercase;
111
+ letter-spacing: 0.14em;
112
+ font-size: 0.7rem;
113
+ font-weight: 500;
114
+ }
115
+
116
+ .glow-primary {
117
+ box-shadow: 0 0 0 1px hsl(var(--primary) / 0.25), 0 8px 30px -8px hsl(var(--primary) / 0.45);
118
+ }
119
+
120
+ .bg-grid {
121
+ background-image:
122
+ linear-gradient(to right, hsl(var(--border) / 0.4) 1px, transparent 1px),
123
+ linear-gradient(to bottom, hsl(var(--border) / 0.4) 1px, transparent 1px);
124
+ background-size: 56px 56px;
125
+ }
126
+
127
+ .animate-in-up {
128
+ animation: fade-up 0.7s cubic-bezier(0.16, 1, 0.3, 1) both;
129
+ }
130
+ }
131
+
132
+ @keyframes fade-up {
133
+ from {
134
+ opacity: 0;
135
+ transform: translateY(14px);
136
+ }
137
+ to {
138
+ opacity: 1;
139
+ transform: translateY(0);
140
+ }
141
+ }
@@ -0,0 +1,43 @@
1
+ import type { Metadata } from "next";
2
+ import { Fraunces, Work_Sans, JetBrains_Mono } from "next/font/google";
3
+ import "./globals.css";
4
+
5
+ const display = Fraunces({
6
+ subsets: ["latin"],
7
+ variable: "--font-display",
8
+ axes: ["opsz", "SOFT", "WONK"],
9
+ style: ["normal", "italic"],
10
+ });
11
+
12
+ const sans = Work_Sans({
13
+ subsets: ["latin"],
14
+ variable: "--font-sans",
15
+ weight: ["400", "500", "600", "700"],
16
+ });
17
+
18
+ const mono = JetBrains_Mono({
19
+ subsets: ["latin"],
20
+ variable: "--font-mono",
21
+ weight: ["400", "500"],
22
+ });
23
+
24
+ export const metadata: Metadata = {
25
+ title: "SHIP Starter Kit",
26
+ description:
27
+ "The SHIP Method OS starter kit — a working UI shell for the sale, member, and backoffice route groups.",
28
+ };
29
+
30
+ export default function RootLayout({
31
+ children,
32
+ }: {
33
+ children: React.ReactNode;
34
+ }) {
35
+ return (
36
+ <html
37
+ lang="en"
38
+ className={`dark ${display.variable} ${sans.variable} ${mono.variable}`}
39
+ >
40
+ <body className="font-sans antialiased">{children}</body>
41
+ </html>
42
+ );
43
+ }
@@ -0,0 +1,137 @@
1
+ "use client";
2
+
3
+ import { users } from "@/lib/mock-data";
4
+ import {
5
+ Card,
6
+ CardHeader,
7
+ CardTitle,
8
+ CardDescription,
9
+ CardContent,
10
+ CardFooter,
11
+ } from "@/components/ui/card";
12
+ import { Badge } from "@/components/ui/badge";
13
+ import { Button } from "@/components/ui/button";
14
+ import {
15
+ Table,
16
+ TableHeader,
17
+ TableBody,
18
+ TableRow,
19
+ TableHead,
20
+ TableCell,
21
+ } from "@/components/ui/table";
22
+
23
+ const mockUser = users[0];
24
+
25
+ interface MockInvoice {
26
+ date: string;
27
+ description: string;
28
+ amount: string;
29
+ status: "Paid" | "Pending" | "Failed";
30
+ }
31
+
32
+ const invoices: MockInvoice[] = [
33
+ {
34
+ date: "2026-06-01",
35
+ description: "Pro Plan — Monthly Subscription",
36
+ amount: "$49.00",
37
+ status: "Paid",
38
+ },
39
+ {
40
+ date: "2026-05-01",
41
+ description: "Pro Plan — Monthly Subscription",
42
+ amount: "$49.00",
43
+ status: "Paid",
44
+ },
45
+ {
46
+ date: "2026-04-01",
47
+ description: "Pro Plan — Monthly Subscription",
48
+ amount: "$49.00",
49
+ status: "Paid",
50
+ },
51
+ {
52
+ date: "2026-03-01",
53
+ description: "Pro Plan — Monthly Subscription",
54
+ amount: "$49.00",
55
+ status: "Failed",
56
+ },
57
+ ];
58
+
59
+ const statusVariant: Record<
60
+ MockInvoice["status"],
61
+ "default" | "secondary" | "outline" | "destructive"
62
+ > = {
63
+ Paid: "default",
64
+ Pending: "secondary",
65
+ Failed: "destructive",
66
+ };
67
+
68
+ export default function MemberBillingPage() {
69
+ return (
70
+ <div className="space-y-6">
71
+ <div>
72
+ <h2 className="font-display text-3xl font-medium tracking-tight">
73
+ Billing
74
+ </h2>
75
+ <p className="mt-1 text-muted-foreground">
76
+ Manage your subscription and view past invoices.
77
+ </p>
78
+ </div>
79
+
80
+ <Card className="animate-fade-up">
81
+ <CardHeader>
82
+ <span className="label-mono text-muted-foreground">Current Plan</span>
83
+ <CardTitle className="font-display text-3xl font-medium">
84
+ {mockUser.plan} Plan
85
+ </CardTitle>
86
+ </CardHeader>
87
+ <CardContent>
88
+ <p className="text-sm text-muted-foreground">
89
+ Status:{" "}
90
+ <span className="font-medium text-foreground">
91
+ {mockUser.status}
92
+ </span>{" "}
93
+ • Member since {mockUser.joinedAt}
94
+ </p>
95
+ </CardContent>
96
+ <CardFooter>
97
+ <Button>Manage Subscription</Button>
98
+ </CardFooter>
99
+ </Card>
100
+
101
+ <Card className="animate-fade-up" style={{ animationDelay: "80ms" }}>
102
+ <CardHeader>
103
+ <CardTitle className="text-base">Invoice History</CardTitle>
104
+ <CardDescription>
105
+ Your billing history for the past few months.
106
+ </CardDescription>
107
+ </CardHeader>
108
+ <CardContent>
109
+ <Table>
110
+ <TableHeader>
111
+ <TableRow>
112
+ <TableHead>Date</TableHead>
113
+ <TableHead>Description</TableHead>
114
+ <TableHead>Amount</TableHead>
115
+ <TableHead>Status</TableHead>
116
+ </TableRow>
117
+ </TableHeader>
118
+ <TableBody>
119
+ {invoices.map((invoice) => (
120
+ <TableRow key={invoice.date}>
121
+ <TableCell>{invoice.date}</TableCell>
122
+ <TableCell>{invoice.description}</TableCell>
123
+ <TableCell>{invoice.amount}</TableCell>
124
+ <TableCell>
125
+ <Badge variant={statusVariant[invoice.status]}>
126
+ {invoice.status}
127
+ </Badge>
128
+ </TableCell>
129
+ </TableRow>
130
+ ))}
131
+ </TableBody>
132
+ </Table>
133
+ </CardContent>
134
+ </Card>
135
+ </div>
136
+ );
137
+ }
@@ -0,0 +1,111 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+
5
+ import { members, type MockMemberContent } from "@/lib/mock-data";
6
+ import {
7
+ Card,
8
+ CardHeader,
9
+ CardTitle,
10
+ CardContent,
11
+ CardFooter,
12
+ } from "@/components/ui/card";
13
+ import { Badge } from "@/components/ui/badge";
14
+ import { Button } from "@/components/ui/button";
15
+ import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs";
16
+
17
+ const tiers = ["All", "Free", "Starter", "Pro", "Lifetime"] as const;
18
+
19
+ const tierBadgeVariant: Record<
20
+ string,
21
+ "default" | "secondary" | "outline" | "destructive"
22
+ > = {
23
+ Free: "outline",
24
+ Starter: "secondary",
25
+ Pro: "default",
26
+ Lifetime: "destructive",
27
+ };
28
+
29
+ function ContentGrid({ items }: { items: MockMemberContent[] }) {
30
+ if (items.length === 0) {
31
+ return (
32
+ <p className="py-12 text-center text-sm text-muted-foreground">
33
+ No courses in this tier yet.
34
+ </p>
35
+ );
36
+ }
37
+
38
+ return (
39
+ <div className="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3">
40
+ {items.map((item, idx) => (
41
+ <Card
42
+ key={item.id}
43
+ className="animate-fade-up group transition-all duration-200 hover:-translate-y-0.5 hover:border-primary/40 hover:shadow-[0_0_0_1px_hsl(var(--primary)/0.25),0_8px_30px_-8px_hsl(var(--primary)/0.45)]"
44
+ style={{ animationDelay: `${idx * 60}ms` }}
45
+ >
46
+ <CardHeader>
47
+ <div className="flex items-center justify-between gap-2">
48
+ <CardTitle className="text-base">{item.title}</CardTitle>
49
+ <Badge variant={tierBadgeVariant[item.tier] ?? "outline"}>
50
+ {item.tier}
51
+ </Badge>
52
+ </div>
53
+ </CardHeader>
54
+ <CardContent className="space-y-2">
55
+ <div className="h-2 w-full overflow-hidden rounded-full bg-muted">
56
+ <div
57
+ className="h-full rounded-full bg-primary transition-all"
58
+ style={{ width: `${item.progress}%` }}
59
+ />
60
+ </div>
61
+ <p className="label-mono text-muted-foreground">
62
+ {item.progress}% complete
63
+ </p>
64
+ </CardContent>
65
+ <CardFooter>
66
+ <Button size="sm" className="w-full">
67
+ Continue
68
+ </Button>
69
+ </CardFooter>
70
+ </Card>
71
+ ))}
72
+ </div>
73
+ );
74
+ }
75
+
76
+ export default function MemberContentPage() {
77
+ return (
78
+ <div className="space-y-6">
79
+ <div>
80
+ <h2 className="font-display text-3xl font-medium tracking-tight">
81
+ Courses
82
+ </h2>
83
+ <p className="mt-1 text-muted-foreground">
84
+ Browse everything available across your plan.
85
+ </p>
86
+ </div>
87
+
88
+ <Tabs defaultValue="All">
89
+ <TabsList>
90
+ {tiers.map((tier) => (
91
+ <TabsTrigger key={tier} value={tier}>
92
+ {tier}
93
+ </TabsTrigger>
94
+ ))}
95
+ </TabsList>
96
+
97
+ {tiers.map((tier) => (
98
+ <TabsContent key={tier} value={tier}>
99
+ <ContentGrid
100
+ items={
101
+ tier === "All"
102
+ ? members
103
+ : members.filter((item) => item.tier === tier)
104
+ }
105
+ />
106
+ </TabsContent>
107
+ ))}
108
+ </Tabs>
109
+ </div>
110
+ );
111
+ }
@@ -0,0 +1,129 @@
1
+ "use client";
2
+
3
+ import { useRouter } from "next/navigation";
4
+ import { TrendingUp, TrendingDown } from "lucide-react";
5
+
6
+ import { users, members, metrics } from "@/lib/mock-data";
7
+ import {
8
+ Card,
9
+ CardHeader,
10
+ CardTitle,
11
+ CardDescription,
12
+ CardContent,
13
+ CardFooter,
14
+ } from "@/components/ui/card";
15
+ import { Badge } from "@/components/ui/badge";
16
+ import { Button } from "@/components/ui/button";
17
+
18
+ const mockUser = users[0];
19
+
20
+ const tierBadgeVariant: Record<
21
+ string,
22
+ "default" | "secondary" | "outline" | "destructive"
23
+ > = {
24
+ Free: "outline",
25
+ Starter: "secondary",
26
+ Pro: "default",
27
+ Lifetime: "destructive",
28
+ };
29
+
30
+ export default function MemberDashboardPage() {
31
+ const router = useRouter();
32
+
33
+ return (
34
+ <div className="space-y-10">
35
+ <div className="animate-fade-up">
36
+ <h2 className="font-display text-3xl font-medium tracking-tight">
37
+ Welcome back, {mockUser.name.split(" ")[0]}
38
+ </h2>
39
+ <p className="mt-1 text-muted-foreground">
40
+ Here&apos;s what&apos;s happening with your account today.
41
+ </p>
42
+ </div>
43
+
44
+ <div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4">
45
+ {metrics.slice(0, 4).map((metric, idx) => {
46
+ const positive = metric.change >= 0;
47
+ return (
48
+ <Card
49
+ key={metric.label}
50
+ className="animate-fade-up"
51
+ style={{ animationDelay: `${idx * 80}ms` }}
52
+ >
53
+ <CardHeader className="pb-2">
54
+ <span className="label-mono text-muted-foreground">
55
+ {metric.label}
56
+ </span>
57
+ <CardTitle className="font-display text-3xl font-medium">
58
+ {metric.value}
59
+ </CardTitle>
60
+ </CardHeader>
61
+ <CardContent>
62
+ <div
63
+ className={
64
+ "inline-flex items-center gap-1 rounded-full px-2 py-0.5 text-xs font-medium " +
65
+ (positive
66
+ ? "bg-success/15 text-success"
67
+ : "bg-destructive/15 text-destructive")
68
+ }
69
+ >
70
+ {positive ? (
71
+ <TrendingUp className="h-3.5 w-3.5" />
72
+ ) : (
73
+ <TrendingDown className="h-3.5 w-3.5" />
74
+ )}
75
+ {positive ? "+" : ""}
76
+ {metric.change}%
77
+ </div>
78
+ </CardContent>
79
+ </Card>
80
+ );
81
+ })}
82
+ </div>
83
+
84
+ <div>
85
+ <h3 className="label-mono mb-4 text-muted-foreground">
86
+ Continue Learning
87
+ </h3>
88
+ <div className="grid grid-cols-1 gap-4 md:grid-cols-2">
89
+ {members.map((item, idx) => (
90
+ <Card
91
+ key={item.id}
92
+ className="animate-fade-up group transition-all duration-200 hover:-translate-y-0.5 hover:border-primary/40"
93
+ style={{ animationDelay: `${(idx + 4) * 80}ms` }}
94
+ >
95
+ <CardHeader>
96
+ <div className="flex items-center justify-between gap-2">
97
+ <CardTitle className="text-base">{item.title}</CardTitle>
98
+ <Badge variant={tierBadgeVariant[item.tier] ?? "outline"}>
99
+ {item.tier}
100
+ </Badge>
101
+ </div>
102
+ </CardHeader>
103
+ <CardContent className="space-y-2">
104
+ <div className="h-2 w-full overflow-hidden rounded-full bg-muted">
105
+ <div
106
+ className="h-full rounded-full bg-primary transition-all"
107
+ style={{ width: `${item.progress}%` }}
108
+ />
109
+ </div>
110
+ <p className="label-mono text-muted-foreground">
111
+ {item.progress}% complete
112
+ </p>
113
+ </CardContent>
114
+ <CardFooter>
115
+ <Button
116
+ size="sm"
117
+ className="w-full"
118
+ onClick={() => router.push("/member/content")}
119
+ >
120
+ Continue
121
+ </Button>
122
+ </CardFooter>
123
+ </Card>
124
+ ))}
125
+ </div>
126
+ </div>
127
+ </div>
128
+ );
129
+ }