@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,113 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* SaaS Project List - Standalone project list component
|
|
5
|
+
*/
|
|
6
|
+
import {
|
|
7
|
+
StatCard,
|
|
8
|
+
StatCardGroup,
|
|
9
|
+
StatusChip,
|
|
10
|
+
EntityCard,
|
|
11
|
+
EmptyState,
|
|
12
|
+
LoaderBlock,
|
|
13
|
+
ErrorState,
|
|
14
|
+
Button,
|
|
15
|
+
} from '@contractspec/lib.design-system';
|
|
16
|
+
import { useProjectList, type Project } from './hooks/useProjectList';
|
|
17
|
+
|
|
18
|
+
interface SaasProjectListProps {
|
|
19
|
+
onProjectClick?: (projectId: string) => void;
|
|
20
|
+
onCreateProject?: () => void;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function getStatusTone(
|
|
24
|
+
status: Project['status']
|
|
25
|
+
): 'success' | 'warning' | 'neutral' | 'danger' {
|
|
26
|
+
switch (status) {
|
|
27
|
+
case 'ACTIVE':
|
|
28
|
+
return 'success';
|
|
29
|
+
case 'DRAFT':
|
|
30
|
+
return 'neutral';
|
|
31
|
+
case 'ARCHIVED':
|
|
32
|
+
return 'danger';
|
|
33
|
+
default:
|
|
34
|
+
return 'neutral';
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function SaasProjectList({
|
|
39
|
+
onProjectClick,
|
|
40
|
+
onCreateProject,
|
|
41
|
+
}: SaasProjectListProps) {
|
|
42
|
+
const { data, loading, error, stats, refetch } = useProjectList();
|
|
43
|
+
|
|
44
|
+
if (loading && !data) {
|
|
45
|
+
return <LoaderBlock label="Loading projects..." />;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (error) {
|
|
49
|
+
return (
|
|
50
|
+
<ErrorState
|
|
51
|
+
title="Failed to load projects"
|
|
52
|
+
description={error.message}
|
|
53
|
+
onRetry={refetch}
|
|
54
|
+
retryLabel="Retry"
|
|
55
|
+
/>
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (!data?.items.length) {
|
|
60
|
+
return (
|
|
61
|
+
<EmptyState
|
|
62
|
+
title="No projects found"
|
|
63
|
+
description="Create your first project to get started."
|
|
64
|
+
primaryAction={
|
|
65
|
+
onCreateProject ? (
|
|
66
|
+
<Button onPress={onCreateProject}>Create Project</Button>
|
|
67
|
+
) : undefined
|
|
68
|
+
}
|
|
69
|
+
/>
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return (
|
|
74
|
+
<div className="space-y-6">
|
|
75
|
+
{stats && (
|
|
76
|
+
<StatCardGroup>
|
|
77
|
+
<StatCard label="Total Projects" value={stats.total.toString()} />
|
|
78
|
+
<StatCard label="Active" value={stats.activeCount.toString()} />
|
|
79
|
+
<StatCard label="Draft" value={stats.draftCount.toString()} />
|
|
80
|
+
</StatCardGroup>
|
|
81
|
+
)}
|
|
82
|
+
|
|
83
|
+
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
|
84
|
+
{data.items.map((project: Project) => (
|
|
85
|
+
<EntityCard
|
|
86
|
+
key={project.id}
|
|
87
|
+
cardTitle={project.name}
|
|
88
|
+
cardSubtitle={project.tier}
|
|
89
|
+
meta={
|
|
90
|
+
<p className="text-muted-foreground text-sm">
|
|
91
|
+
{project.description}
|
|
92
|
+
</p>
|
|
93
|
+
}
|
|
94
|
+
chips={
|
|
95
|
+
<StatusChip
|
|
96
|
+
tone={getStatusTone(project.status)}
|
|
97
|
+
label={project.status}
|
|
98
|
+
/>
|
|
99
|
+
}
|
|
100
|
+
footer={
|
|
101
|
+
<span className="text-muted-foreground text-xs">
|
|
102
|
+
{project.updatedAt.toLocaleDateString()}
|
|
103
|
+
</span>
|
|
104
|
+
}
|
|
105
|
+
onClick={
|
|
106
|
+
onProjectClick ? () => onProjectClick(project.id) : undefined
|
|
107
|
+
}
|
|
108
|
+
/>
|
|
109
|
+
))}
|
|
110
|
+
</div>
|
|
111
|
+
</div>
|
|
112
|
+
);
|
|
113
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* SaaS Settings Panel - Organization and user settings
|
|
5
|
+
*/
|
|
6
|
+
import { useState } from 'react';
|
|
7
|
+
import { Button } from '@contractspec/lib.design-system';
|
|
8
|
+
|
|
9
|
+
export function SaasSettingsPanel() {
|
|
10
|
+
const [orgName, setOrgName] = useState('Demo Organization');
|
|
11
|
+
const [timezone, setTimezone] = useState('UTC');
|
|
12
|
+
|
|
13
|
+
return (
|
|
14
|
+
<div className="space-y-6">
|
|
15
|
+
<div className="border-border bg-card rounded-xl border p-6">
|
|
16
|
+
<h3 className="mb-4 text-lg font-semibold">Organization Settings</h3>
|
|
17
|
+
<div className="space-y-4">
|
|
18
|
+
<div>
|
|
19
|
+
<label
|
|
20
|
+
htmlFor="setting-org-name"
|
|
21
|
+
className="block text-sm font-medium"
|
|
22
|
+
>
|
|
23
|
+
Organization Name
|
|
24
|
+
</label>
|
|
25
|
+
<input
|
|
26
|
+
id="setting-org-name"
|
|
27
|
+
type="text"
|
|
28
|
+
value={orgName}
|
|
29
|
+
onChange={(e) => setOrgName(e.target.value)}
|
|
30
|
+
className="border-input bg-background mt-1 block w-full rounded-md border px-3 py-2"
|
|
31
|
+
/>
|
|
32
|
+
</div>
|
|
33
|
+
<div>
|
|
34
|
+
<label
|
|
35
|
+
htmlFor="setting-timezone"
|
|
36
|
+
className="block text-sm font-medium"
|
|
37
|
+
>
|
|
38
|
+
Default Timezone
|
|
39
|
+
</label>
|
|
40
|
+
<select
|
|
41
|
+
id="setting-timezone"
|
|
42
|
+
value={timezone}
|
|
43
|
+
onChange={(e) => setTimezone(e.target.value)}
|
|
44
|
+
className="border-input bg-background mt-1 block w-full rounded-md border px-3 py-2"
|
|
45
|
+
>
|
|
46
|
+
<option value="UTC">UTC</option>
|
|
47
|
+
<option value="America/New_York">America/New_York</option>
|
|
48
|
+
<option value="Europe/London">Europe/London</option>
|
|
49
|
+
<option value="Asia/Tokyo">Asia/Tokyo</option>
|
|
50
|
+
</select>
|
|
51
|
+
</div>
|
|
52
|
+
</div>
|
|
53
|
+
<div className="mt-6">
|
|
54
|
+
<Button variant="default">Save Changes</Button>
|
|
55
|
+
</div>
|
|
56
|
+
</div>
|
|
57
|
+
|
|
58
|
+
<div className="border-border bg-card rounded-xl border p-6">
|
|
59
|
+
<h3 className="mb-4 text-lg font-semibold">Notifications</h3>
|
|
60
|
+
<div className="space-y-3">
|
|
61
|
+
{[
|
|
62
|
+
{ label: 'Email notifications', defaultChecked: true },
|
|
63
|
+
{ label: 'Usage alerts', defaultChecked: true },
|
|
64
|
+
{ label: 'Weekly digest', defaultChecked: false },
|
|
65
|
+
].map((item) => (
|
|
66
|
+
<label key={item.label} className="flex items-center gap-3">
|
|
67
|
+
<input
|
|
68
|
+
type="checkbox"
|
|
69
|
+
defaultChecked={item.defaultChecked}
|
|
70
|
+
className="border-input h-4 w-4 rounded"
|
|
71
|
+
/>
|
|
72
|
+
<span className="text-sm">{item.label}</span>
|
|
73
|
+
</label>
|
|
74
|
+
))}
|
|
75
|
+
</div>
|
|
76
|
+
</div>
|
|
77
|
+
|
|
78
|
+
<div className="rounded-xl border border-red-200 bg-red-50 p-6 dark:border-red-900 dark:bg-red-950/20">
|
|
79
|
+
<h3 className="mb-2 text-lg font-semibold text-red-700 dark:text-red-400">
|
|
80
|
+
Danger Zone
|
|
81
|
+
</h3>
|
|
82
|
+
<p className="mb-4 text-sm text-red-600 dark:text-red-300">
|
|
83
|
+
These actions are irreversible. Please proceed with caution.
|
|
84
|
+
</p>
|
|
85
|
+
<div className="flex gap-3">
|
|
86
|
+
<Button variant="secondary" size="sm">
|
|
87
|
+
Export Data
|
|
88
|
+
</Button>
|
|
89
|
+
<Button variant="secondary" size="sm">
|
|
90
|
+
Delete Organization
|
|
91
|
+
</Button>
|
|
92
|
+
</div>
|
|
93
|
+
</div>
|
|
94
|
+
</div>
|
|
95
|
+
);
|
|
96
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
export { useProjectList, type UseProjectListOptions } from './useProjectList';
|
|
4
|
+
export {
|
|
5
|
+
useProjectMutations,
|
|
6
|
+
type UseProjectMutationsOptions,
|
|
7
|
+
} from './useProjectMutations';
|
|
8
|
+
|
|
9
|
+
// Note: For project types (CreateProjectInput, UpdateProjectInput, Project), import directly from:
|
|
10
|
+
// import type { Project, CreateProjectInput, etc. } from '@contractspec/example.saas-boilerplate/handlers';
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hook for fetching and managing project list data
|
|
3
|
+
*
|
|
4
|
+
* Uses runtime-local database-backed handlers.
|
|
5
|
+
*/
|
|
6
|
+
import { useCallback, useEffect, useMemo, useState } from 'react';
|
|
7
|
+
import { useTemplateRuntime } from '@contractspec/lib.example-shared-ui';
|
|
8
|
+
import type {
|
|
9
|
+
Project as RuntimeProject,
|
|
10
|
+
Subscription as RuntimeSubscription,
|
|
11
|
+
SaasHandlers,
|
|
12
|
+
} from '../../handlers/saas.handlers';
|
|
13
|
+
|
|
14
|
+
// Re-export types for convenience
|
|
15
|
+
export type Project = RuntimeProject;
|
|
16
|
+
export type Subscription = RuntimeSubscription;
|
|
17
|
+
|
|
18
|
+
export interface ListProjectsOutput {
|
|
19
|
+
items: Project[];
|
|
20
|
+
total: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface UseProjectListOptions {
|
|
24
|
+
status?: 'DRAFT' | 'ACTIVE' | 'ARCHIVED' | 'all';
|
|
25
|
+
search?: string;
|
|
26
|
+
limit?: number;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function useProjectList(options: UseProjectListOptions = {}) {
|
|
30
|
+
const { handlers, projectId } = useTemplateRuntime<{ saas: SaasHandlers }>();
|
|
31
|
+
const { saas } = handlers;
|
|
32
|
+
|
|
33
|
+
const [data, setData] = useState<ListProjectsOutput | null>(null);
|
|
34
|
+
const [subscription, setSubscription] = useState<Subscription | null>(null);
|
|
35
|
+
const [loading, setLoading] = useState(true);
|
|
36
|
+
const [error, setError] = useState<Error | null>(null);
|
|
37
|
+
const [page, setPage] = useState(1);
|
|
38
|
+
|
|
39
|
+
const fetchData = useCallback(async () => {
|
|
40
|
+
setLoading(true);
|
|
41
|
+
setError(null);
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
const [projectsResult, subscriptionResult] = await Promise.all([
|
|
45
|
+
saas.listProjects({
|
|
46
|
+
projectId,
|
|
47
|
+
status: options.status === 'all' ? undefined : options.status,
|
|
48
|
+
search: options.search,
|
|
49
|
+
limit: options.limit ?? 20,
|
|
50
|
+
offset: (page - 1) * (options.limit ?? 20),
|
|
51
|
+
}),
|
|
52
|
+
saas.getSubscription({ projectId }),
|
|
53
|
+
]);
|
|
54
|
+
setData({
|
|
55
|
+
items: projectsResult.items,
|
|
56
|
+
total: projectsResult.total,
|
|
57
|
+
});
|
|
58
|
+
setSubscription(subscriptionResult);
|
|
59
|
+
} catch (err) {
|
|
60
|
+
setError(err instanceof Error ? err : new Error('Unknown error'));
|
|
61
|
+
} finally {
|
|
62
|
+
setLoading(false);
|
|
63
|
+
}
|
|
64
|
+
}, [saas, projectId, options.status, options.search, options.limit, page]);
|
|
65
|
+
|
|
66
|
+
useEffect(() => {
|
|
67
|
+
fetchData();
|
|
68
|
+
}, [fetchData]);
|
|
69
|
+
|
|
70
|
+
// Calculate stats
|
|
71
|
+
const stats = useMemo(() => {
|
|
72
|
+
if (!data) return null;
|
|
73
|
+
const items = data.items;
|
|
74
|
+
return {
|
|
75
|
+
total: data.total,
|
|
76
|
+
activeCount: items.filter((p) => p.status === 'ACTIVE').length,
|
|
77
|
+
draftCount: items.filter((p) => p.status === 'DRAFT').length,
|
|
78
|
+
// Subscription stats are optional since they may not be seeded
|
|
79
|
+
projectLimit: 10, // Default limit for demo
|
|
80
|
+
usagePercent: Math.min((data.total / 10) * 100, 100),
|
|
81
|
+
};
|
|
82
|
+
}, [data]);
|
|
83
|
+
|
|
84
|
+
return {
|
|
85
|
+
data,
|
|
86
|
+
subscription,
|
|
87
|
+
loading,
|
|
88
|
+
error,
|
|
89
|
+
stats,
|
|
90
|
+
page,
|
|
91
|
+
refetch: fetchData,
|
|
92
|
+
nextPage: () => setPage((p) => p + 1),
|
|
93
|
+
prevPage: () => page > 1 && setPage((p) => p - 1),
|
|
94
|
+
};
|
|
95
|
+
}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hook for SaaS project mutations (commands)
|
|
3
|
+
*
|
|
4
|
+
* Uses runtime-local database-backed handlers for:
|
|
5
|
+
* - CreateProjectContract
|
|
6
|
+
* - UpdateProjectContract
|
|
7
|
+
* - DeleteProjectContract
|
|
8
|
+
*/
|
|
9
|
+
import { useCallback, useState } from 'react';
|
|
10
|
+
import { useTemplateRuntime } from '@contractspec/lib.example-shared-ui';
|
|
11
|
+
import type {
|
|
12
|
+
CreateProjectInput,
|
|
13
|
+
Project,
|
|
14
|
+
UpdateProjectInput,
|
|
15
|
+
SaasHandlers,
|
|
16
|
+
} from '../../handlers/saas.handlers';
|
|
17
|
+
|
|
18
|
+
export interface MutationState<T> {
|
|
19
|
+
loading: boolean;
|
|
20
|
+
error: Error | null;
|
|
21
|
+
data: T | null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface UseProjectMutationsOptions {
|
|
25
|
+
onSuccess?: () => void;
|
|
26
|
+
onError?: (error: Error) => void;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function useProjectMutations(options: UseProjectMutationsOptions = {}) {
|
|
30
|
+
const { handlers, projectId } = useTemplateRuntime<{ saas: SaasHandlers }>();
|
|
31
|
+
const { saas } = handlers;
|
|
32
|
+
|
|
33
|
+
const [createState, setCreateState] = useState<MutationState<Project>>({
|
|
34
|
+
loading: false,
|
|
35
|
+
error: null,
|
|
36
|
+
data: null,
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
const [updateState, setUpdateState] = useState<MutationState<Project>>({
|
|
40
|
+
loading: false,
|
|
41
|
+
error: null,
|
|
42
|
+
data: null,
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
const [deleteState, setDeleteState] = useState<
|
|
46
|
+
MutationState<{ success: boolean }>
|
|
47
|
+
>({
|
|
48
|
+
loading: false,
|
|
49
|
+
error: null,
|
|
50
|
+
data: null,
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Create a new project
|
|
55
|
+
*/
|
|
56
|
+
const createProject = useCallback(
|
|
57
|
+
async (input: CreateProjectInput): Promise<Project | null> => {
|
|
58
|
+
setCreateState({ loading: true, error: null, data: null });
|
|
59
|
+
try {
|
|
60
|
+
const result = await saas.createProject(input, {
|
|
61
|
+
projectId,
|
|
62
|
+
organizationId: 'demo-org',
|
|
63
|
+
});
|
|
64
|
+
setCreateState({ loading: false, error: null, data: result });
|
|
65
|
+
options.onSuccess?.();
|
|
66
|
+
return result;
|
|
67
|
+
} catch (err) {
|
|
68
|
+
const error =
|
|
69
|
+
err instanceof Error ? err : new Error('Failed to create project');
|
|
70
|
+
setCreateState({ loading: false, error, data: null });
|
|
71
|
+
options.onError?.(error);
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
[saas, projectId, options]
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Update a project
|
|
80
|
+
*/
|
|
81
|
+
const updateProject = useCallback(
|
|
82
|
+
async (input: UpdateProjectInput): Promise<Project | null> => {
|
|
83
|
+
setUpdateState({ loading: true, error: null, data: null });
|
|
84
|
+
try {
|
|
85
|
+
const result = await saas.updateProject(input);
|
|
86
|
+
setUpdateState({ loading: false, error: null, data: result });
|
|
87
|
+
options.onSuccess?.();
|
|
88
|
+
return result;
|
|
89
|
+
} catch (err) {
|
|
90
|
+
const error =
|
|
91
|
+
err instanceof Error ? err : new Error('Failed to update project');
|
|
92
|
+
setUpdateState({ loading: false, error, data: null });
|
|
93
|
+
options.onError?.(error);
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
[saas, options]
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Delete a project (soft delete)
|
|
102
|
+
*/
|
|
103
|
+
const deleteProject = useCallback(
|
|
104
|
+
async (id: string): Promise<boolean> => {
|
|
105
|
+
setDeleteState({ loading: true, error: null, data: null });
|
|
106
|
+
try {
|
|
107
|
+
await saas.deleteProject(id);
|
|
108
|
+
setDeleteState({
|
|
109
|
+
loading: false,
|
|
110
|
+
error: null,
|
|
111
|
+
data: { success: true },
|
|
112
|
+
});
|
|
113
|
+
options.onSuccess?.();
|
|
114
|
+
return true;
|
|
115
|
+
} catch (err) {
|
|
116
|
+
const error =
|
|
117
|
+
err instanceof Error ? err : new Error('Failed to delete project');
|
|
118
|
+
setDeleteState({ loading: false, error, data: null });
|
|
119
|
+
options.onError?.(error);
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
},
|
|
123
|
+
[saas, options]
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Archive a project (status change)
|
|
128
|
+
*/
|
|
129
|
+
const archiveProject = useCallback(
|
|
130
|
+
async (id: string): Promise<Project | null> => {
|
|
131
|
+
return updateProject({ id, status: 'ARCHIVED' });
|
|
132
|
+
},
|
|
133
|
+
[updateProject]
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Activate a project (status change)
|
|
138
|
+
*/
|
|
139
|
+
const activateProject = useCallback(
|
|
140
|
+
async (id: string): Promise<Project | null> => {
|
|
141
|
+
return updateProject({ id, status: 'ACTIVE' });
|
|
142
|
+
},
|
|
143
|
+
[updateProject]
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
return {
|
|
147
|
+
// Mutations
|
|
148
|
+
createProject,
|
|
149
|
+
updateProject,
|
|
150
|
+
deleteProject,
|
|
151
|
+
archiveProject,
|
|
152
|
+
activateProject,
|
|
153
|
+
|
|
154
|
+
// State
|
|
155
|
+
createState,
|
|
156
|
+
updateState,
|
|
157
|
+
deleteState,
|
|
158
|
+
|
|
159
|
+
// Convenience
|
|
160
|
+
isLoading:
|
|
161
|
+
createState.loading || updateState.loading || deleteState.loading,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Note: Types are re-exported from the handlers package
|
|
166
|
+
// Consumers should import types directly from '@contractspec/example.saas-boilerplate/handlers'
|
package/src/ui/index.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// Main dashboard
|
|
2
|
+
export * from './SaasDashboard';
|
|
3
|
+
|
|
4
|
+
// Standalone components
|
|
5
|
+
export * from './SaasProjectList';
|
|
6
|
+
export * from './SaasSettingsPanel';
|
|
7
|
+
|
|
8
|
+
// Modals
|
|
9
|
+
export * from './modals';
|
|
10
|
+
|
|
11
|
+
// Hooks
|
|
12
|
+
export * from './hooks';
|
|
13
|
+
|
|
14
|
+
// Renderers
|
|
15
|
+
export * from './renderers';
|
|
16
|
+
|
|
17
|
+
// Overlays
|
|
18
|
+
export * from './overlays';
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* CreateProjectModal - Form for creating a new project
|
|
5
|
+
*
|
|
6
|
+
* Wires to CreateProjectContract via useProjectMutations hook.
|
|
7
|
+
*/
|
|
8
|
+
import { useState } from 'react';
|
|
9
|
+
import { Button, Input } from '@contractspec/lib.design-system';
|
|
10
|
+
|
|
11
|
+
// Local type definition for modal props
|
|
12
|
+
export interface CreateProjectInput {
|
|
13
|
+
name: string;
|
|
14
|
+
description?: string;
|
|
15
|
+
tier: 'FREE' | 'PRO' | 'ENTERPRISE';
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface CreateProjectModalProps {
|
|
19
|
+
isOpen: boolean;
|
|
20
|
+
onClose: () => void;
|
|
21
|
+
onSubmit: (input: CreateProjectInput) => Promise<void>;
|
|
22
|
+
isLoading?: boolean;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const TIERS: { value: CreateProjectInput['tier']; label: string }[] = [
|
|
26
|
+
{ value: 'FREE', label: 'Free' },
|
|
27
|
+
{ value: 'PRO', label: 'Pro' },
|
|
28
|
+
{ value: 'ENTERPRISE', label: 'Enterprise' },
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
export function CreateProjectModal({
|
|
32
|
+
isOpen,
|
|
33
|
+
onClose,
|
|
34
|
+
onSubmit,
|
|
35
|
+
isLoading = false,
|
|
36
|
+
}: CreateProjectModalProps) {
|
|
37
|
+
const [name, setName] = useState('');
|
|
38
|
+
const [description, setDescription] = useState('');
|
|
39
|
+
const [tier, setTier] = useState<CreateProjectInput['tier']>('FREE');
|
|
40
|
+
const [error, setError] = useState<string | null>(null);
|
|
41
|
+
|
|
42
|
+
const handleSubmit = async (e: React.FormEvent) => {
|
|
43
|
+
e.preventDefault();
|
|
44
|
+
setError(null);
|
|
45
|
+
|
|
46
|
+
// Validation
|
|
47
|
+
if (!name.trim()) {
|
|
48
|
+
setError('Project name is required');
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
await onSubmit({
|
|
54
|
+
name: name.trim(),
|
|
55
|
+
description: description.trim() || undefined,
|
|
56
|
+
tier,
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
// Reset form
|
|
60
|
+
setName('');
|
|
61
|
+
setDescription('');
|
|
62
|
+
setTier('FREE');
|
|
63
|
+
onClose();
|
|
64
|
+
} catch (err) {
|
|
65
|
+
setError(err instanceof Error ? err.message : 'Failed to create project');
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
if (!isOpen) return null;
|
|
70
|
+
|
|
71
|
+
return (
|
|
72
|
+
<div className="fixed inset-0 z-50 flex items-center justify-center">
|
|
73
|
+
{/* Backdrop */}
|
|
74
|
+
<div
|
|
75
|
+
className="bg-background/80 absolute inset-0 backdrop-blur-sm"
|
|
76
|
+
onClick={onClose}
|
|
77
|
+
role="button"
|
|
78
|
+
tabIndex={0}
|
|
79
|
+
onKeyDown={(e) => {
|
|
80
|
+
if (e.key === 'Enter' || e.key === ' ') onClose();
|
|
81
|
+
}}
|
|
82
|
+
aria-label="Close modal"
|
|
83
|
+
/>
|
|
84
|
+
|
|
85
|
+
{/* Modal */}
|
|
86
|
+
<div className="bg-card border-border relative z-10 w-full max-w-md rounded-xl border p-6 shadow-xl">
|
|
87
|
+
<h2 className="mb-4 text-xl font-semibold">Create New Project</h2>
|
|
88
|
+
|
|
89
|
+
<form onSubmit={handleSubmit} className="space-y-4">
|
|
90
|
+
{/* Project Name */}
|
|
91
|
+
<div>
|
|
92
|
+
<label
|
|
93
|
+
htmlFor="project-name"
|
|
94
|
+
className="text-muted-foreground mb-1 block text-sm font-medium"
|
|
95
|
+
>
|
|
96
|
+
Project Name *
|
|
97
|
+
</label>
|
|
98
|
+
<Input
|
|
99
|
+
id="project-name"
|
|
100
|
+
value={name}
|
|
101
|
+
onChange={(e) => setName(e.target.value)}
|
|
102
|
+
placeholder="e.g., My Awesome Project"
|
|
103
|
+
disabled={isLoading}
|
|
104
|
+
/>
|
|
105
|
+
</div>
|
|
106
|
+
|
|
107
|
+
{/* Description */}
|
|
108
|
+
<div>
|
|
109
|
+
<label
|
|
110
|
+
htmlFor="project-description"
|
|
111
|
+
className="text-muted-foreground mb-1 block text-sm font-medium"
|
|
112
|
+
>
|
|
113
|
+
Description
|
|
114
|
+
</label>
|
|
115
|
+
<textarea
|
|
116
|
+
id="project-description"
|
|
117
|
+
value={description}
|
|
118
|
+
onChange={(e) => setDescription(e.target.value)}
|
|
119
|
+
placeholder="Describe what this project is about..."
|
|
120
|
+
rows={3}
|
|
121
|
+
disabled={isLoading}
|
|
122
|
+
className="border-input bg-background focus:ring-ring w-full rounded-md border px-3 py-2 text-sm focus:ring-2 focus:outline-none disabled:opacity-50"
|
|
123
|
+
/>
|
|
124
|
+
</div>
|
|
125
|
+
|
|
126
|
+
{/* Tier */}
|
|
127
|
+
<div>
|
|
128
|
+
<label
|
|
129
|
+
htmlFor="project-tier"
|
|
130
|
+
className="text-muted-foreground mb-1 block text-sm font-medium"
|
|
131
|
+
>
|
|
132
|
+
Tier
|
|
133
|
+
</label>
|
|
134
|
+
<select
|
|
135
|
+
id="project-tier"
|
|
136
|
+
value={tier}
|
|
137
|
+
onChange={(e) =>
|
|
138
|
+
setTier(e.target.value as CreateProjectInput['tier'])
|
|
139
|
+
}
|
|
140
|
+
disabled={isLoading}
|
|
141
|
+
className="border-input bg-background focus:ring-ring h-10 w-full rounded-md border px-3 py-2 text-sm focus:ring-2 focus:outline-none disabled:opacity-50"
|
|
142
|
+
>
|
|
143
|
+
{TIERS.map((t) => (
|
|
144
|
+
<option key={t.value} value={t.value}>
|
|
145
|
+
{t.label}
|
|
146
|
+
</option>
|
|
147
|
+
))}
|
|
148
|
+
</select>
|
|
149
|
+
</div>
|
|
150
|
+
|
|
151
|
+
{/* Error Message */}
|
|
152
|
+
{error && (
|
|
153
|
+
<div className="bg-destructive/10 text-destructive rounded-md p-3 text-sm">
|
|
154
|
+
{error}
|
|
155
|
+
</div>
|
|
156
|
+
)}
|
|
157
|
+
|
|
158
|
+
{/* Actions */}
|
|
159
|
+
<div className="flex justify-end gap-3 pt-2">
|
|
160
|
+
<Button
|
|
161
|
+
type="button"
|
|
162
|
+
variant="ghost"
|
|
163
|
+
onPress={onClose}
|
|
164
|
+
disabled={isLoading}
|
|
165
|
+
>
|
|
166
|
+
Cancel
|
|
167
|
+
</Button>
|
|
168
|
+
<Button type="submit" disabled={isLoading}>
|
|
169
|
+
{isLoading ? 'Creating...' : 'Create Project'}
|
|
170
|
+
</Button>
|
|
171
|
+
</div>
|
|
172
|
+
</form>
|
|
173
|
+
</div>
|
|
174
|
+
</div>
|
|
175
|
+
);
|
|
176
|
+
}
|