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,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's what'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
|
+
}
|