@contractspec/example.saas-boilerplate 3.8.9 → 3.8.11
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/.turbo/turbo-build.log +155 -155
- package/CHANGELOG.md +34 -0
- package/dist/billing/billing.entity.js +1 -113
- package/dist/billing/billing.enum.js +1 -19
- package/dist/billing/billing.event.js +1 -90
- package/dist/billing/billing.handler.js +1 -148
- package/dist/billing/billing.operations.js +1 -278
- package/dist/billing/billing.presentation.js +1 -55
- package/dist/billing/billing.schema.js +1 -121
- package/dist/billing/index.js +1 -691
- package/dist/browser/billing/billing.entity.js +1 -113
- package/dist/browser/billing/billing.enum.js +1 -19
- package/dist/browser/billing/billing.event.js +1 -90
- package/dist/browser/billing/billing.handler.js +1 -148
- package/dist/browser/billing/billing.operations.js +1 -278
- package/dist/browser/billing/billing.presentation.js +1 -55
- package/dist/browser/billing/billing.schema.js +1 -121
- package/dist/browser/billing/index.js +1 -691
- package/dist/browser/dashboard/dashboard.presentation.js +1 -55
- package/dist/browser/dashboard/index.js +1 -55
- package/dist/browser/docs/index.js +5 -49
- package/dist/browser/docs/saas-boilerplate.docblock.js +5 -49
- package/dist/browser/example.js +1 -39
- package/dist/browser/handlers/index.js +2 -358
- package/dist/browser/handlers/saas.handlers.js +2 -134
- package/dist/browser/index.js +9 -3591
- package/dist/browser/presentations/index.js +1 -299
- package/dist/browser/project/index.js +1 -793
- package/dist/browser/project/project.entity.js +1 -77
- package/dist/browser/project/project.enum.js +1 -18
- package/dist/browser/project/project.event.js +1 -103
- package/dist/browser/project/project.handler.js +1 -178
- package/dist/browser/project/project.operations.js +1 -372
- package/dist/browser/project/project.presentation.js +1 -180
- package/dist/browser/project/project.schema.js +1 -134
- package/dist/browser/saas-boilerplate.feature.js +1 -304
- package/dist/browser/seeders/index.js +2 -20
- package/dist/browser/settings/index.js +1 -75
- package/dist/browser/settings/settings.entity.js +1 -74
- package/dist/browser/settings/settings.enum.js +1 -11
- package/dist/browser/shared/mock-data.js +1 -104
- package/dist/browser/tests/operations.test-spec.js +1 -112
- package/dist/browser/ui/SaasDashboard.js +1 -1239
- package/dist/browser/ui/SaasDashboard.visualizations.js +1 -249
- package/dist/browser/ui/SaasProjectList.js +1 -162
- package/dist/browser/ui/SaasSettingsPanel.js +1 -145
- package/dist/browser/ui/hooks/index.js +1 -159
- package/dist/browser/ui/hooks/useProjectList.js +1 -66
- package/dist/browser/ui/hooks/useProjectMutations.js +1 -91
- package/dist/browser/ui/index.js +5 -2077
- package/dist/browser/ui/modals/CreateProjectModal.js +1 -153
- package/dist/browser/ui/modals/ProjectActionsModal.js +1 -335
- package/dist/browser/ui/modals/index.js +1 -487
- package/dist/browser/ui/overlays/demo-overlays.js +1 -61
- package/dist/browser/ui/overlays/index.js +1 -61
- package/dist/browser/ui/renderers/index.js +5 -901
- package/dist/browser/ui/renderers/project-list.markdown.js +5 -725
- package/dist/browser/ui/renderers/project-list.renderer.js +1 -177
- package/dist/browser/visualizations/catalog.js +1 -155
- package/dist/browser/visualizations/index.js +1 -217
- package/dist/browser/visualizations/selectors.js +1 -210
- package/dist/dashboard/dashboard.presentation.js +1 -55
- package/dist/dashboard/index.js +1 -55
- package/dist/docs/index.js +5 -49
- package/dist/docs/saas-boilerplate.docblock.js +5 -49
- package/dist/example.js +1 -39
- package/dist/handlers/index.js +2 -358
- package/dist/handlers/saas.handlers.js +2 -134
- package/dist/index.js +9 -3591
- package/dist/node/billing/billing.entity.js +1 -113
- package/dist/node/billing/billing.enum.js +1 -19
- package/dist/node/billing/billing.event.js +1 -90
- package/dist/node/billing/billing.handler.js +1 -148
- package/dist/node/billing/billing.operations.js +1 -278
- package/dist/node/billing/billing.presentation.js +1 -55
- package/dist/node/billing/billing.schema.js +1 -121
- package/dist/node/billing/index.js +1 -691
- package/dist/node/dashboard/dashboard.presentation.js +1 -55
- package/dist/node/dashboard/index.js +1 -55
- package/dist/node/docs/index.js +5 -49
- package/dist/node/docs/saas-boilerplate.docblock.js +5 -49
- package/dist/node/example.js +1 -39
- package/dist/node/handlers/index.js +2 -358
- package/dist/node/handlers/saas.handlers.js +2 -134
- package/dist/node/index.js +9 -3591
- package/dist/node/presentations/index.js +1 -299
- package/dist/node/project/index.js +1 -793
- package/dist/node/project/project.entity.js +1 -77
- package/dist/node/project/project.enum.js +1 -18
- package/dist/node/project/project.event.js +1 -103
- package/dist/node/project/project.handler.js +1 -178
- package/dist/node/project/project.operations.js +1 -372
- package/dist/node/project/project.presentation.js +1 -180
- package/dist/node/project/project.schema.js +1 -134
- package/dist/node/saas-boilerplate.feature.js +1 -304
- package/dist/node/seeders/index.js +2 -20
- package/dist/node/settings/index.js +1 -75
- package/dist/node/settings/settings.entity.js +1 -74
- package/dist/node/settings/settings.enum.js +1 -11
- package/dist/node/shared/mock-data.js +1 -104
- package/dist/node/tests/operations.test-spec.js +1 -112
- package/dist/node/ui/SaasDashboard.js +1 -1239
- package/dist/node/ui/SaasDashboard.visualizations.js +1 -249
- package/dist/node/ui/SaasProjectList.js +1 -162
- package/dist/node/ui/SaasSettingsPanel.js +1 -145
- package/dist/node/ui/hooks/index.js +1 -159
- package/dist/node/ui/hooks/useProjectList.js +1 -66
- package/dist/node/ui/hooks/useProjectMutations.js +1 -91
- package/dist/node/ui/index.js +5 -2077
- package/dist/node/ui/modals/CreateProjectModal.js +1 -153
- package/dist/node/ui/modals/ProjectActionsModal.js +1 -335
- package/dist/node/ui/modals/index.js +1 -487
- package/dist/node/ui/overlays/demo-overlays.js +1 -61
- package/dist/node/ui/overlays/index.js +1 -61
- package/dist/node/ui/renderers/index.js +5 -901
- package/dist/node/ui/renderers/project-list.markdown.js +5 -725
- package/dist/node/ui/renderers/project-list.renderer.js +1 -177
- package/dist/node/visualizations/catalog.js +1 -155
- package/dist/node/visualizations/index.js +1 -217
- package/dist/node/visualizations/selectors.js +1 -210
- package/dist/presentations/index.js +1 -299
- package/dist/project/index.js +1 -793
- package/dist/project/project.entity.js +1 -77
- package/dist/project/project.enum.js +1 -18
- package/dist/project/project.event.js +1 -103
- package/dist/project/project.handler.js +1 -178
- package/dist/project/project.operations.js +1 -372
- package/dist/project/project.presentation.js +1 -180
- package/dist/project/project.schema.js +1 -134
- package/dist/saas-boilerplate.feature.js +1 -304
- package/dist/seeders/index.js +2 -20
- package/dist/settings/index.js +1 -75
- package/dist/settings/settings.entity.js +1 -74
- package/dist/settings/settings.enum.js +1 -11
- package/dist/shared/mock-data.js +1 -104
- package/dist/tests/operations.test-spec.js +1 -112
- package/dist/ui/SaasDashboard.js +1 -1239
- package/dist/ui/SaasDashboard.visualizations.js +1 -249
- package/dist/ui/SaasProjectList.js +1 -162
- package/dist/ui/SaasSettingsPanel.js +1 -145
- package/dist/ui/hooks/index.js +1 -159
- package/dist/ui/hooks/useProjectList.js +1 -66
- package/dist/ui/hooks/useProjectMutations.js +1 -91
- package/dist/ui/index.js +5 -2077
- package/dist/ui/modals/CreateProjectModal.js +1 -153
- package/dist/ui/modals/ProjectActionsModal.js +1 -335
- package/dist/ui/modals/index.js +1 -487
- package/dist/ui/overlays/demo-overlays.js +1 -61
- package/dist/ui/overlays/index.js +1 -61
- package/dist/ui/renderers/index.js +5 -901
- package/dist/ui/renderers/project-list.markdown.js +5 -725
- package/dist/ui/renderers/project-list.renderer.js +1 -177
- package/dist/visualizations/catalog.js +1 -155
- package/dist/visualizations/index.js +1 -217
- package/dist/visualizations/selectors.js +1 -210
- package/package.json +12 -12
|
@@ -1,902 +1,6 @@
|
|
|
1
1
|
// @bun
|
|
2
|
-
|
|
3
|
-
var
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
description: "Main company website redesign project",
|
|
8
|
-
slug: "marketing-website",
|
|
9
|
-
organizationId: "demo-org",
|
|
10
|
-
createdBy: "user-1",
|
|
11
|
-
status: "ACTIVE",
|
|
12
|
-
isPublic: false,
|
|
13
|
-
tags: ["marketing", "website", "redesign"],
|
|
14
|
-
createdAt: new Date("2024-01-15T10:00:00Z"),
|
|
15
|
-
updatedAt: new Date("2024-03-20T14:30:00Z")
|
|
16
|
-
},
|
|
17
|
-
{
|
|
18
|
-
id: "proj-2",
|
|
19
|
-
name: "Mobile App v2",
|
|
20
|
-
description: "Next generation mobile application",
|
|
21
|
-
slug: "mobile-app-v2",
|
|
22
|
-
organizationId: "demo-org",
|
|
23
|
-
createdBy: "user-2",
|
|
24
|
-
status: "ACTIVE",
|
|
25
|
-
isPublic: false,
|
|
26
|
-
tags: ["mobile", "app", "v2"],
|
|
27
|
-
createdAt: new Date("2024-02-01T09:00:00Z"),
|
|
28
|
-
updatedAt: new Date("2024-04-05T11:15:00Z")
|
|
29
|
-
},
|
|
30
|
-
{
|
|
31
|
-
id: "proj-3",
|
|
32
|
-
name: "API Integration",
|
|
33
|
-
description: "Third-party API integration project",
|
|
34
|
-
slug: "api-integration",
|
|
35
|
-
organizationId: "demo-org",
|
|
36
|
-
createdBy: "user-1",
|
|
37
|
-
status: "DRAFT",
|
|
38
|
-
isPublic: false,
|
|
39
|
-
tags: ["api", "integration"],
|
|
40
|
-
createdAt: new Date("2024-03-10T08:00:00Z"),
|
|
41
|
-
updatedAt: new Date("2024-03-10T08:00:00Z")
|
|
42
|
-
},
|
|
43
|
-
{
|
|
44
|
-
id: "proj-4",
|
|
45
|
-
name: "Analytics Dashboard",
|
|
46
|
-
description: "Internal analytics and reporting dashboard",
|
|
47
|
-
slug: "analytics-dashboard",
|
|
48
|
-
organizationId: "demo-org",
|
|
49
|
-
createdBy: "user-3",
|
|
50
|
-
status: "ARCHIVED",
|
|
51
|
-
isPublic: true,
|
|
52
|
-
tags: ["analytics", "dashboard", "reporting"],
|
|
53
|
-
createdAt: new Date("2023-10-01T12:00:00Z"),
|
|
54
|
-
updatedAt: new Date("2024-02-28T16:45:00Z")
|
|
55
|
-
}
|
|
56
|
-
];
|
|
57
|
-
var MOCK_SUBSCRIPTION = {
|
|
58
|
-
id: "sub-1",
|
|
59
|
-
organizationId: "demo-org",
|
|
60
|
-
planId: "pro",
|
|
61
|
-
planName: "Professional",
|
|
62
|
-
status: "ACTIVE",
|
|
63
|
-
currentPeriodStart: new Date("2024-04-01T00:00:00Z"),
|
|
64
|
-
currentPeriodEnd: new Date("2024-05-01T00:00:00Z"),
|
|
65
|
-
limits: {
|
|
66
|
-
projects: 25,
|
|
67
|
-
users: 10,
|
|
68
|
-
storage: 50,
|
|
69
|
-
apiCalls: 1e5
|
|
70
|
-
},
|
|
71
|
-
usage: {
|
|
72
|
-
projects: 4,
|
|
73
|
-
users: 5,
|
|
74
|
-
storage: 12.5,
|
|
75
|
-
apiCalls: 45230
|
|
76
|
-
}
|
|
77
|
-
};
|
|
78
|
-
var MOCK_USAGE_SUMMARY = {
|
|
79
|
-
organizationId: "demo-org",
|
|
80
|
-
period: "current_month",
|
|
81
|
-
apiCalls: {
|
|
82
|
-
total: 45230,
|
|
83
|
-
limit: 1e5,
|
|
84
|
-
percentUsed: 45.23
|
|
85
|
-
},
|
|
86
|
-
storage: {
|
|
87
|
-
totalGb: 12.5,
|
|
88
|
-
limitGb: 50,
|
|
89
|
-
percentUsed: 25
|
|
90
|
-
},
|
|
91
|
-
activeProjects: 4,
|
|
92
|
-
activeUsers: 5,
|
|
93
|
-
breakdown: [
|
|
94
|
-
{ date: "2024-04-01", apiCalls: 3200, storageGb: 12.1 },
|
|
95
|
-
{ date: "2024-04-02", apiCalls: 2800, storageGb: 12.2 },
|
|
96
|
-
{ date: "2024-04-03", apiCalls: 4100, storageGb: 12.3 },
|
|
97
|
-
{ date: "2024-04-04", apiCalls: 3600, storageGb: 12.4 },
|
|
98
|
-
{ date: "2024-04-05", apiCalls: 3800, storageGb: 12.5 }
|
|
99
|
-
]
|
|
100
|
-
};
|
|
101
|
-
|
|
102
|
-
// src/billing/billing.handler.ts
|
|
103
|
-
async function mockGetSubscriptionHandler() {
|
|
104
|
-
return MOCK_SUBSCRIPTION;
|
|
105
|
-
}
|
|
106
|
-
async function mockGetUsageSummaryHandler(input) {
|
|
107
|
-
return {
|
|
108
|
-
...MOCK_USAGE_SUMMARY,
|
|
109
|
-
period: input.period ?? "current_month"
|
|
110
|
-
};
|
|
111
|
-
}
|
|
112
|
-
async function mockRecordUsageHandler(input) {
|
|
113
|
-
const currentUsage = MOCK_USAGE_SUMMARY.apiCalls.total;
|
|
114
|
-
const newTotal = currentUsage + input.quantity;
|
|
115
|
-
return {
|
|
116
|
-
recorded: true,
|
|
117
|
-
newTotal
|
|
118
|
-
};
|
|
119
|
-
}
|
|
120
|
-
async function mockCheckFeatureAccessHandler(input) {
|
|
121
|
-
const { feature } = input;
|
|
122
|
-
const featureMap = {
|
|
123
|
-
custom_domains: {
|
|
124
|
-
allowed: true
|
|
125
|
-
},
|
|
126
|
-
api_access: {
|
|
127
|
-
allowed: true,
|
|
128
|
-
currentUsage: MOCK_USAGE_SUMMARY.apiCalls.total,
|
|
129
|
-
limit: MOCK_USAGE_SUMMARY.apiCalls.limit
|
|
130
|
-
},
|
|
131
|
-
advanced_analytics: {
|
|
132
|
-
allowed: false,
|
|
133
|
-
reason: "FEATURE_NOT_INCLUDED"
|
|
134
|
-
},
|
|
135
|
-
unlimited_projects: {
|
|
136
|
-
allowed: false,
|
|
137
|
-
reason: "PLAN_LIMIT",
|
|
138
|
-
currentUsage: MOCK_SUBSCRIPTION.usage.projects,
|
|
139
|
-
limit: MOCK_SUBSCRIPTION.limits.projects
|
|
140
|
-
}
|
|
141
|
-
};
|
|
142
|
-
return featureMap[feature] ?? { allowed: true };
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
// src/project/project.handler.ts
|
|
146
|
-
async function mockListProjectsHandler(input) {
|
|
147
|
-
const { status, search, limit = 20, offset = 0 } = input;
|
|
148
|
-
let filtered = [...MOCK_PROJECTS];
|
|
149
|
-
if (status && status !== "all") {
|
|
150
|
-
filtered = filtered.filter((p) => p.status === status);
|
|
151
|
-
}
|
|
152
|
-
if (search) {
|
|
153
|
-
const q = search.toLowerCase();
|
|
154
|
-
filtered = filtered.filter((p) => p.name.toLowerCase().includes(q) || p.description?.toLowerCase().includes(q) || p.tags.some((t) => t.toLowerCase().includes(q)));
|
|
155
|
-
}
|
|
156
|
-
filtered.sort((a, b) => b.updatedAt.getTime() - a.updatedAt.getTime());
|
|
157
|
-
const total = filtered.length;
|
|
158
|
-
const projects = filtered.slice(offset, offset + limit);
|
|
159
|
-
return {
|
|
160
|
-
projects,
|
|
161
|
-
total
|
|
162
|
-
};
|
|
163
|
-
}
|
|
164
|
-
async function mockGetProjectHandler(input) {
|
|
165
|
-
const project = MOCK_PROJECTS.find((p) => p.id === input.projectId);
|
|
166
|
-
if (!project) {
|
|
167
|
-
throw new Error("NOT_FOUND");
|
|
168
|
-
}
|
|
169
|
-
return project;
|
|
170
|
-
}
|
|
171
|
-
async function mockCreateProjectHandler(input, context) {
|
|
172
|
-
if (input.slug) {
|
|
173
|
-
const exists = MOCK_PROJECTS.some((p) => p.slug === input.slug);
|
|
174
|
-
if (exists) {
|
|
175
|
-
throw new Error("SLUG_EXISTS");
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
const now = new Date;
|
|
179
|
-
return {
|
|
180
|
-
id: `proj-${Date.now()}`,
|
|
181
|
-
name: input.name,
|
|
182
|
-
description: input.description,
|
|
183
|
-
slug: input.slug ?? input.name.toLowerCase().replace(/\s+/g, "-"),
|
|
184
|
-
organizationId: context.organizationId,
|
|
185
|
-
createdBy: context.userId,
|
|
186
|
-
status: "DRAFT",
|
|
187
|
-
isPublic: input.isPublic ?? false,
|
|
188
|
-
tags: input.tags ?? [],
|
|
189
|
-
createdAt: now,
|
|
190
|
-
updatedAt: now
|
|
191
|
-
};
|
|
192
|
-
}
|
|
193
|
-
async function mockUpdateProjectHandler(input) {
|
|
194
|
-
const project = MOCK_PROJECTS.find((p) => p.id === input.projectId);
|
|
195
|
-
if (!project) {
|
|
196
|
-
throw new Error("NOT_FOUND");
|
|
197
|
-
}
|
|
198
|
-
return {
|
|
199
|
-
...project,
|
|
200
|
-
name: input.name ?? project.name,
|
|
201
|
-
description: input.description ?? project.description,
|
|
202
|
-
slug: input.slug ?? project.slug,
|
|
203
|
-
isPublic: input.isPublic ?? project.isPublic,
|
|
204
|
-
tags: input.tags ?? project.tags,
|
|
205
|
-
status: input.status ?? project.status,
|
|
206
|
-
updatedAt: new Date
|
|
207
|
-
};
|
|
208
|
-
}
|
|
209
|
-
async function mockDeleteProjectHandler(input) {
|
|
210
|
-
const project = MOCK_PROJECTS.find((p) => p.id === input.projectId);
|
|
211
|
-
if (!project) {
|
|
212
|
-
throw new Error("NOT_FOUND");
|
|
213
|
-
}
|
|
214
|
-
return { success: true };
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
// src/handlers/saas.handlers.ts
|
|
218
|
-
import { web } from "@contractspec/lib.runtime-sandbox";
|
|
219
|
-
var { generateId } = web;
|
|
220
|
-
function rowToProject(row) {
|
|
221
|
-
return {
|
|
222
|
-
id: row.id,
|
|
223
|
-
projectId: row.projectId,
|
|
224
|
-
organizationId: row.organizationId,
|
|
225
|
-
name: row.name,
|
|
226
|
-
description: row.description ?? undefined,
|
|
227
|
-
status: row.status,
|
|
228
|
-
tier: row.tier,
|
|
229
|
-
createdAt: new Date(row.createdAt),
|
|
230
|
-
updatedAt: new Date(row.updatedAt)
|
|
231
|
-
};
|
|
232
|
-
}
|
|
233
|
-
function rowToSubscription(row) {
|
|
234
|
-
return {
|
|
235
|
-
id: row.id,
|
|
236
|
-
projectId: row.projectId,
|
|
237
|
-
organizationId: row.organizationId,
|
|
238
|
-
plan: row.plan,
|
|
239
|
-
status: row.status,
|
|
240
|
-
billingCycle: row.billingCycle,
|
|
241
|
-
currentPeriodStart: new Date(row.currentPeriodStart),
|
|
242
|
-
currentPeriodEnd: new Date(row.currentPeriodEnd),
|
|
243
|
-
cancelAtPeriodEnd: Boolean(row.cancelAtPeriodEnd)
|
|
244
|
-
};
|
|
245
|
-
}
|
|
246
|
-
function createSaasHandlers(db) {
|
|
247
|
-
async function listProjects(input) {
|
|
248
|
-
const {
|
|
249
|
-
projectId,
|
|
250
|
-
organizationId,
|
|
251
|
-
status,
|
|
252
|
-
search,
|
|
253
|
-
limit = 20,
|
|
254
|
-
offset = 0
|
|
255
|
-
} = input;
|
|
256
|
-
let whereClause = "WHERE projectId = ?";
|
|
257
|
-
const params = [projectId];
|
|
258
|
-
if (organizationId) {
|
|
259
|
-
whereClause += " AND organizationId = ?";
|
|
260
|
-
params.push(organizationId);
|
|
261
|
-
}
|
|
262
|
-
if (status && status !== "all") {
|
|
263
|
-
whereClause += " AND status = ?";
|
|
264
|
-
params.push(status);
|
|
265
|
-
}
|
|
266
|
-
if (search) {
|
|
267
|
-
whereClause += " AND (name LIKE ? OR description LIKE ?)";
|
|
268
|
-
params.push(`%${search}%`, `%${search}%`);
|
|
269
|
-
}
|
|
270
|
-
const countResult = (await db.query(`SELECT COUNT(*) as count FROM saas_project ${whereClause}`, params)).rows;
|
|
271
|
-
const total = countResult[0]?.count ?? 0;
|
|
272
|
-
const rows = (await db.query(`SELECT * FROM saas_project ${whereClause} ORDER BY createdAt DESC LIMIT ? OFFSET ?`, [...params, limit, offset])).rows;
|
|
273
|
-
return {
|
|
274
|
-
items: rows.map(rowToProject),
|
|
275
|
-
total,
|
|
276
|
-
hasMore: offset + rows.length < total
|
|
277
|
-
};
|
|
278
|
-
}
|
|
279
|
-
async function getProject(id) {
|
|
280
|
-
const rows = (await db.query(`SELECT * FROM saas_project WHERE id = ?`, [id])).rows;
|
|
281
|
-
return rows[0] ? rowToProject(rows[0]) : null;
|
|
282
|
-
}
|
|
283
|
-
async function createProject(input, context) {
|
|
284
|
-
const id = generateId("proj");
|
|
285
|
-
const now = new Date().toISOString();
|
|
286
|
-
await db.execute(`INSERT INTO saas_project (id, projectId, organizationId, name, description, status, tier, createdAt, updatedAt)
|
|
287
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
288
|
-
id,
|
|
289
|
-
context.projectId,
|
|
290
|
-
context.organizationId,
|
|
291
|
-
input.name,
|
|
292
|
-
input.description ?? null,
|
|
293
|
-
"DRAFT",
|
|
294
|
-
input.tier ?? "FREE",
|
|
295
|
-
now,
|
|
296
|
-
now
|
|
297
|
-
]);
|
|
298
|
-
const rows = (await db.query(`SELECT * FROM saas_project WHERE id = ?`, [id])).rows;
|
|
299
|
-
return rowToProject(rows[0]);
|
|
300
|
-
}
|
|
301
|
-
async function updateProject(input) {
|
|
302
|
-
const now = new Date().toISOString();
|
|
303
|
-
const updates = ["updatedAt = ?"];
|
|
304
|
-
const params = [now];
|
|
305
|
-
if (input.name !== undefined) {
|
|
306
|
-
updates.push("name = ?");
|
|
307
|
-
params.push(input.name);
|
|
308
|
-
}
|
|
309
|
-
if (input.description !== undefined) {
|
|
310
|
-
updates.push("description = ?");
|
|
311
|
-
params.push(input.description);
|
|
312
|
-
}
|
|
313
|
-
if (input.status !== undefined) {
|
|
314
|
-
updates.push("status = ?");
|
|
315
|
-
params.push(input.status);
|
|
316
|
-
}
|
|
317
|
-
params.push(input.id);
|
|
318
|
-
await db.execute(`UPDATE saas_project SET ${updates.join(", ")} WHERE id = ?`, params);
|
|
319
|
-
const rows = (await db.query(`SELECT * FROM saas_project WHERE id = ?`, [input.id])).rows;
|
|
320
|
-
if (!rows[0]) {
|
|
321
|
-
throw new Error("NOT_FOUND");
|
|
322
|
-
}
|
|
323
|
-
return rowToProject(rows[0]);
|
|
324
|
-
}
|
|
325
|
-
async function deleteProject(id) {
|
|
326
|
-
await db.execute(`DELETE FROM saas_project WHERE id = ?`, [id]);
|
|
327
|
-
}
|
|
328
|
-
async function getSubscription(input) {
|
|
329
|
-
let query = `SELECT * FROM saas_subscription WHERE projectId = ?`;
|
|
330
|
-
const params = [input.projectId];
|
|
331
|
-
if (input.organizationId) {
|
|
332
|
-
query += " AND organizationId = ?";
|
|
333
|
-
params.push(input.organizationId);
|
|
334
|
-
}
|
|
335
|
-
query += " LIMIT 1";
|
|
336
|
-
const rows = (await db.query(query, params)).rows;
|
|
337
|
-
return rows[0] ? rowToSubscription(rows[0]) : null;
|
|
338
|
-
}
|
|
339
|
-
return {
|
|
340
|
-
listProjects,
|
|
341
|
-
getProject,
|
|
342
|
-
createProject,
|
|
343
|
-
updateProject,
|
|
344
|
-
deleteProject,
|
|
345
|
-
getSubscription
|
|
346
|
-
};
|
|
347
|
-
}
|
|
348
|
-
// src/visualizations/catalog.ts
|
|
349
|
-
import {
|
|
350
|
-
defineVisualization,
|
|
351
|
-
VisualizationRegistry
|
|
352
|
-
} from "@contractspec/lib.contracts-spec/visualizations";
|
|
353
|
-
var PROJECT_LIST_REF = {
|
|
354
|
-
key: "saas.project.list",
|
|
355
|
-
version: "1.0.0"
|
|
356
|
-
};
|
|
357
|
-
var META = {
|
|
358
|
-
version: "1.0.0",
|
|
359
|
-
domain: "saas",
|
|
360
|
-
stability: "experimental",
|
|
361
|
-
owners: ["@example.saas-boilerplate"],
|
|
362
|
-
tags: ["saas", "visualization", "projects"]
|
|
363
|
-
};
|
|
364
|
-
var SaasProjectUsageVisualization = defineVisualization({
|
|
365
|
-
meta: {
|
|
366
|
-
...META,
|
|
367
|
-
key: "saas-boilerplate.visualization.project-usage",
|
|
368
|
-
title: "Project Capacity",
|
|
369
|
-
description: "Current project count against the current plan limit.",
|
|
370
|
-
goal: "Show usage against the active plan allowance.",
|
|
371
|
-
context: "SaaS account overview."
|
|
372
|
-
},
|
|
373
|
-
source: { primary: PROJECT_LIST_REF, resultPath: "data" },
|
|
374
|
-
visualization: {
|
|
375
|
-
kind: "metric",
|
|
376
|
-
measure: "totalProjects",
|
|
377
|
-
comparisonMeasure: "projectLimit",
|
|
378
|
-
measures: [
|
|
379
|
-
{
|
|
380
|
-
key: "totalProjects",
|
|
381
|
-
label: "Projects",
|
|
382
|
-
dataPath: "totalProjects",
|
|
383
|
-
format: "number"
|
|
384
|
-
},
|
|
385
|
-
{
|
|
386
|
-
key: "projectLimit",
|
|
387
|
-
label: "Plan Limit",
|
|
388
|
-
dataPath: "projectLimit",
|
|
389
|
-
format: "number"
|
|
390
|
-
}
|
|
391
|
-
],
|
|
392
|
-
table: { caption: "Current project count and plan limit." }
|
|
393
|
-
}
|
|
394
|
-
});
|
|
395
|
-
var SaasProjectStatusVisualization = defineVisualization({
|
|
396
|
-
meta: {
|
|
397
|
-
...META,
|
|
398
|
-
key: "saas-boilerplate.visualization.project-status",
|
|
399
|
-
title: "Project Status",
|
|
400
|
-
description: "Distribution of project states.",
|
|
401
|
-
goal: "Show the mix of active, draft, and archived projects.",
|
|
402
|
-
context: "Project portfolio overview."
|
|
403
|
-
},
|
|
404
|
-
source: { primary: PROJECT_LIST_REF, resultPath: "data" },
|
|
405
|
-
visualization: {
|
|
406
|
-
kind: "pie",
|
|
407
|
-
nameDimension: "status",
|
|
408
|
-
valueMeasure: "projects",
|
|
409
|
-
dimensions: [
|
|
410
|
-
{ key: "status", label: "Status", dataPath: "status", type: "category" }
|
|
411
|
-
],
|
|
412
|
-
measures: [
|
|
413
|
-
{
|
|
414
|
-
key: "projects",
|
|
415
|
-
label: "Projects",
|
|
416
|
-
dataPath: "projects",
|
|
417
|
-
format: "number"
|
|
418
|
-
}
|
|
419
|
-
],
|
|
420
|
-
table: { caption: "Project counts by status." }
|
|
421
|
-
}
|
|
422
|
-
});
|
|
423
|
-
var SaasProjectTierVisualization = defineVisualization({
|
|
424
|
-
meta: {
|
|
425
|
-
...META,
|
|
426
|
-
key: "saas-boilerplate.visualization.project-tiers",
|
|
427
|
-
title: "Tier Comparison",
|
|
428
|
-
description: "Distribution of projects across tiers.",
|
|
429
|
-
goal: "Compare how the current portfolio is distributed by tier.",
|
|
430
|
-
context: "Plan and packaging overview."
|
|
431
|
-
},
|
|
432
|
-
source: { primary: PROJECT_LIST_REF, resultPath: "data" },
|
|
433
|
-
visualization: {
|
|
434
|
-
kind: "cartesian",
|
|
435
|
-
variant: "bar",
|
|
436
|
-
xDimension: "tier",
|
|
437
|
-
yMeasures: ["projects"],
|
|
438
|
-
dimensions: [
|
|
439
|
-
{ key: "tier", label: "Tier", dataPath: "tier", type: "category" }
|
|
440
|
-
],
|
|
441
|
-
measures: [
|
|
442
|
-
{
|
|
443
|
-
key: "projects",
|
|
444
|
-
label: "Projects",
|
|
445
|
-
dataPath: "projects",
|
|
446
|
-
format: "number",
|
|
447
|
-
color: "#1d4ed8"
|
|
448
|
-
}
|
|
449
|
-
],
|
|
450
|
-
table: { caption: "Project counts by tier." }
|
|
451
|
-
}
|
|
452
|
-
});
|
|
453
|
-
var SaasProjectActivityVisualization = defineVisualization({
|
|
454
|
-
meta: {
|
|
455
|
-
...META,
|
|
456
|
-
key: "saas-boilerplate.visualization.project-activity",
|
|
457
|
-
title: "Recent Project Activity",
|
|
458
|
-
description: "Daily project creation activity.",
|
|
459
|
-
goal: "Show recent project activity over time.",
|
|
460
|
-
context: "Project portfolio trend view."
|
|
461
|
-
},
|
|
462
|
-
source: { primary: PROJECT_LIST_REF, resultPath: "data" },
|
|
463
|
-
visualization: {
|
|
464
|
-
kind: "cartesian",
|
|
465
|
-
variant: "line",
|
|
466
|
-
xDimension: "day",
|
|
467
|
-
yMeasures: ["projects"],
|
|
468
|
-
dimensions: [{ key: "day", label: "Day", dataPath: "day", type: "time" }],
|
|
469
|
-
measures: [
|
|
470
|
-
{
|
|
471
|
-
key: "projects",
|
|
472
|
-
label: "Projects",
|
|
473
|
-
dataPath: "projects",
|
|
474
|
-
format: "number",
|
|
475
|
-
color: "#0f766e"
|
|
476
|
-
}
|
|
477
|
-
],
|
|
478
|
-
table: { caption: "Daily project creation counts." }
|
|
479
|
-
}
|
|
480
|
-
});
|
|
481
|
-
var SaasVisualizationSpecs = [
|
|
482
|
-
SaasProjectUsageVisualization,
|
|
483
|
-
SaasProjectStatusVisualization,
|
|
484
|
-
SaasProjectTierVisualization,
|
|
485
|
-
SaasProjectActivityVisualization
|
|
486
|
-
];
|
|
487
|
-
var SaasVisualizationRegistry = new VisualizationRegistry([
|
|
488
|
-
...SaasVisualizationSpecs
|
|
489
|
-
]);
|
|
490
|
-
var SaasVisualizationRefs = SaasVisualizationSpecs.map((spec) => ({
|
|
491
|
-
key: spec.meta.key,
|
|
492
|
-
version: spec.meta.version
|
|
493
|
-
}));
|
|
494
|
-
|
|
495
|
-
// src/visualizations/selectors.ts
|
|
496
|
-
function toDayKey(value) {
|
|
497
|
-
const date = value instanceof Date ? value : new Date(value);
|
|
498
|
-
return date.toISOString().slice(0, 10);
|
|
499
|
-
}
|
|
500
|
-
function createSaasVisualizationItems(projects, projectLimit = 10) {
|
|
501
|
-
const statusCounts = new Map;
|
|
502
|
-
const tierCounts = new Map;
|
|
503
|
-
const activityCounts = new Map;
|
|
504
|
-
for (const project of projects) {
|
|
505
|
-
statusCounts.set(project.status, (statusCounts.get(project.status) ?? 0) + 1);
|
|
506
|
-
tierCounts.set(project.tier, (tierCounts.get(project.tier) ?? 0) + 1);
|
|
507
|
-
const day = toDayKey(project.createdAt);
|
|
508
|
-
activityCounts.set(day, (activityCounts.get(day) ?? 0) + 1);
|
|
509
|
-
}
|
|
510
|
-
return [
|
|
511
|
-
{
|
|
512
|
-
key: "saas-capacity",
|
|
513
|
-
spec: SaasProjectUsageVisualization,
|
|
514
|
-
data: { data: [{ totalProjects: projects.length, projectLimit }] },
|
|
515
|
-
title: "Project Capacity",
|
|
516
|
-
description: "Current project count compared to the active limit.",
|
|
517
|
-
height: 220
|
|
518
|
-
},
|
|
519
|
-
{
|
|
520
|
-
key: "saas-status",
|
|
521
|
-
spec: SaasProjectStatusVisualization,
|
|
522
|
-
data: {
|
|
523
|
-
data: Array.from(statusCounts.entries()).map(([status, count]) => ({
|
|
524
|
-
status,
|
|
525
|
-
projects: count
|
|
526
|
-
}))
|
|
527
|
-
},
|
|
528
|
-
title: "Project Status",
|
|
529
|
-
description: "Status mix across the current project portfolio.",
|
|
530
|
-
height: 260
|
|
531
|
-
},
|
|
532
|
-
{
|
|
533
|
-
key: "saas-tier",
|
|
534
|
-
spec: SaasProjectTierVisualization,
|
|
535
|
-
data: {
|
|
536
|
-
data: Array.from(tierCounts.entries()).map(([tier, count]) => ({
|
|
537
|
-
tier,
|
|
538
|
-
projects: count
|
|
539
|
-
}))
|
|
540
|
-
},
|
|
541
|
-
title: "Tier Comparison",
|
|
542
|
-
description: "How projects are distributed across tiers."
|
|
543
|
-
},
|
|
544
|
-
{
|
|
545
|
-
key: "saas-activity",
|
|
546
|
-
spec: SaasProjectActivityVisualization,
|
|
547
|
-
data: {
|
|
548
|
-
data: Array.from(activityCounts.entries()).sort(([left], [right]) => left.localeCompare(right)).map(([day, count]) => ({ day, projects: count }))
|
|
549
|
-
},
|
|
550
|
-
title: "Recent Project Activity",
|
|
551
|
-
description: "Daily project creation activity."
|
|
552
|
-
}
|
|
553
|
-
];
|
|
554
|
-
}
|
|
555
|
-
// src/ui/hooks/useProjectList.ts
|
|
556
|
-
import { useTemplateRuntime } from "@contractspec/lib.example-shared-ui";
|
|
557
|
-
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
558
|
-
function useProjectList(options = {}) {
|
|
559
|
-
const { handlers, projectId } = useTemplateRuntime();
|
|
560
|
-
const { saas: saas2 } = handlers;
|
|
561
|
-
const [data, setData] = useState(null);
|
|
562
|
-
const [subscription, setSubscription] = useState(null);
|
|
563
|
-
const [loading, setLoading] = useState(true);
|
|
564
|
-
const [error, setError] = useState(null);
|
|
565
|
-
const [page, setPage] = useState(1);
|
|
566
|
-
const fetchData = useCallback(async () => {
|
|
567
|
-
setLoading(true);
|
|
568
|
-
setError(null);
|
|
569
|
-
try {
|
|
570
|
-
const [projectsResult, subscriptionResult] = await Promise.all([
|
|
571
|
-
saas2.listProjects({
|
|
572
|
-
projectId,
|
|
573
|
-
status: options.status === "all" ? undefined : options.status,
|
|
574
|
-
search: options.search,
|
|
575
|
-
limit: options.limit ?? 20,
|
|
576
|
-
offset: (page - 1) * (options.limit ?? 20)
|
|
577
|
-
}),
|
|
578
|
-
saas2.getSubscription({ projectId })
|
|
579
|
-
]);
|
|
580
|
-
setData({
|
|
581
|
-
items: projectsResult.items,
|
|
582
|
-
total: projectsResult.total
|
|
583
|
-
});
|
|
584
|
-
setSubscription(subscriptionResult);
|
|
585
|
-
} catch (err) {
|
|
586
|
-
setError(err instanceof Error ? err : new Error("Unknown error"));
|
|
587
|
-
} finally {
|
|
588
|
-
setLoading(false);
|
|
589
|
-
}
|
|
590
|
-
}, [saas2, projectId, options.status, options.search, options.limit, page]);
|
|
591
|
-
useEffect(() => {
|
|
592
|
-
fetchData();
|
|
593
|
-
}, [fetchData]);
|
|
594
|
-
const stats = useMemo(() => {
|
|
595
|
-
if (!data)
|
|
596
|
-
return null;
|
|
597
|
-
const items = data.items;
|
|
598
|
-
return {
|
|
599
|
-
total: data.total,
|
|
600
|
-
activeCount: items.filter((p) => p.status === "ACTIVE").length,
|
|
601
|
-
draftCount: items.filter((p) => p.status === "DRAFT").length,
|
|
602
|
-
projectLimit: 10,
|
|
603
|
-
usagePercent: Math.min(data.total / 10 * 100, 100)
|
|
604
|
-
};
|
|
605
|
-
}, [data]);
|
|
606
|
-
return {
|
|
607
|
-
data,
|
|
608
|
-
subscription,
|
|
609
|
-
loading,
|
|
610
|
-
error,
|
|
611
|
-
stats,
|
|
612
|
-
page,
|
|
613
|
-
refetch: fetchData,
|
|
614
|
-
nextPage: () => setPage((p) => p + 1),
|
|
615
|
-
prevPage: () => page > 1 && setPage((p) => p - 1)
|
|
616
|
-
};
|
|
617
|
-
}
|
|
618
|
-
|
|
619
|
-
// src/ui/renderers/project-list.markdown.ts
|
|
620
|
-
var PROJECT_TIERS = [
|
|
621
|
-
"FREE",
|
|
622
|
-
"PRO",
|
|
623
|
-
"ENTERPRISE"
|
|
624
|
-
];
|
|
625
|
-
function toVisualizationProject(project, index) {
|
|
626
|
-
return {
|
|
627
|
-
status: project.status === "DELETED" ? "ARCHIVED" : project.status,
|
|
628
|
-
tier: PROJECT_TIERS[index % PROJECT_TIERS.length] ?? "FREE",
|
|
629
|
-
createdAt: project.createdAt
|
|
630
|
-
};
|
|
631
|
-
}
|
|
632
|
-
var projectListMarkdownRenderer = {
|
|
633
|
-
target: "markdown",
|
|
634
|
-
render: async (desc, _ctx) => {
|
|
635
|
-
if (desc.source.type !== "component" || desc.source.componentKey !== "ProjectListView") {
|
|
636
|
-
throw new Error("projectListMarkdownRenderer: not ProjectListView");
|
|
637
|
-
}
|
|
638
|
-
const data = await mockListProjectsHandler({
|
|
639
|
-
limit: 20,
|
|
640
|
-
offset: 0
|
|
641
|
-
});
|
|
642
|
-
const items = data.projects ?? [];
|
|
643
|
-
const lines = [
|
|
644
|
-
"# Projects",
|
|
645
|
-
"",
|
|
646
|
-
`**Total**: ${data.total} projects`,
|
|
647
|
-
""
|
|
648
|
-
];
|
|
649
|
-
if (items.length === 0) {
|
|
650
|
-
lines.push("_No projects found._");
|
|
651
|
-
} else {
|
|
652
|
-
lines.push("| Status | Project | Description |");
|
|
653
|
-
lines.push("|--------|---------|-------------|");
|
|
654
|
-
for (const project of items) {
|
|
655
|
-
const status = project.status === "ACTIVE" ? "\u2705" : project.status === "ARCHIVED" ? "\uD83D\uDCE6" : "\u23F8\uFE0F";
|
|
656
|
-
lines.push(`| ${status} | **${project.name}** | ${project.description ?? "-"} |`);
|
|
657
|
-
}
|
|
658
|
-
}
|
|
659
|
-
return {
|
|
660
|
-
mimeType: "text/markdown",
|
|
661
|
-
body: lines.join(`
|
|
662
|
-
`)
|
|
663
|
-
};
|
|
664
|
-
}
|
|
665
|
-
};
|
|
666
|
-
var saasDashboardMarkdownRenderer = {
|
|
667
|
-
target: "markdown",
|
|
668
|
-
render: async (desc, _ctx) => {
|
|
669
|
-
if (desc.source.type !== "component" || desc.source.componentKey !== "SaasDashboard") {
|
|
670
|
-
throw new Error("saasDashboardMarkdownRenderer: not SaasDashboard");
|
|
671
|
-
}
|
|
672
|
-
const [projectsData, subscription] = await Promise.all([
|
|
673
|
-
mockListProjectsHandler({ limit: 50 }),
|
|
674
|
-
mockGetSubscriptionHandler()
|
|
675
|
-
]);
|
|
676
|
-
const projects = projectsData.projects ?? [];
|
|
677
|
-
const activeProjects = projects.filter((p) => p.status === "ACTIVE").length;
|
|
678
|
-
const archivedProjects = projects.filter((p) => p.status === "ARCHIVED").length;
|
|
679
|
-
const visualizations = createSaasVisualizationItems(projects.map(toVisualizationProject), 10);
|
|
680
|
-
const lines = [
|
|
681
|
-
"# SaaS Dashboard",
|
|
682
|
-
"",
|
|
683
|
-
"> Organization overview and usage summary",
|
|
684
|
-
"",
|
|
685
|
-
"## Summary",
|
|
686
|
-
"",
|
|
687
|
-
"| Metric | Value |",
|
|
688
|
-
"|--------|-------|",
|
|
689
|
-
`| Total Projects | ${projectsData.total} |`,
|
|
690
|
-
`| Active Projects | ${activeProjects} |`,
|
|
691
|
-
`| Archived Projects | ${archivedProjects} |`,
|
|
692
|
-
`| Subscription Plan | ${subscription.planName} |`,
|
|
693
|
-
`| Subscription Status | ${subscription.status} |`,
|
|
694
|
-
""
|
|
695
|
-
];
|
|
696
|
-
lines.push("## Visualization Overview");
|
|
697
|
-
lines.push("");
|
|
698
|
-
for (const item of visualizations) {
|
|
699
|
-
lines.push(`- **${item.title}** via \`${item.spec.meta.key}\``);
|
|
700
|
-
}
|
|
701
|
-
lines.push("");
|
|
702
|
-
lines.push("## Projects");
|
|
703
|
-
lines.push("");
|
|
704
|
-
if (projects.length === 0) {
|
|
705
|
-
lines.push("_No projects yet._");
|
|
706
|
-
} else {
|
|
707
|
-
lines.push("| Status | Project | Description |");
|
|
708
|
-
lines.push("|--------|---------|-------------|");
|
|
709
|
-
for (const project of projects.slice(0, 10)) {
|
|
710
|
-
const status = project.status === "ACTIVE" ? "\u2705" : project.status === "ARCHIVED" ? "\uD83D\uDCE6" : "\u23F8\uFE0F";
|
|
711
|
-
lines.push(`| ${status} | **${project.name}** | ${project.description ?? "-"} |`);
|
|
712
|
-
}
|
|
713
|
-
if (projects.length > 10) {
|
|
714
|
-
lines.push(`| ... | ... | _${projectsData.total - 10} more projects_ |`);
|
|
715
|
-
}
|
|
716
|
-
}
|
|
717
|
-
lines.push("");
|
|
718
|
-
lines.push("## Subscription");
|
|
719
|
-
lines.push("");
|
|
720
|
-
lines.push(`- **Plan**: ${subscription.planName}`);
|
|
721
|
-
lines.push(`- **Status**: ${subscription.status}`);
|
|
722
|
-
if (subscription.currentPeriodEnd) {
|
|
723
|
-
lines.push(`- **Period End**: ${new Date(subscription.currentPeriodEnd).toLocaleDateString()}`);
|
|
724
|
-
}
|
|
725
|
-
return {
|
|
726
|
-
mimeType: "text/markdown",
|
|
727
|
-
body: lines.join(`
|
|
728
|
-
`)
|
|
729
|
-
};
|
|
730
|
-
}
|
|
731
|
-
};
|
|
732
|
-
var saasBillingMarkdownRenderer = {
|
|
733
|
-
target: "markdown",
|
|
734
|
-
render: async (desc, _ctx) => {
|
|
735
|
-
if (desc.source.type !== "component" || desc.source.componentKey !== "SubscriptionView") {
|
|
736
|
-
throw new Error("saasBillingMarkdownRenderer: not SubscriptionView");
|
|
737
|
-
}
|
|
738
|
-
const subscription = await mockGetSubscriptionHandler();
|
|
739
|
-
const lines = [
|
|
740
|
-
"# Billing & Subscription",
|
|
741
|
-
"",
|
|
742
|
-
"> Current subscription details and billing information",
|
|
743
|
-
"",
|
|
744
|
-
"## Subscription Details",
|
|
745
|
-
"",
|
|
746
|
-
"| Property | Value |",
|
|
747
|
-
"|----------|-------|",
|
|
748
|
-
`| Plan | ${subscription.planName} |`,
|
|
749
|
-
`| Status | ${subscription.status} |`,
|
|
750
|
-
`| ID | ${subscription.id} |`,
|
|
751
|
-
`| Period Start | ${new Date(subscription.currentPeriodStart).toLocaleDateString()} |`,
|
|
752
|
-
`| Period End | ${new Date(subscription.currentPeriodEnd).toLocaleDateString()} |`
|
|
753
|
-
];
|
|
754
|
-
lines.push("");
|
|
755
|
-
lines.push("## Plan Limits");
|
|
756
|
-
lines.push("");
|
|
757
|
-
lines.push(`- **Projects**: ${subscription.limits.projects}`);
|
|
758
|
-
lines.push(`- **Users**: ${subscription.limits.users}`);
|
|
759
|
-
lines.push("");
|
|
760
|
-
lines.push("## Plan Features");
|
|
761
|
-
lines.push("");
|
|
762
|
-
if (subscription.planName.toLowerCase().includes("free")) {
|
|
763
|
-
lines.push("- \u2705 Up to 3 projects");
|
|
764
|
-
lines.push("- \u2705 Basic support");
|
|
765
|
-
lines.push("- \u274C Priority support");
|
|
766
|
-
lines.push("- \u274C Advanced analytics");
|
|
767
|
-
} else if (subscription.planName.toLowerCase().includes("pro")) {
|
|
768
|
-
lines.push("- \u2705 Unlimited projects");
|
|
769
|
-
lines.push("- \u2705 Priority support");
|
|
770
|
-
lines.push("- \u2705 Advanced analytics");
|
|
771
|
-
lines.push("- \u274C Custom integrations");
|
|
772
|
-
} else {
|
|
773
|
-
lines.push("- \u2705 Unlimited projects");
|
|
774
|
-
lines.push("- \u2705 Priority support");
|
|
775
|
-
lines.push("- \u2705 Advanced analytics");
|
|
776
|
-
lines.push("- \u2705 Custom integrations");
|
|
777
|
-
lines.push("- \u2705 Dedicated support");
|
|
778
|
-
}
|
|
779
|
-
return {
|
|
780
|
-
mimeType: "text/markdown",
|
|
781
|
-
body: lines.join(`
|
|
782
|
-
`)
|
|
783
|
-
};
|
|
784
|
-
}
|
|
785
|
-
};
|
|
786
|
-
|
|
787
|
-
// src/ui/SaasProjectList.tsx
|
|
788
|
-
import {
|
|
789
|
-
Button,
|
|
790
|
-
EmptyState,
|
|
791
|
-
EntityCard,
|
|
792
|
-
ErrorState,
|
|
793
|
-
LoaderBlock,
|
|
794
|
-
StatCard,
|
|
795
|
-
StatCardGroup,
|
|
796
|
-
StatusChip
|
|
797
|
-
} from "@contractspec/lib.design-system";
|
|
798
|
-
import { jsxDEV } from "react/jsx-dev-runtime";
|
|
799
|
-
"use client";
|
|
800
|
-
function getStatusTone(status) {
|
|
801
|
-
switch (status) {
|
|
802
|
-
case "ACTIVE":
|
|
803
|
-
return "success";
|
|
804
|
-
case "DRAFT":
|
|
805
|
-
return "neutral";
|
|
806
|
-
case "ARCHIVED":
|
|
807
|
-
return "danger";
|
|
808
|
-
default:
|
|
809
|
-
return "neutral";
|
|
810
|
-
}
|
|
811
|
-
}
|
|
812
|
-
function SaasProjectList({
|
|
813
|
-
onProjectClick,
|
|
814
|
-
onCreateProject
|
|
815
|
-
}) {
|
|
816
|
-
const { data, loading, error, stats, refetch } = useProjectList();
|
|
817
|
-
if (loading && !data) {
|
|
818
|
-
return /* @__PURE__ */ jsxDEV(LoaderBlock, {
|
|
819
|
-
label: "Loading projects..."
|
|
820
|
-
}, undefined, false, undefined, this);
|
|
821
|
-
}
|
|
822
|
-
if (error) {
|
|
823
|
-
return /* @__PURE__ */ jsxDEV(ErrorState, {
|
|
824
|
-
title: "Failed to load projects",
|
|
825
|
-
description: error.message,
|
|
826
|
-
onRetry: refetch,
|
|
827
|
-
retryLabel: "Retry"
|
|
828
|
-
}, undefined, false, undefined, this);
|
|
829
|
-
}
|
|
830
|
-
if (!data?.items.length) {
|
|
831
|
-
return /* @__PURE__ */ jsxDEV(EmptyState, {
|
|
832
|
-
title: "No projects found",
|
|
833
|
-
description: "Create your first project to get started.",
|
|
834
|
-
primaryAction: onCreateProject ? /* @__PURE__ */ jsxDEV(Button, {
|
|
835
|
-
onPress: onCreateProject,
|
|
836
|
-
children: "Create Project"
|
|
837
|
-
}, undefined, false, undefined, this) : undefined
|
|
838
|
-
}, undefined, false, undefined, this);
|
|
839
|
-
}
|
|
840
|
-
return /* @__PURE__ */ jsxDEV("div", {
|
|
841
|
-
className: "space-y-6",
|
|
842
|
-
children: [
|
|
843
|
-
stats && /* @__PURE__ */ jsxDEV(StatCardGroup, {
|
|
844
|
-
children: [
|
|
845
|
-
/* @__PURE__ */ jsxDEV(StatCard, {
|
|
846
|
-
label: "Total Projects",
|
|
847
|
-
value: stats.total.toString()
|
|
848
|
-
}, undefined, false, undefined, this),
|
|
849
|
-
/* @__PURE__ */ jsxDEV(StatCard, {
|
|
850
|
-
label: "Active",
|
|
851
|
-
value: stats.activeCount.toString()
|
|
852
|
-
}, undefined, false, undefined, this),
|
|
853
|
-
/* @__PURE__ */ jsxDEV(StatCard, {
|
|
854
|
-
label: "Draft",
|
|
855
|
-
value: stats.draftCount.toString()
|
|
856
|
-
}, undefined, false, undefined, this)
|
|
857
|
-
]
|
|
858
|
-
}, undefined, true, undefined, this),
|
|
859
|
-
/* @__PURE__ */ jsxDEV("div", {
|
|
860
|
-
className: "grid gap-4 md:grid-cols-2 lg:grid-cols-3",
|
|
861
|
-
children: data.items.map((project) => /* @__PURE__ */ jsxDEV(EntityCard, {
|
|
862
|
-
cardTitle: project.name,
|
|
863
|
-
cardSubtitle: project.tier,
|
|
864
|
-
meta: /* @__PURE__ */ jsxDEV("p", {
|
|
865
|
-
className: "text-muted-foreground text-sm",
|
|
866
|
-
children: project.description
|
|
867
|
-
}, undefined, false, undefined, this),
|
|
868
|
-
chips: /* @__PURE__ */ jsxDEV(StatusChip, {
|
|
869
|
-
tone: getStatusTone(project.status),
|
|
870
|
-
label: project.status
|
|
871
|
-
}, undefined, false, undefined, this),
|
|
872
|
-
footer: /* @__PURE__ */ jsxDEV("span", {
|
|
873
|
-
className: "text-muted-foreground text-xs",
|
|
874
|
-
children: project.updatedAt.toLocaleDateString()
|
|
875
|
-
}, undefined, false, undefined, this),
|
|
876
|
-
onClick: onProjectClick ? () => onProjectClick(project.id) : undefined
|
|
877
|
-
}, project.id, false, undefined, this))
|
|
878
|
-
}, undefined, false, undefined, this)
|
|
879
|
-
]
|
|
880
|
-
}, undefined, true, undefined, this);
|
|
881
|
-
}
|
|
882
|
-
|
|
883
|
-
// src/ui/renderers/project-list.renderer.tsx
|
|
884
|
-
import { jsxDEV as jsxDEV2 } from "react/jsx-dev-runtime";
|
|
885
|
-
var projectListReactRenderer = {
|
|
886
|
-
target: "react",
|
|
887
|
-
render: async (desc, _ctx) => {
|
|
888
|
-
if (desc.source.type !== "component") {
|
|
889
|
-
throw new Error("Invalid source type");
|
|
890
|
-
}
|
|
891
|
-
if (desc.source.componentKey !== "SaasProjectListView") {
|
|
892
|
-
throw new Error(`Unknown component: ${desc.source.componentKey}`);
|
|
893
|
-
}
|
|
894
|
-
return /* @__PURE__ */ jsxDEV2(SaasProjectList, {}, undefined, false, undefined, this);
|
|
895
|
-
}
|
|
896
|
-
};
|
|
897
|
-
export {
|
|
898
|
-
saasDashboardMarkdownRenderer,
|
|
899
|
-
saasBillingMarkdownRenderer,
|
|
900
|
-
projectListReactRenderer,
|
|
901
|
-
projectListMarkdownRenderer
|
|
902
|
-
};
|
|
2
|
+
var K=[{id:"proj-1",name:"Marketing Website",description:"Main company website redesign project",slug:"marketing-website",organizationId:"demo-org",createdBy:"user-1",status:"ACTIVE",isPublic:!1,tags:["marketing","website","redesign"],createdAt:new Date("2024-01-15T10:00:00Z"),updatedAt:new Date("2024-03-20T14:30:00Z")},{id:"proj-2",name:"Mobile App v2",description:"Next generation mobile application",slug:"mobile-app-v2",organizationId:"demo-org",createdBy:"user-2",status:"ACTIVE",isPublic:!1,tags:["mobile","app","v2"],createdAt:new Date("2024-02-01T09:00:00Z"),updatedAt:new Date("2024-04-05T11:15:00Z")},{id:"proj-3",name:"API Integration",description:"Third-party API integration project",slug:"api-integration",organizationId:"demo-org",createdBy:"user-1",status:"DRAFT",isPublic:!1,tags:["api","integration"],createdAt:new Date("2024-03-10T08:00:00Z"),updatedAt:new Date("2024-03-10T08:00:00Z")},{id:"proj-4",name:"Analytics Dashboard",description:"Internal analytics and reporting dashboard",slug:"analytics-dashboard",organizationId:"demo-org",createdBy:"user-3",status:"ARCHIVED",isPublic:!0,tags:["analytics","dashboard","reporting"],createdAt:new Date("2023-10-01T12:00:00Z"),updatedAt:new Date("2024-02-28T16:45:00Z")}],v={id:"sub-1",organizationId:"demo-org",planId:"pro",planName:"Professional",status:"ACTIVE",currentPeriodStart:new Date("2024-04-01T00:00:00Z"),currentPeriodEnd:new Date("2024-05-01T00:00:00Z"),limits:{projects:25,users:10,storage:50,apiCalls:1e5},usage:{projects:4,users:5,storage:12.5,apiCalls:45230}},U={organizationId:"demo-org",period:"current_month",apiCalls:{total:45230,limit:1e5,percentUsed:45.23},storage:{totalGb:12.5,limitGb:50,percentUsed:25},activeProjects:4,activeUsers:5,breakdown:[{date:"2024-04-01",apiCalls:3200,storageGb:12.1},{date:"2024-04-02",apiCalls:2800,storageGb:12.2},{date:"2024-04-03",apiCalls:4100,storageGb:12.3},{date:"2024-04-04",apiCalls:3600,storageGb:12.4},{date:"2024-04-05",apiCalls:3800,storageGb:12.5}]};async function R(){return v}async function l(H){return{...U,period:H.period??"current_month"}}async function j(H){return{recorded:!0,newTotal:U.apiCalls.total+H.quantity}}async function c(H){let{feature:X}=H;return{custom_domains:{allowed:!0},api_access:{allowed:!0,currentUsage:U.apiCalls.total,limit:U.apiCalls.limit},advanced_analytics:{allowed:!1,reason:"FEATURE_NOT_INCLUDED"},unlimited_projects:{allowed:!1,reason:"PLAN_LIMIT",currentUsage:v.usage.projects,limit:v.limits.projects}}[X]??{allowed:!0}}async function V(H){let{status:X,search:Q,limit:$=20,offset:N=0}=H,Z=[...K];if(X&&X!=="all")Z=Z.filter((W)=>W.status===X);if(Q){let W=Q.toLowerCase();Z=Z.filter((k)=>k.name.toLowerCase().includes(W)||k.description?.toLowerCase().includes(W)||k.tags.some((G)=>G.toLowerCase().includes(W)))}Z.sort((W,k)=>k.updatedAt.getTime()-W.updatedAt.getTime());let F=Z.length;return{projects:Z.slice(N,N+$),total:F}}async function u(H){let X=K.find((Q)=>Q.id===H.projectId);if(!X)throw Error("NOT_FOUND");return X}async function r(H,X){if(H.slug){if(K.some((N)=>N.slug===H.slug))throw Error("SLUG_EXISTS")}let Q=new Date;return{id:`proj-${Date.now()}`,name:H.name,description:H.description,slug:H.slug??H.name.toLowerCase().replace(/\s+/g,"-"),organizationId:X.organizationId,createdBy:X.userId,status:"DRAFT",isPublic:H.isPublic??!1,tags:H.tags??[],createdAt:Q,updatedAt:Q}}async function n(H){let X=K.find((Q)=>Q.id===H.projectId);if(!X)throw Error("NOT_FOUND");return{...X,name:H.name??X.name,description:H.description??X.description,slug:H.slug??X.slug,isPublic:H.isPublic??X.isPublic,tags:H.tags??X.tags,status:H.status??X.status,updatedAt:new Date}}async function a(H){if(!K.find((Q)=>Q.id===H.projectId))throw Error("NOT_FOUND");return{success:!0}}import{web as p}from"@contractspec/lib.runtime-sandbox";var{generateId:o}=p;function g(H){return{id:H.id,projectId:H.projectId,organizationId:H.organizationId,name:H.name,description:H.description??void 0,status:H.status,tier:H.tier,createdAt:new Date(H.createdAt),updatedAt:new Date(H.updatedAt)}}function e(H){return{id:H.id,projectId:H.projectId,organizationId:H.organizationId,plan:H.plan,status:H.status,billingCycle:H.billingCycle,currentPeriodStart:new Date(H.currentPeriodStart),currentPeriodEnd:new Date(H.currentPeriodEnd),cancelAtPeriodEnd:Boolean(H.cancelAtPeriodEnd)}}function RH(H){async function X(B){let{projectId:W,organizationId:k,status:G,search:m,limit:y=20,offset:A=0}=B,L="WHERE projectId = ?",J=[W];if(k)L+=" AND organizationId = ?",J.push(k);if(G&&G!=="all")L+=" AND status = ?",J.push(G);if(m)L+=" AND (name LIKE ? OR description LIKE ?)",J.push(`%${m}%`,`%${m}%`);let D=(await H.query(`SELECT COUNT(*) as count FROM saas_project ${L}`,J)).rows[0]?.count??0,T=(await H.query(`SELECT * FROM saas_project ${L} ORDER BY createdAt DESC LIMIT ? OFFSET ?`,[...J,y,A])).rows;return{items:T.map(g),total:D,hasMore:A+T.length<D}}async function Q(B){let W=(await H.query("SELECT * FROM saas_project WHERE id = ?",[B])).rows;return W[0]?g(W[0]):null}async function $(B,W){let k=o("proj"),G=new Date().toISOString();await H.execute(`INSERT INTO saas_project (id, projectId, organizationId, name, description, status, tier, createdAt, updatedAt)
|
|
3
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,[k,W.projectId,W.organizationId,B.name,B.description??null,"DRAFT",B.tier??"FREE",G,G]);let m=(await H.query("SELECT * FROM saas_project WHERE id = ?",[k])).rows;return g(m[0])}async function N(B){let W=new Date().toISOString(),k=["updatedAt = ?"],G=[W];if(B.name!==void 0)k.push("name = ?"),G.push(B.name);if(B.description!==void 0)k.push("description = ?"),G.push(B.description);if(B.status!==void 0)k.push("status = ?"),G.push(B.status);G.push(B.id),await H.execute(`UPDATE saas_project SET ${k.join(", ")} WHERE id = ?`,G);let m=(await H.query("SELECT * FROM saas_project WHERE id = ?",[B.id])).rows;if(!m[0])throw Error("NOT_FOUND");return g(m[0])}async function Z(B){await H.execute("DELETE FROM saas_project WHERE id = ?",[B])}async function F(B){let W="SELECT * FROM saas_subscription WHERE projectId = ?",k=[B.projectId];if(B.organizationId)W+=" AND organizationId = ?",k.push(B.organizationId);W+=" LIMIT 1";let G=(await H.query(W,k)).rows;return G[0]?e(G[0]):null}return{listProjects:X,getProject:Q,createProject:$,updateProject:N,deleteProject:Z,getSubscription:F}}import{defineVisualization as x,VisualizationRegistry as i}from"@contractspec/lib.contracts-spec/visualizations";var I={key:"saas.project.list",version:"1.0.0"},z={version:"1.0.0",domain:"saas",stability:"experimental",owners:["@example.saas-boilerplate"],tags:["saas","visualization","projects"]},h=x({meta:{...z,key:"saas-boilerplate.visualization.project-usage",title:"Project Capacity",description:"Current project count against the current plan limit.",goal:"Show usage against the active plan allowance.",context:"SaaS account overview."},source:{primary:I,resultPath:"data"},visualization:{kind:"metric",measure:"totalProjects",comparisonMeasure:"projectLimit",measures:[{key:"totalProjects",label:"Projects",dataPath:"totalProjects",format:"number"},{key:"projectLimit",label:"Plan Limit",dataPath:"projectLimit",format:"number"}],table:{caption:"Current project count and plan limit."}}}),_=x({meta:{...z,key:"saas-boilerplate.visualization.project-status",title:"Project Status",description:"Distribution of project states.",goal:"Show the mix of active, draft, and archived projects.",context:"Project portfolio overview."},source:{primary:I,resultPath:"data"},visualization:{kind:"pie",nameDimension:"status",valueMeasure:"projects",dimensions:[{key:"status",label:"Status",dataPath:"status",type:"category"}],measures:[{key:"projects",label:"Projects",dataPath:"projects",format:"number"}],table:{caption:"Project counts by status."}}}),M=x({meta:{...z,key:"saas-boilerplate.visualization.project-tiers",title:"Tier Comparison",description:"Distribution of projects across tiers.",goal:"Compare how the current portfolio is distributed by tier.",context:"Plan and packaging overview."},source:{primary:I,resultPath:"data"},visualization:{kind:"cartesian",variant:"bar",xDimension:"tier",yMeasures:["projects"],dimensions:[{key:"tier",label:"Tier",dataPath:"tier",type:"category"}],measures:[{key:"projects",label:"Projects",dataPath:"projects",format:"number",color:"#1d4ed8"}],table:{caption:"Project counts by tier."}}}),E=x({meta:{...z,key:"saas-boilerplate.visualization.project-activity",title:"Recent Project Activity",description:"Daily project creation activity.",goal:"Show recent project activity over time.",context:"Project portfolio trend view."},source:{primary:I,resultPath:"data"},visualization:{kind:"cartesian",variant:"line",xDimension:"day",yMeasures:["projects"],dimensions:[{key:"day",label:"Day",dataPath:"day",type:"time"}],measures:[{key:"projects",label:"Projects",dataPath:"projects",format:"number",color:"#0f766e"}],table:{caption:"Daily project creation counts."}}}),P=[h,_,M,E],_H=new i([...P]),MH=P.map((H)=>({key:H.meta.key,version:H.meta.version}));function s(H){return(H instanceof Date?H:new Date(H)).toISOString().slice(0,10)}function b(H,X=10){let Q=new Map,$=new Map,N=new Map;for(let Z of H){Q.set(Z.status,(Q.get(Z.status)??0)+1),$.set(Z.tier,($.get(Z.tier)??0)+1);let F=s(Z.createdAt);N.set(F,(N.get(F)??0)+1)}return[{key:"saas-capacity",spec:h,data:{data:[{totalProjects:H.length,projectLimit:X}]},title:"Project Capacity",description:"Current project count compared to the active limit.",height:220},{key:"saas-status",spec:_,data:{data:Array.from(Q.entries()).map(([Z,F])=>({status:Z,projects:F}))},title:"Project Status",description:"Status mix across the current project portfolio.",height:260},{key:"saas-tier",spec:M,data:{data:Array.from($.entries()).map(([Z,F])=>({tier:Z,projects:F}))},title:"Tier Comparison",description:"How projects are distributed across tiers."},{key:"saas-activity",spec:E,data:{data:Array.from(N.entries()).sort(([Z],[F])=>Z.localeCompare(F)).map(([Z,F])=>({day:Z,projects:F}))},title:"Recent Project Activity",description:"Daily project creation activity."}]}import{useTemplateRuntime as t}from"@contractspec/lib.example-shared-ui";import{useCallback as HH,useEffect as $H,useMemo as QH,useState as f}from"react";function w(H={}){let{handlers:X,projectId:Q}=t(),{saas:$}=X,[N,Z]=f(null),[F,B]=f(null),[W,k]=f(!0),[G,m]=f(null),[y,A]=f(1),L=HH(async()=>{k(!0),m(null);try{let[Y,D]=await Promise.all([$.listProjects({projectId:Q,status:H.status==="all"?void 0:H.status,search:H.search,limit:H.limit??20,offset:(y-1)*(H.limit??20)}),$.getSubscription({projectId:Q})]);Z({items:Y.items,total:Y.total}),B(D)}catch(Y){m(Y instanceof Error?Y:Error("Unknown error"))}finally{k(!1)}},[$,Q,H.status,H.search,H.limit,y]);$H(()=>{L()},[L]);let J=QH(()=>{if(!N)return null;let Y=N.items;return{total:N.total,activeCount:Y.filter((D)=>D.status==="ACTIVE").length,draftCount:Y.filter((D)=>D.status==="DRAFT").length,projectLimit:10,usagePercent:Math.min(N.total/10*100,100)}},[N]);return{data:N,subscription:F,loading:W,error:G,stats:J,page:y,refetch:L,nextPage:()=>A((Y)=>Y+1),prevPage:()=>y>1&&A((Y)=>Y-1)}}var S=["FREE","PRO","ENTERPRISE"];function WH(H,X){return{status:H.status==="DELETED"?"ARCHIVED":H.status,tier:S[X%S.length]??"FREE",createdAt:H.createdAt}}var XH={target:"markdown",render:async(H,X)=>{if(H.source.type!=="component"||H.source.componentKey!=="ProjectListView")throw Error("projectListMarkdownRenderer: not ProjectListView");let Q=await V({limit:20,offset:0}),$=Q.projects??[],N=["# Projects","",`**Total**: ${Q.total} projects`,""];if($.length===0)N.push("_No projects found._");else{N.push("| Status | Project | Description |"),N.push("|--------|---------|-------------|");for(let Z of $){let F=Z.status==="ACTIVE"?"\u2705":Z.status==="ARCHIVED"?"\uD83D\uDCE6":"\u23F8\uFE0F";N.push(`| ${F} | **${Z.name}** | ${Z.description??"-"} |`)}}return{mimeType:"text/markdown",body:N.join(`
|
|
4
|
+
`)}}},ZH={target:"markdown",render:async(H,X)=>{if(H.source.type!=="component"||H.source.componentKey!=="SaasDashboard")throw Error("saasDashboardMarkdownRenderer: not SaasDashboard");let[Q,$]=await Promise.all([V({limit:50}),R()]),N=Q.projects??[],Z=N.filter((k)=>k.status==="ACTIVE").length,F=N.filter((k)=>k.status==="ARCHIVED").length,B=b(N.map(WH),10),W=["# SaaS Dashboard","","> Organization overview and usage summary","","## Summary","","| Metric | Value |","|--------|-------|",`| Total Projects | ${Q.total} |`,`| Active Projects | ${Z} |`,`| Archived Projects | ${F} |`,`| Subscription Plan | ${$.planName} |`,`| Subscription Status | ${$.status} |`,""];W.push("## Visualization Overview"),W.push("");for(let k of B)W.push(`- **${k.title}** via \`${k.spec.meta.key}\``);if(W.push(""),W.push("## Projects"),W.push(""),N.length===0)W.push("_No projects yet._");else{W.push("| Status | Project | Description |"),W.push("|--------|---------|-------------|");for(let k of N.slice(0,10)){let G=k.status==="ACTIVE"?"\u2705":k.status==="ARCHIVED"?"\uD83D\uDCE6":"\u23F8\uFE0F";W.push(`| ${G} | **${k.name}** | ${k.description??"-"} |`)}if(N.length>10)W.push(`| ... | ... | _${Q.total-10} more projects_ |`)}if(W.push(""),W.push("## Subscription"),W.push(""),W.push(`- **Plan**: ${$.planName}`),W.push(`- **Status**: ${$.status}`),$.currentPeriodEnd)W.push(`- **Period End**: ${new Date($.currentPeriodEnd).toLocaleDateString()}`);return{mimeType:"text/markdown",body:W.join(`
|
|
5
|
+
`)}}},BH={target:"markdown",render:async(H,X)=>{if(H.source.type!=="component"||H.source.componentKey!=="SubscriptionView")throw Error("saasBillingMarkdownRenderer: not SubscriptionView");let Q=await R(),$=["# Billing & Subscription","","> Current subscription details and billing information","","## Subscription Details","","| Property | Value |","|----------|-------|",`| Plan | ${Q.planName} |`,`| Status | ${Q.status} |`,`| ID | ${Q.id} |`,`| Period Start | ${new Date(Q.currentPeriodStart).toLocaleDateString()} |`,`| Period End | ${new Date(Q.currentPeriodEnd).toLocaleDateString()} |`];if($.push(""),$.push("## Plan Limits"),$.push(""),$.push(`- **Projects**: ${Q.limits.projects}`),$.push(`- **Users**: ${Q.limits.users}`),$.push(""),$.push("## Plan Features"),$.push(""),Q.planName.toLowerCase().includes("free"))$.push("- \u2705 Up to 3 projects"),$.push("- \u2705 Basic support"),$.push("- \u274C Priority support"),$.push("- \u274C Advanced analytics");else if(Q.planName.toLowerCase().includes("pro"))$.push("- \u2705 Unlimited projects"),$.push("- \u2705 Priority support"),$.push("- \u2705 Advanced analytics"),$.push("- \u274C Custom integrations");else $.push("- \u2705 Unlimited projects"),$.push("- \u2705 Priority support"),$.push("- \u2705 Advanced analytics"),$.push("- \u2705 Custom integrations"),$.push("- \u2705 Dedicated support");return{mimeType:"text/markdown",body:$.join(`
|
|
6
|
+
`)}}};import{Button as kH,EmptyState as NH,EntityCard as FH,ErrorState as GH,LoaderBlock as YH,StatCard as O,StatCardGroup as qH,StatusChip as mH}from"@contractspec/lib.design-system";import{jsx as q,jsxs as C}from"react/jsx-runtime";function LH(H){switch(H){case"ACTIVE":return"success";case"DRAFT":return"neutral";case"ARCHIVED":return"danger";default:return"neutral"}}function d({onProjectClick:H,onCreateProject:X}){let{data:Q,loading:$,error:N,stats:Z,refetch:F}=w();if($&&!Q)return q(YH,{label:"Loading projects..."});if(N)return q(GH,{title:"Failed to load projects",description:N.message,onRetry:F,retryLabel:"Retry"});if(!Q?.items.length)return q(NH,{title:"No projects found",description:"Create your first project to get started.",primaryAction:X?q(kH,{onPress:X,children:"Create Project"}):void 0});return C("div",{className:"space-y-6",children:[Z&&C(qH,{children:[q(O,{label:"Total Projects",value:Z.total.toString()}),q(O,{label:"Active",value:Z.activeCount.toString()}),q(O,{label:"Draft",value:Z.draftCount.toString()})]}),q("div",{className:"grid gap-4 md:grid-cols-2 lg:grid-cols-3",children:Q.items.map((B)=>q(FH,{cardTitle:B.name,cardSubtitle:B.tier,meta:q("p",{className:"text-muted-foreground text-sm",children:B.description}),chips:q(mH,{tone:LH(B.status),label:B.status}),footer:q("span",{className:"text-muted-foreground text-xs",children:B.updatedAt.toLocaleDateString()}),onClick:H?()=>H(B.id):void 0},B.id))})]})}import{jsx as UH}from"react/jsx-runtime";var DH={target:"react",render:async(H,X)=>{if(H.source.type!=="component")throw Error("Invalid source type");if(H.source.componentKey!=="SaasProjectListView")throw Error(`Unknown component: ${H.source.componentKey}`);return UH(d,{})}};export{ZH as saasDashboardMarkdownRenderer,BH as saasBillingMarkdownRenderer,DH as projectListReactRenderer,XH as projectListMarkdownRenderer};
|