@contractspec/example.saas-boilerplate 0.0.0-canary-20260113170453
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$colon$bundle.log +188 -0
- package/.turbo/turbo-build.log +189 -0
- package/CHANGELOG.md +440 -0
- package/LICENSE +21 -0
- package/README.md +155 -0
- package/dist/billing/billing.entity.d.ts +61 -0
- package/dist/billing/billing.entity.d.ts.map +1 -0
- package/dist/billing/billing.entity.js +122 -0
- package/dist/billing/billing.entity.js.map +1 -0
- package/dist/billing/billing.enum.d.ts +16 -0
- package/dist/billing/billing.enum.d.ts.map +1 -0
- package/dist/billing/billing.enum.js +27 -0
- package/dist/billing/billing.enum.js.map +1 -0
- package/dist/billing/billing.event.d.ts +86 -0
- package/dist/billing/billing.event.d.ts.map +1 -0
- package/dist/billing/billing.event.js +153 -0
- package/dist/billing/billing.event.js.map +1 -0
- package/dist/billing/billing.handler.d.ts +82 -0
- package/dist/billing/billing.handler.d.ts.map +1 -0
- package/dist/billing/billing.handler.js +58 -0
- package/dist/billing/billing.handler.js.map +1 -0
- package/dist/billing/billing.operations.d.ts +166 -0
- package/dist/billing/billing.operations.d.ts.map +1 -0
- package/dist/billing/billing.operations.js +181 -0
- package/dist/billing/billing.operations.js.map +1 -0
- package/dist/billing/billing.presentation.d.ts +14 -0
- package/dist/billing/billing.presentation.d.ts.map +1 -0
- package/dist/billing/billing.presentation.js +59 -0
- package/dist/billing/billing.presentation.js.map +1 -0
- package/dist/billing/billing.schema.d.ts +201 -0
- package/dist/billing/billing.schema.d.ts.map +1 -0
- package/dist/billing/billing.schema.js +214 -0
- package/dist/billing/billing.schema.js.map +1 -0
- package/dist/billing/index.d.ts +8 -0
- package/dist/billing/index.js +9 -0
- package/dist/dashboard/dashboard.presentation.d.ts +14 -0
- package/dist/dashboard/dashboard.presentation.d.ts.map +1 -0
- package/dist/dashboard/dashboard.presentation.js +55 -0
- package/dist/dashboard/dashboard.presentation.js.map +1 -0
- package/dist/dashboard/index.d.ts +2 -0
- package/dist/dashboard/index.js +3 -0
- package/dist/docs/index.d.ts +1 -0
- package/dist/docs/index.js +1 -0
- package/dist/docs/saas-boilerplate.docblock.d.ts +1 -0
- package/dist/docs/saas-boilerplate.docblock.js +100 -0
- package/dist/docs/saas-boilerplate.docblock.js.map +1 -0
- package/dist/example.d.ts +7 -0
- package/dist/example.d.ts.map +1 -0
- package/dist/example.js +53 -0
- package/dist/example.js.map +1 -0
- package/dist/handlers/index.d.ts +4 -0
- package/dist/handlers/index.js +5 -0
- package/dist/handlers/saas.handlers.d.ts +68 -0
- package/dist/handlers/saas.handlers.d.ts.map +1 -0
- package/dist/handlers/saas.handlers.js +148 -0
- package/dist/handlers/saas.handlers.js.map +1 -0
- package/dist/index.d.ts +54 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +81 -0
- package/dist/index.js.map +1 -0
- package/dist/presentations/index.d.ts +17 -0
- package/dist/presentations/index.d.ts.map +1 -0
- package/dist/presentations/index.js +17 -0
- package/dist/presentations/index.js.map +1 -0
- package/dist/project/index.d.ts +8 -0
- package/dist/project/index.js +9 -0
- package/dist/project/project.entity.d.ts +40 -0
- package/dist/project/project.entity.d.ts.map +1 -0
- package/dist/project/project.entity.js +85 -0
- package/dist/project/project.entity.js.map +1 -0
- package/dist/project/project.enum.d.ts +16 -0
- package/dist/project/project.enum.d.ts.map +1 -0
- package/dist/project/project.enum.js +26 -0
- package/dist/project/project.enum.js.map +1 -0
- package/dist/project/project.event.d.ts +92 -0
- package/dist/project/project.event.d.ts.map +1 -0
- package/dist/project/project.event.js +165 -0
- package/dist/project/project.event.js.map +1 -0
- package/dist/project/project.handler.d.ts +72 -0
- package/dist/project/project.handler.d.ts.map +1 -0
- package/dist/project/project.handler.js +82 -0
- package/dist/project/project.handler.js.map +1 -0
- package/dist/project/project.operations.d.ts +419 -0
- package/dist/project/project.operations.d.ts.map +1 -0
- package/dist/project/project.operations.js +260 -0
- package/dist/project/project.operations.js.map +1 -0
- package/dist/project/project.presentation.d.ts +14 -0
- package/dist/project/project.presentation.d.ts.map +1 -0
- package/dist/project/project.presentation.js +65 -0
- package/dist/project/project.presentation.js.map +1 -0
- package/dist/project/project.schema.d.ts +235 -0
- package/dist/project/project.schema.d.ts.map +1 -0
- package/dist/project/project.schema.js +215 -0
- package/dist/project/project.schema.js.map +1 -0
- package/dist/saas-boilerplate.feature.d.ts +12 -0
- package/dist/saas-boilerplate.feature.d.ts.map +1 -0
- package/dist/saas-boilerplate.feature.js +208 -0
- package/dist/saas-boilerplate.feature.js.map +1 -0
- package/dist/seeders/index.d.ts +10 -0
- package/dist/seeders/index.d.ts.map +1 -0
- package/dist/seeders/index.js +19 -0
- package/dist/seeders/index.js.map +1 -0
- package/dist/settings/index.d.ts +3 -0
- package/dist/settings/index.js +4 -0
- package/dist/settings/settings.entity.d.ts +37 -0
- package/dist/settings/settings.entity.d.ts.map +1 -0
- package/dist/settings/settings.entity.js +78 -0
- package/dist/settings/settings.entity.js.map +1 -0
- package/dist/settings/settings.enum.d.ts +10 -0
- package/dist/settings/settings.enum.d.ts.map +1 -0
- package/dist/settings/settings.enum.js +21 -0
- package/dist/settings/settings.enum.js.map +1 -0
- package/dist/shared/mock-data.d.ts +86 -0
- package/dist/shared/mock-data.d.ts.map +1 -0
- package/dist/shared/mock-data.js +138 -0
- package/dist/shared/mock-data.js.map +1 -0
- package/dist/shared/overlay-types.d.ts +34 -0
- package/dist/shared/overlay-types.d.ts.map +1 -0
- package/dist/shared/overlay-types.js +0 -0
- package/dist/tests/operations.test-spec.d.ts +10 -0
- package/dist/tests/operations.test-spec.d.ts.map +1 -0
- package/dist/tests/operations.test-spec.js +123 -0
- package/dist/tests/operations.test-spec.js.map +1 -0
- package/dist/ui/SaasDashboard.d.ts +7 -0
- package/dist/ui/SaasDashboard.d.ts.map +1 -0
- package/dist/ui/SaasDashboard.js +298 -0
- package/dist/ui/SaasDashboard.js.map +1 -0
- package/dist/ui/SaasProjectList.d.ts +14 -0
- package/dist/ui/SaasProjectList.d.ts.map +1 -0
- package/dist/ui/SaasProjectList.js +76 -0
- package/dist/ui/SaasProjectList.js.map +1 -0
- package/dist/ui/SaasSettingsPanel.d.ts +7 -0
- package/dist/ui/SaasSettingsPanel.d.ts.map +1 -0
- package/dist/ui/SaasSettingsPanel.js +138 -0
- package/dist/ui/SaasSettingsPanel.js.map +1 -0
- package/dist/ui/hooks/index.d.ts +3 -0
- package/dist/ui/hooks/index.js +6 -0
- package/dist/ui/hooks/useProjectList.d.ts +34 -0
- package/dist/ui/hooks/useProjectList.d.ts.map +1 -0
- package/dist/ui/hooks/useProjectList.js +75 -0
- package/dist/ui/hooks/useProjectList.js.map +1 -0
- package/dist/ui/hooks/useProjectMutations.d.ts +28 -0
- package/dist/ui/hooks/useProjectMutations.d.ts.map +1 -0
- package/dist/ui/hooks/useProjectMutations.js +146 -0
- package/dist/ui/hooks/useProjectMutations.js.map +1 -0
- package/dist/ui/index.d.ts +14 -0
- package/dist/ui/index.js +15 -0
- package/dist/ui/modals/CreateProjectModal.d.ts +23 -0
- package/dist/ui/modals/CreateProjectModal.d.ts.map +1 -0
- package/dist/ui/modals/CreateProjectModal.js +139 -0
- package/dist/ui/modals/CreateProjectModal.js.map +1 -0
- package/dist/ui/modals/ProjectActionsModal.d.ts +38 -0
- package/dist/ui/modals/ProjectActionsModal.d.ts.map +1 -0
- package/dist/ui/modals/ProjectActionsModal.js +292 -0
- package/dist/ui/modals/ProjectActionsModal.js.map +1 -0
- package/dist/ui/modals/index.d.ts +3 -0
- package/dist/ui/modals/index.js +4 -0
- package/dist/ui/overlays/demo-overlays.d.ts +19 -0
- package/dist/ui/overlays/demo-overlays.d.ts.map +1 -0
- package/dist/ui/overlays/demo-overlays.js +70 -0
- package/dist/ui/overlays/demo-overlays.js.map +1 -0
- package/dist/ui/overlays/index.d.ts +2 -0
- package/dist/ui/overlays/index.js +3 -0
- package/dist/ui/renderers/index.d.ts +3 -0
- package/dist/ui/renderers/index.js +4 -0
- package/dist/ui/renderers/project-list.markdown.d.ts +31 -0
- package/dist/ui/renderers/project-list.markdown.d.ts.map +1 -0
- package/dist/ui/renderers/project-list.markdown.js +148 -0
- package/dist/ui/renderers/project-list.markdown.js.map +1 -0
- package/dist/ui/renderers/project-list.renderer.d.ts +9 -0
- package/dist/ui/renderers/project-list.renderer.d.ts.map +1 -0
- package/dist/ui/renderers/project-list.renderer.js +17 -0
- package/dist/ui/renderers/project-list.renderer.js.map +1 -0
- package/example.ts +1 -0
- package/package.json +135 -0
- package/src/billing/billing.entity.ts +158 -0
- package/src/billing/billing.enum.ts +23 -0
- package/src/billing/billing.event.ts +108 -0
- package/src/billing/billing.handler.ts +137 -0
- package/src/billing/billing.operations.ts +187 -0
- package/src/billing/billing.presentation.ts +56 -0
- package/src/billing/billing.schema.ts +133 -0
- package/src/billing/index.ts +64 -0
- package/src/dashboard/dashboard.presentation.ts +56 -0
- package/src/dashboard/index.ts +8 -0
- package/src/docs/index.ts +1 -0
- package/src/docs/saas-boilerplate.docblock.ts +98 -0
- package/src/example.ts +38 -0
- package/src/handlers/index.ts +23 -0
- package/src/handlers/saas.handlers.ts +300 -0
- package/src/index.ts +76 -0
- package/src/presentations/index.ts +36 -0
- package/src/project/index.ts +66 -0
- package/src/project/project.entity.ts +93 -0
- package/src/project/project.enum.ts +22 -0
- package/src/project/project.event.ts +128 -0
- package/src/project/project.handler.ts +168 -0
- package/src/project/project.operations.ts +272 -0
- package/src/project/project.presentation.ts +58 -0
- package/src/project/project.schema.ts +147 -0
- package/src/saas-boilerplate.feature.ts +113 -0
- package/src/seeders/index.ts +28 -0
- package/src/settings/index.ts +9 -0
- package/src/settings/settings.entity.ts +89 -0
- package/src/settings/settings.enum.ts +11 -0
- package/src/shared/mock-data.ts +110 -0
- package/src/shared/overlay-types.ts +39 -0
- package/src/tests/operations.test-spec.ts +109 -0
- package/src/ui/SaasDashboard.tsx +325 -0
- package/src/ui/SaasProjectList.tsx +113 -0
- package/src/ui/SaasSettingsPanel.tsx +96 -0
- package/src/ui/hooks/index.ts +10 -0
- package/src/ui/hooks/useProjectList.ts +95 -0
- package/src/ui/hooks/useProjectMutations.ts +166 -0
- package/src/ui/index.ts +18 -0
- package/src/ui/modals/CreateProjectModal.tsx +176 -0
- package/src/ui/modals/ProjectActionsModal.tsx +346 -0
- package/src/ui/modals/index.ts +2 -0
- package/src/ui/overlays/demo-overlays.ts +74 -0
- package/src/ui/overlays/index.ts +1 -0
- package/src/ui/renderers/index.ts +7 -0
- package/src/ui/renderers/project-list.markdown.ts +239 -0
- package/src/ui/renderers/project-list.renderer.tsx +22 -0
- package/tsconfig.json +10 -0
- package/tsconfig.tsbuildinfo +1 -0
- package/tsdown.config.js +7 -0
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { defineEntity, field, index } from '@contractspec/lib.schema';
|
|
2
|
+
import { SettingsScopeEnum } from './settings.enum';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Settings entity - key-value configuration store.
|
|
6
|
+
*/
|
|
7
|
+
export const SettingsEntity = defineEntity({
|
|
8
|
+
name: 'Settings',
|
|
9
|
+
description: 'Application, organization, or user settings.',
|
|
10
|
+
schema: 'saas_app',
|
|
11
|
+
map: 'settings',
|
|
12
|
+
fields: {
|
|
13
|
+
id: field.id(),
|
|
14
|
+
|
|
15
|
+
// Key identification
|
|
16
|
+
key: field.string({
|
|
17
|
+
description: 'Setting key (e.g., "theme", "notifications.email")',
|
|
18
|
+
}),
|
|
19
|
+
|
|
20
|
+
// Scope
|
|
21
|
+
scope: field.enum('SettingsScope'),
|
|
22
|
+
scopeId: field.string({
|
|
23
|
+
isOptional: true,
|
|
24
|
+
description: 'ID of scoped entity (org, user, project)',
|
|
25
|
+
}),
|
|
26
|
+
|
|
27
|
+
// Value
|
|
28
|
+
value: field.json({ description: 'Setting value' }),
|
|
29
|
+
valueType: field.string({
|
|
30
|
+
default: '"string"',
|
|
31
|
+
description: 'Type hint for value',
|
|
32
|
+
}),
|
|
33
|
+
|
|
34
|
+
// Schema
|
|
35
|
+
schema: field.json({
|
|
36
|
+
isOptional: true,
|
|
37
|
+
description: 'JSON schema for validation',
|
|
38
|
+
}),
|
|
39
|
+
|
|
40
|
+
// Metadata
|
|
41
|
+
description: field.string({ isOptional: true }),
|
|
42
|
+
isSecret: field.boolean({
|
|
43
|
+
default: false,
|
|
44
|
+
description: 'Whether value should be encrypted',
|
|
45
|
+
}),
|
|
46
|
+
|
|
47
|
+
// Timestamps
|
|
48
|
+
createdAt: field.createdAt(),
|
|
49
|
+
updatedAt: field.updatedAt(),
|
|
50
|
+
},
|
|
51
|
+
indexes: [
|
|
52
|
+
index.unique(['scope', 'scopeId', 'key']),
|
|
53
|
+
index.on(['scope', 'key']),
|
|
54
|
+
],
|
|
55
|
+
enums: [SettingsScopeEnum],
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* FeatureFlag entity - feature toggles.
|
|
60
|
+
*/
|
|
61
|
+
export const FeatureFlagEntity = defineEntity({
|
|
62
|
+
name: 'FeatureFlag',
|
|
63
|
+
description: 'Feature flags for progressive rollout.',
|
|
64
|
+
schema: 'saas_app',
|
|
65
|
+
map: 'feature_flag',
|
|
66
|
+
fields: {
|
|
67
|
+
id: field.id(),
|
|
68
|
+
key: field.string({ isUnique: true, description: 'Feature flag key' }),
|
|
69
|
+
name: field.string({ description: 'Human-readable name' }),
|
|
70
|
+
description: field.string({ isOptional: true }),
|
|
71
|
+
|
|
72
|
+
// Status
|
|
73
|
+
enabled: field.boolean({ default: false }),
|
|
74
|
+
|
|
75
|
+
// Targeting
|
|
76
|
+
defaultValue: field.boolean({ default: false }),
|
|
77
|
+
rules: field.json({ isOptional: true, description: 'Targeting rules' }),
|
|
78
|
+
|
|
79
|
+
// Rollout
|
|
80
|
+
rolloutPercentage: field.int({
|
|
81
|
+
default: 0,
|
|
82
|
+
description: 'Percentage rollout (0-100)',
|
|
83
|
+
}),
|
|
84
|
+
|
|
85
|
+
// Timestamps
|
|
86
|
+
createdAt: field.createdAt(),
|
|
87
|
+
updatedAt: field.updatedAt(),
|
|
88
|
+
},
|
|
89
|
+
});
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { defineEntityEnum } from '@contractspec/lib.schema';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Settings scope enum.
|
|
5
|
+
*/
|
|
6
|
+
export const SettingsScopeEnum = defineEntityEnum({
|
|
7
|
+
name: 'SettingsScope',
|
|
8
|
+
values: ['APP', 'ORG', 'USER', 'PROJECT'] as const,
|
|
9
|
+
schema: 'saas_app',
|
|
10
|
+
description: 'Scope of a setting.',
|
|
11
|
+
});
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared mock data for saas-boilerplate handlers.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
// ============ Project Mock Data ============
|
|
6
|
+
|
|
7
|
+
export const MOCK_PROJECTS = [
|
|
8
|
+
{
|
|
9
|
+
id: 'proj-1',
|
|
10
|
+
name: 'Marketing Website',
|
|
11
|
+
description: 'Main company website redesign project',
|
|
12
|
+
slug: 'marketing-website',
|
|
13
|
+
organizationId: 'demo-org',
|
|
14
|
+
createdBy: 'user-1',
|
|
15
|
+
status: 'ACTIVE' as const,
|
|
16
|
+
isPublic: false,
|
|
17
|
+
tags: ['marketing', 'website', 'redesign'],
|
|
18
|
+
createdAt: new Date('2024-01-15T10:00:00Z'),
|
|
19
|
+
updatedAt: new Date('2024-03-20T14:30:00Z'),
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
id: 'proj-2',
|
|
23
|
+
name: 'Mobile App v2',
|
|
24
|
+
description: 'Next generation mobile application',
|
|
25
|
+
slug: 'mobile-app-v2',
|
|
26
|
+
organizationId: 'demo-org',
|
|
27
|
+
createdBy: 'user-2',
|
|
28
|
+
status: 'ACTIVE' as const,
|
|
29
|
+
isPublic: false,
|
|
30
|
+
tags: ['mobile', 'app', 'v2'],
|
|
31
|
+
createdAt: new Date('2024-02-01T09:00:00Z'),
|
|
32
|
+
updatedAt: new Date('2024-04-05T11:15:00Z'),
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
id: 'proj-3',
|
|
36
|
+
name: 'API Integration',
|
|
37
|
+
description: 'Third-party API integration project',
|
|
38
|
+
slug: 'api-integration',
|
|
39
|
+
organizationId: 'demo-org',
|
|
40
|
+
createdBy: 'user-1',
|
|
41
|
+
status: 'DRAFT' as const,
|
|
42
|
+
isPublic: false,
|
|
43
|
+
tags: ['api', 'integration'],
|
|
44
|
+
createdAt: new Date('2024-03-10T08:00:00Z'),
|
|
45
|
+
updatedAt: new Date('2024-03-10T08:00:00Z'),
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
id: 'proj-4',
|
|
49
|
+
name: 'Analytics Dashboard',
|
|
50
|
+
description: 'Internal analytics and reporting dashboard',
|
|
51
|
+
slug: 'analytics-dashboard',
|
|
52
|
+
organizationId: 'demo-org',
|
|
53
|
+
createdBy: 'user-3',
|
|
54
|
+
status: 'ARCHIVED' as const,
|
|
55
|
+
isPublic: true,
|
|
56
|
+
tags: ['analytics', 'dashboard', 'reporting'],
|
|
57
|
+
createdAt: new Date('2023-10-01T12:00:00Z'),
|
|
58
|
+
updatedAt: new Date('2024-02-28T16:45:00Z'),
|
|
59
|
+
},
|
|
60
|
+
];
|
|
61
|
+
|
|
62
|
+
// ============ Subscription Mock Data ============
|
|
63
|
+
|
|
64
|
+
export const MOCK_SUBSCRIPTION = {
|
|
65
|
+
id: 'sub-1',
|
|
66
|
+
organizationId: 'demo-org',
|
|
67
|
+
planId: 'pro',
|
|
68
|
+
planName: 'Professional',
|
|
69
|
+
status: 'ACTIVE' as const,
|
|
70
|
+
currentPeriodStart: new Date('2024-04-01T00:00:00Z'),
|
|
71
|
+
currentPeriodEnd: new Date('2024-05-01T00:00:00Z'),
|
|
72
|
+
limits: {
|
|
73
|
+
projects: 25,
|
|
74
|
+
users: 10,
|
|
75
|
+
storage: 50, // GB
|
|
76
|
+
apiCalls: 100000,
|
|
77
|
+
},
|
|
78
|
+
usage: {
|
|
79
|
+
projects: 4,
|
|
80
|
+
users: 5,
|
|
81
|
+
storage: 12.5,
|
|
82
|
+
apiCalls: 45230,
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
// ============ Usage Summary Mock Data ============
|
|
87
|
+
|
|
88
|
+
export const MOCK_USAGE_SUMMARY = {
|
|
89
|
+
organizationId: 'demo-org',
|
|
90
|
+
period: 'current_month',
|
|
91
|
+
apiCalls: {
|
|
92
|
+
total: 45230,
|
|
93
|
+
limit: 100000,
|
|
94
|
+
percentUsed: 45.23,
|
|
95
|
+
},
|
|
96
|
+
storage: {
|
|
97
|
+
totalGb: 12.5,
|
|
98
|
+
limitGb: 50,
|
|
99
|
+
percentUsed: 25,
|
|
100
|
+
},
|
|
101
|
+
activeProjects: 4,
|
|
102
|
+
activeUsers: 5,
|
|
103
|
+
breakdown: [
|
|
104
|
+
{ date: '2024-04-01', apiCalls: 3200, storageGb: 12.1 },
|
|
105
|
+
{ date: '2024-04-02', apiCalls: 2800, storageGb: 12.2 },
|
|
106
|
+
{ date: '2024-04-03', apiCalls: 4100, storageGb: 12.3 },
|
|
107
|
+
{ date: '2024-04-04', apiCalls: 3600, storageGb: 12.4 },
|
|
108
|
+
{ date: '2024-04-05', apiCalls: 3800, storageGb: 12.5 },
|
|
109
|
+
],
|
|
110
|
+
};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
export interface OverlayDefinition {
|
|
2
|
+
overlayId: string;
|
|
3
|
+
version: string;
|
|
4
|
+
description: string;
|
|
5
|
+
appliesTo: Record<string, string>;
|
|
6
|
+
modifications: OverlayModification[];
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export type OverlayModification =
|
|
10
|
+
| HideFieldModification
|
|
11
|
+
| RenameLabelModification
|
|
12
|
+
| AddBadgeModification
|
|
13
|
+
| SetLimitModification;
|
|
14
|
+
|
|
15
|
+
export interface HideFieldModification {
|
|
16
|
+
type: 'hideField';
|
|
17
|
+
field: string;
|
|
18
|
+
reason?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface RenameLabelModification {
|
|
22
|
+
type: 'renameLabel';
|
|
23
|
+
field: string;
|
|
24
|
+
newLabel: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface AddBadgeModification {
|
|
28
|
+
type: 'addBadge';
|
|
29
|
+
position: 'header' | 'footer';
|
|
30
|
+
label: string;
|
|
31
|
+
variant: 'warning' | 'info' | 'error' | 'success' | 'default';
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface SetLimitModification {
|
|
35
|
+
type: 'setLimit';
|
|
36
|
+
field: string;
|
|
37
|
+
max: number;
|
|
38
|
+
message: string;
|
|
39
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { defineTestSpec } from '@contractspec/lib.contracts';
|
|
2
|
+
|
|
3
|
+
export const ProjectListTest = defineTestSpec({
|
|
4
|
+
meta: {
|
|
5
|
+
key: 'saas.project.list.test',
|
|
6
|
+
version: '1.0.0',
|
|
7
|
+
stability: 'experimental',
|
|
8
|
+
owners: ['@example.saas-boilerplate'],
|
|
9
|
+
description: 'Test for listing projects',
|
|
10
|
+
tags: ['test'],
|
|
11
|
+
},
|
|
12
|
+
target: {
|
|
13
|
+
type: 'operation',
|
|
14
|
+
operation: { key: 'saas.project.list', version: '1.0.0' },
|
|
15
|
+
},
|
|
16
|
+
scenarios: [
|
|
17
|
+
{
|
|
18
|
+
key: 'success',
|
|
19
|
+
when: { operation: { key: 'saas.project.list' } },
|
|
20
|
+
then: [{ type: 'expectOutput', match: {} }],
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
key: 'error',
|
|
24
|
+
when: { operation: { key: 'saas.project.list' } },
|
|
25
|
+
then: [{ type: 'expectError' }],
|
|
26
|
+
},
|
|
27
|
+
],
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
export const ProjectGetTest = defineTestSpec({
|
|
31
|
+
meta: {
|
|
32
|
+
key: 'saas.project.get.test',
|
|
33
|
+
version: '1.0.0',
|
|
34
|
+
stability: 'experimental',
|
|
35
|
+
owners: ['@example.saas-boilerplate'],
|
|
36
|
+
description: 'Test for getting project',
|
|
37
|
+
tags: ['test'],
|
|
38
|
+
},
|
|
39
|
+
target: {
|
|
40
|
+
type: 'operation',
|
|
41
|
+
operation: { key: 'saas.project.get', version: '1.0.0' },
|
|
42
|
+
},
|
|
43
|
+
scenarios: [
|
|
44
|
+
{
|
|
45
|
+
key: 'success',
|
|
46
|
+
when: { operation: { key: 'saas.project.get' } },
|
|
47
|
+
then: [{ type: 'expectOutput', match: {} }],
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
key: 'error',
|
|
51
|
+
when: { operation: { key: 'saas.project.get' } },
|
|
52
|
+
then: [{ type: 'expectError' }],
|
|
53
|
+
},
|
|
54
|
+
],
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
export const BillingSubscriptionGetTest = defineTestSpec({
|
|
58
|
+
meta: {
|
|
59
|
+
key: 'saas.billing.subscription.get.test',
|
|
60
|
+
version: '1.0.0',
|
|
61
|
+
stability: 'experimental',
|
|
62
|
+
owners: ['@example.saas-boilerplate'],
|
|
63
|
+
description: 'Test for getting subscription',
|
|
64
|
+
tags: ['test'],
|
|
65
|
+
},
|
|
66
|
+
target: {
|
|
67
|
+
type: 'operation',
|
|
68
|
+
operation: { key: 'saas.billing.subscription.get', version: '1.0.0' },
|
|
69
|
+
},
|
|
70
|
+
scenarios: [
|
|
71
|
+
{
|
|
72
|
+
key: 'success',
|
|
73
|
+
when: { operation: { key: 'saas.billing.subscription.get' } },
|
|
74
|
+
then: [{ type: 'expectOutput', match: {} }],
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
key: 'error',
|
|
78
|
+
when: { operation: { key: 'saas.billing.subscription.get' } },
|
|
79
|
+
then: [{ type: 'expectError' }],
|
|
80
|
+
},
|
|
81
|
+
],
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
export const BillingUsageSummaryTest = defineTestSpec({
|
|
85
|
+
meta: {
|
|
86
|
+
key: 'saas.billing.usage.summary.test',
|
|
87
|
+
version: '1.0.0',
|
|
88
|
+
stability: 'experimental',
|
|
89
|
+
owners: ['@example.saas-boilerplate'],
|
|
90
|
+
description: 'Test for getting usage summary',
|
|
91
|
+
tags: ['test'],
|
|
92
|
+
},
|
|
93
|
+
target: {
|
|
94
|
+
type: 'operation',
|
|
95
|
+
operation: { key: 'saas.billing.usage.summary', version: '1.0.0' },
|
|
96
|
+
},
|
|
97
|
+
scenarios: [
|
|
98
|
+
{
|
|
99
|
+
key: 'success',
|
|
100
|
+
when: { operation: { key: 'saas.billing.usage.summary' } },
|
|
101
|
+
then: [{ type: 'expectOutput', match: {} }],
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
key: 'error',
|
|
105
|
+
when: { operation: { key: 'saas.billing.usage.summary' } },
|
|
106
|
+
then: [{ type: 'expectError' }],
|
|
107
|
+
},
|
|
108
|
+
],
|
|
109
|
+
});
|
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* SaaS Dashboard
|
|
5
|
+
*
|
|
6
|
+
* Fully integrated with ContractSpec example handlers
|
|
7
|
+
* and design-system components.
|
|
8
|
+
*
|
|
9
|
+
* Commands wired:
|
|
10
|
+
* - CreateProjectContract -> Create Project button + modal
|
|
11
|
+
* - UpdateProjectContract -> Edit project via modal
|
|
12
|
+
* - DeleteProjectContract -> Delete project via modal
|
|
13
|
+
*/
|
|
14
|
+
import { useState, useCallback } from 'react';
|
|
15
|
+
import {
|
|
16
|
+
StatCard,
|
|
17
|
+
StatCardGroup,
|
|
18
|
+
StatusChip,
|
|
19
|
+
EntityCard,
|
|
20
|
+
EmptyState,
|
|
21
|
+
LoaderBlock,
|
|
22
|
+
ErrorState,
|
|
23
|
+
Button,
|
|
24
|
+
} from '@contractspec/lib.design-system';
|
|
25
|
+
import {
|
|
26
|
+
useProjectList,
|
|
27
|
+
type Project,
|
|
28
|
+
type Subscription,
|
|
29
|
+
} from './hooks/useProjectList';
|
|
30
|
+
import { useProjectMutations } from './hooks/useProjectMutations';
|
|
31
|
+
import { CreateProjectModal } from './modals/CreateProjectModal';
|
|
32
|
+
import { ProjectActionsModal } from './modals/ProjectActionsModal';
|
|
33
|
+
|
|
34
|
+
type Tab = 'projects' | 'billing' | 'settings';
|
|
35
|
+
|
|
36
|
+
function getStatusTone(
|
|
37
|
+
status: Project['status']
|
|
38
|
+
): 'success' | 'warning' | 'neutral' | 'danger' {
|
|
39
|
+
switch (status) {
|
|
40
|
+
case 'ACTIVE':
|
|
41
|
+
return 'success';
|
|
42
|
+
case 'DRAFT':
|
|
43
|
+
return 'neutral';
|
|
44
|
+
case 'ARCHIVED':
|
|
45
|
+
return 'warning';
|
|
46
|
+
default:
|
|
47
|
+
return 'neutral';
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function SaasDashboard() {
|
|
52
|
+
const [activeTab, setActiveTab] = useState<Tab>('projects');
|
|
53
|
+
const [isCreateModalOpen, setIsCreateModalOpen] = useState(false);
|
|
54
|
+
const [selectedProject, setSelectedProject] = useState<Project | null>(null);
|
|
55
|
+
const [isProjectActionsOpen, setIsProjectActionsOpen] = useState(false);
|
|
56
|
+
|
|
57
|
+
const { data, subscription, loading, error, stats, refetch } =
|
|
58
|
+
useProjectList();
|
|
59
|
+
|
|
60
|
+
const mutations = useProjectMutations({
|
|
61
|
+
onSuccess: () => {
|
|
62
|
+
refetch();
|
|
63
|
+
},
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
const handleProjectClick = useCallback((project: Project) => {
|
|
67
|
+
setSelectedProject(project);
|
|
68
|
+
setIsProjectActionsOpen(true);
|
|
69
|
+
}, []);
|
|
70
|
+
|
|
71
|
+
const tabs: { id: Tab; label: string; icon: string }[] = [
|
|
72
|
+
{ id: 'projects', label: 'Projects', icon: '📁' },
|
|
73
|
+
{ id: 'billing', label: 'Billing', icon: '💳' },
|
|
74
|
+
{ id: 'settings', label: 'Settings', icon: '⚙️' },
|
|
75
|
+
];
|
|
76
|
+
|
|
77
|
+
if (loading && !data) {
|
|
78
|
+
return <LoaderBlock label="Loading dashboard..." />;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (error) {
|
|
82
|
+
return (
|
|
83
|
+
<ErrorState
|
|
84
|
+
title="Failed to load dashboard"
|
|
85
|
+
description={error.message}
|
|
86
|
+
onRetry={refetch}
|
|
87
|
+
retryLabel="Retry"
|
|
88
|
+
/>
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return (
|
|
93
|
+
<div className="space-y-6">
|
|
94
|
+
{/* Header */}
|
|
95
|
+
<div className="flex items-center justify-between">
|
|
96
|
+
<h2 className="text-2xl font-bold">SaaS Dashboard</h2>
|
|
97
|
+
{activeTab === 'projects' && (
|
|
98
|
+
<Button onPress={() => setIsCreateModalOpen(true)}>
|
|
99
|
+
<span className="mr-2">+</span> New Project
|
|
100
|
+
</Button>
|
|
101
|
+
)}
|
|
102
|
+
</div>
|
|
103
|
+
|
|
104
|
+
{/* Stats Row */}
|
|
105
|
+
{stats && subscription && (
|
|
106
|
+
<StatCardGroup>
|
|
107
|
+
<StatCard label="Projects" value={stats.total.toString()} />
|
|
108
|
+
<StatCard label="Active" value={stats.activeCount.toString()} />
|
|
109
|
+
<StatCard label="Draft" value={stats.draftCount.toString()} />
|
|
110
|
+
<StatCard
|
|
111
|
+
label="Plan"
|
|
112
|
+
value={subscription.plan}
|
|
113
|
+
hint={subscription.status}
|
|
114
|
+
/>
|
|
115
|
+
</StatCardGroup>
|
|
116
|
+
)}
|
|
117
|
+
|
|
118
|
+
{/* Navigation Tabs */}
|
|
119
|
+
<nav className="bg-muted flex gap-1 rounded-lg p-1" role="tablist">
|
|
120
|
+
{tabs.map((tab) => (
|
|
121
|
+
<button
|
|
122
|
+
key={tab.id}
|
|
123
|
+
type="button"
|
|
124
|
+
role="tab"
|
|
125
|
+
aria-selected={activeTab === tab.id}
|
|
126
|
+
onClick={() => setActiveTab(tab.id)}
|
|
127
|
+
className={`flex flex-1 items-center justify-center gap-2 rounded-md px-4 py-2 text-sm font-medium transition-colors ${
|
|
128
|
+
activeTab === tab.id
|
|
129
|
+
? 'bg-background text-foreground shadow-sm'
|
|
130
|
+
: 'text-muted-foreground hover:text-foreground'
|
|
131
|
+
}`}
|
|
132
|
+
>
|
|
133
|
+
<span>{tab.icon}</span>
|
|
134
|
+
{tab.label}
|
|
135
|
+
</button>
|
|
136
|
+
))}
|
|
137
|
+
</nav>
|
|
138
|
+
|
|
139
|
+
{/* Tab Content */}
|
|
140
|
+
<div className="min-h-[400px]" role="tabpanel">
|
|
141
|
+
{activeTab === 'projects' && (
|
|
142
|
+
<ProjectsTab data={data} onProjectClick={handleProjectClick} />
|
|
143
|
+
)}
|
|
144
|
+
{activeTab === 'billing' && <BillingTab subscription={subscription} />}
|
|
145
|
+
{activeTab === 'settings' && <SettingsTab />}
|
|
146
|
+
</div>
|
|
147
|
+
|
|
148
|
+
{/* Create Project Modal */}
|
|
149
|
+
<CreateProjectModal
|
|
150
|
+
isOpen={isCreateModalOpen}
|
|
151
|
+
onClose={() => setIsCreateModalOpen(false)}
|
|
152
|
+
onSubmit={async (input) => {
|
|
153
|
+
await mutations.createProject(input);
|
|
154
|
+
}}
|
|
155
|
+
isLoading={mutations.createState.loading}
|
|
156
|
+
/>
|
|
157
|
+
|
|
158
|
+
{/* Project Actions Modal */}
|
|
159
|
+
<ProjectActionsModal
|
|
160
|
+
isOpen={isProjectActionsOpen}
|
|
161
|
+
project={selectedProject}
|
|
162
|
+
onClose={() => {
|
|
163
|
+
setIsProjectActionsOpen(false);
|
|
164
|
+
setSelectedProject(null);
|
|
165
|
+
}}
|
|
166
|
+
onUpdate={async (input) => {
|
|
167
|
+
await mutations.updateProject(input);
|
|
168
|
+
}}
|
|
169
|
+
onArchive={async (projectId) => {
|
|
170
|
+
await mutations.archiveProject(projectId);
|
|
171
|
+
}}
|
|
172
|
+
onActivate={async (projectId) => {
|
|
173
|
+
await mutations.activateProject(projectId);
|
|
174
|
+
}}
|
|
175
|
+
onDelete={async (projectId) => {
|
|
176
|
+
await mutations.deleteProject(projectId);
|
|
177
|
+
}}
|
|
178
|
+
isLoading={mutations.isLoading}
|
|
179
|
+
/>
|
|
180
|
+
</div>
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
interface ProjectsTabProps {
|
|
185
|
+
data: ReturnType<typeof useProjectList>['data'];
|
|
186
|
+
onProjectClick?: (project: Project) => void;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function ProjectsTab({ data, onProjectClick }: ProjectsTabProps) {
|
|
190
|
+
if (!data?.items.length) {
|
|
191
|
+
return (
|
|
192
|
+
<EmptyState
|
|
193
|
+
title="No projects yet"
|
|
194
|
+
description="Create your first project to get started."
|
|
195
|
+
/>
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return (
|
|
200
|
+
<div className="space-y-4">
|
|
201
|
+
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
|
202
|
+
{data.items.map((project: Project) => (
|
|
203
|
+
<EntityCard
|
|
204
|
+
key={project.id}
|
|
205
|
+
cardTitle={project.name}
|
|
206
|
+
cardSubtitle={project.tier}
|
|
207
|
+
meta={
|
|
208
|
+
<p className="text-muted-foreground text-sm">
|
|
209
|
+
{project.description}
|
|
210
|
+
</p>
|
|
211
|
+
}
|
|
212
|
+
chips={
|
|
213
|
+
<StatusChip
|
|
214
|
+
tone={getStatusTone(project.status)}
|
|
215
|
+
label={project.status}
|
|
216
|
+
/>
|
|
217
|
+
}
|
|
218
|
+
footer={
|
|
219
|
+
<div className="flex w-full items-center justify-between">
|
|
220
|
+
<span className="text-muted-foreground text-xs">
|
|
221
|
+
{project.updatedAt.toLocaleDateString()}
|
|
222
|
+
</span>
|
|
223
|
+
<Button
|
|
224
|
+
variant="ghost"
|
|
225
|
+
size="sm"
|
|
226
|
+
onPress={() => onProjectClick?.(project)}
|
|
227
|
+
>
|
|
228
|
+
Actions
|
|
229
|
+
</Button>
|
|
230
|
+
</div>
|
|
231
|
+
}
|
|
232
|
+
/>
|
|
233
|
+
))}
|
|
234
|
+
</div>
|
|
235
|
+
</div>
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function BillingTab({ subscription }: { subscription: Subscription | null }) {
|
|
240
|
+
if (!subscription) return null;
|
|
241
|
+
|
|
242
|
+
return (
|
|
243
|
+
<div className="space-y-6">
|
|
244
|
+
<div className="border-border bg-card rounded-xl border p-6">
|
|
245
|
+
<div className="flex items-start justify-between">
|
|
246
|
+
<div>
|
|
247
|
+
<h3 className="text-lg font-semibold">{subscription.plan} Plan</h3>
|
|
248
|
+
<p className="text-muted-foreground text-sm">
|
|
249
|
+
Current period:{' '}
|
|
250
|
+
{subscription.currentPeriodStart.toLocaleDateString()} -{' '}
|
|
251
|
+
{subscription.currentPeriodEnd.toLocaleDateString()}
|
|
252
|
+
</p>
|
|
253
|
+
<p className="text-muted-foreground text-sm">
|
|
254
|
+
Billing cycle: {subscription.billingCycle}
|
|
255
|
+
</p>
|
|
256
|
+
</div>
|
|
257
|
+
<StatusChip tone="success" label={subscription.status} />
|
|
258
|
+
</div>
|
|
259
|
+
|
|
260
|
+
<div className="mt-4 flex gap-3">
|
|
261
|
+
<Button variant="outline" onPress={() => alert('Upgrade clicked!')}>
|
|
262
|
+
Upgrade Plan
|
|
263
|
+
</Button>
|
|
264
|
+
<Button
|
|
265
|
+
variant="ghost"
|
|
266
|
+
onPress={() => alert('Manage Billing clicked!')}
|
|
267
|
+
>
|
|
268
|
+
Manage Billing
|
|
269
|
+
</Button>
|
|
270
|
+
</div>
|
|
271
|
+
</div>
|
|
272
|
+
|
|
273
|
+
{subscription.cancelAtPeriodEnd && (
|
|
274
|
+
<div className="border-border bg-destructive/10 text-destructive rounded-xl border p-4">
|
|
275
|
+
<p className="text-sm font-medium">
|
|
276
|
+
⚠️ Your subscription will be cancelled at the end of the current
|
|
277
|
+
period.
|
|
278
|
+
</p>
|
|
279
|
+
</div>
|
|
280
|
+
)}
|
|
281
|
+
</div>
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function SettingsTab() {
|
|
286
|
+
return (
|
|
287
|
+
<div className="space-y-6">
|
|
288
|
+
<div className="border-border bg-card rounded-xl border p-6">
|
|
289
|
+
<h3 className="mb-4 text-lg font-semibold">Organization Settings</h3>
|
|
290
|
+
<div className="space-y-4">
|
|
291
|
+
<div>
|
|
292
|
+
<label htmlFor="org-name" className="text-sm font-medium">
|
|
293
|
+
Organization Name
|
|
294
|
+
</label>
|
|
295
|
+
<input
|
|
296
|
+
id="org-name"
|
|
297
|
+
type="text"
|
|
298
|
+
defaultValue="Demo Organization"
|
|
299
|
+
className="border-input bg-background mt-1 block w-full rounded-md border px-3 py-2"
|
|
300
|
+
/>
|
|
301
|
+
</div>
|
|
302
|
+
<div>
|
|
303
|
+
<label htmlFor="timezone" className="text-sm font-medium">
|
|
304
|
+
Default Timezone
|
|
305
|
+
</label>
|
|
306
|
+
<select
|
|
307
|
+
id="timezone"
|
|
308
|
+
className="border-input bg-background mt-1 block w-full rounded-md border px-3 py-2"
|
|
309
|
+
>
|
|
310
|
+
<option>UTC</option>
|
|
311
|
+
<option>America/New_York</option>
|
|
312
|
+
<option>Europe/London</option>
|
|
313
|
+
<option>Asia/Tokyo</option>
|
|
314
|
+
</select>
|
|
315
|
+
</div>
|
|
316
|
+
<div className="pt-2">
|
|
317
|
+
<Button onPress={() => alert('Settings saved!')}>
|
|
318
|
+
Save Settings
|
|
319
|
+
</Button>
|
|
320
|
+
</div>
|
|
321
|
+
</div>
|
|
322
|
+
</div>
|
|
323
|
+
</div>
|
|
324
|
+
);
|
|
325
|
+
}
|