@contractspec/example.saas-boilerplate 3.7.6 → 3.7.7
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 +8 -8
- package/AGENTS.md +50 -27
- package/README.md +64 -144
- package/dist/billing/billing.event.js +1 -1
- package/dist/billing/index.d.ts +6 -6
- package/dist/billing/index.js +1 -1
- package/dist/browser/billing/billing.event.js +1 -1
- package/dist/browser/billing/index.js +1 -1
- package/dist/browser/index.js +931 -932
- package/dist/browser/project/index.js +209 -209
- package/dist/browser/project/project.event.js +1 -1
- package/dist/browser/ui/SaasDashboard.js +45 -45
- package/dist/browser/ui/SaasProjectList.js +7 -7
- package/dist/browser/ui/SaasSettingsPanel.js +12 -12
- package/dist/browser/ui/hooks/index.js +2 -2
- package/dist/browser/ui/hooks/useProjectList.js +1 -1
- package/dist/browser/ui/hooks/useProjectMutations.js +1 -1
- package/dist/browser/ui/index.js +483 -484
- package/dist/browser/ui/modals/CreateProjectModal.js +10 -10
- package/dist/browser/ui/modals/ProjectActionsModal.js +13 -13
- package/dist/browser/ui/modals/index.js +23 -23
- package/dist/browser/ui/renderers/index.js +112 -112
- package/dist/browser/ui/renderers/project-list.renderer.js +7 -7
- package/dist/handlers/index.d.ts +2 -2
- package/dist/index.d.ts +4 -4
- package/dist/index.js +931 -932
- package/dist/node/billing/billing.event.js +1 -1
- package/dist/node/billing/index.js +1 -1
- package/dist/node/index.js +931 -932
- package/dist/node/project/index.js +209 -209
- package/dist/node/project/project.event.js +1 -1
- package/dist/node/ui/SaasDashboard.js +45 -45
- package/dist/node/ui/SaasProjectList.js +7 -7
- package/dist/node/ui/SaasSettingsPanel.js +12 -12
- package/dist/node/ui/hooks/index.js +2 -2
- package/dist/node/ui/hooks/useProjectList.js +1 -1
- package/dist/node/ui/hooks/useProjectMutations.js +1 -1
- package/dist/node/ui/index.js +483 -484
- package/dist/node/ui/modals/CreateProjectModal.js +10 -10
- package/dist/node/ui/modals/ProjectActionsModal.js +13 -13
- package/dist/node/ui/modals/index.js +23 -23
- package/dist/node/ui/renderers/index.js +112 -112
- package/dist/node/ui/renderers/project-list.renderer.js +7 -7
- package/dist/presentations/index.d.ts +1 -1
- package/dist/project/index.d.ts +7 -7
- package/dist/project/index.js +209 -209
- package/dist/project/project.event.js +1 -1
- package/dist/settings/index.d.ts +1 -1
- package/dist/ui/SaasDashboard.js +45 -45
- package/dist/ui/SaasProjectList.js +7 -7
- package/dist/ui/SaasSettingsPanel.js +12 -12
- package/dist/ui/hooks/index.d.ts +2 -2
- package/dist/ui/hooks/index.js +2 -2
- package/dist/ui/hooks/useProjectList.d.ts +5 -0
- package/dist/ui/hooks/useProjectList.js +1 -1
- package/dist/ui/hooks/useProjectMutations.d.ts +8 -0
- package/dist/ui/hooks/useProjectMutations.js +1 -1
- package/dist/ui/index.d.ts +4 -4
- package/dist/ui/index.js +483 -484
- package/dist/ui/modals/CreateProjectModal.js +10 -10
- package/dist/ui/modals/ProjectActionsModal.js +13 -13
- package/dist/ui/modals/index.js +23 -23
- package/dist/ui/renderers/index.d.ts +1 -1
- package/dist/ui/renderers/index.js +112 -112
- package/dist/ui/renderers/project-list.renderer.d.ts +1 -1
- package/dist/ui/renderers/project-list.renderer.js +7 -7
- package/package.json +10 -10
- package/src/billing/billing.entity.ts +132 -132
- package/src/billing/billing.enum.ts +9 -9
- package/src/billing/billing.event.ts +71 -71
- package/src/billing/billing.handler.ts +87 -87
- package/src/billing/billing.operations.ts +158 -158
- package/src/billing/billing.presentation.ts +45 -45
- package/src/billing/billing.schema.ts +76 -76
- package/src/billing/index.ts +43 -48
- package/src/dashboard/dashboard.presentation.ts +45 -45
- package/src/dashboard/index.ts +2 -2
- package/src/docs/saas-boilerplate.docblock.ts +43 -43
- package/src/example.ts +32 -32
- package/src/handlers/index.ts +9 -9
- package/src/handlers/saas.handlers.ts +250 -249
- package/src/index.ts +40 -41
- package/src/presentations/index.ts +18 -20
- package/src/project/index.ts +45 -50
- package/src/project/project.entity.ts +68 -68
- package/src/project/project.enum.ts +8 -8
- package/src/project/project.event.ts +79 -79
- package/src/project/project.handler.ts +103 -103
- package/src/project/project.operations.ts +236 -236
- package/src/project/project.presentation.ts +46 -46
- package/src/project/project.schema.ts +90 -90
- package/src/saas-boilerplate.feature.ts +100 -100
- package/src/seeders/index.ts +20 -20
- package/src/settings/index.ts +2 -3
- package/src/settings/settings.entity.ts +65 -65
- package/src/settings/settings.enum.ts +4 -4
- package/src/shared/mock-data.ts +92 -92
- package/src/shared/overlay-types.ts +23 -23
- package/src/tests/operations.test-spec.ts +96 -96
- package/src/ui/SaasDashboard.tsx +270 -270
- package/src/ui/SaasProjectList.tsx +90 -90
- package/src/ui/SaasSettingsPanel.tsx +84 -84
- package/src/ui/hooks/index.ts +3 -3
- package/src/ui/hooks/useProjectList.ts +69 -68
- package/src/ui/hooks/useProjectMutations.ts +144 -143
- package/src/ui/index.ts +8 -12
- package/src/ui/modals/CreateProjectModal.tsx +154 -154
- package/src/ui/modals/ProjectActionsModal.tsx +321 -321
- package/src/ui/overlays/demo-overlays.ts +49 -49
- package/src/ui/renderers/index.ts +5 -4
- package/src/ui/renderers/project-list.markdown.ts +204 -204
- package/src/ui/renderers/project-list.renderer.tsx +14 -13
- package/tsconfig.json +7 -8
- package/tsdown.config.js +7 -3
|
@@ -4,110 +4,110 @@
|
|
|
4
4
|
* SaaS Project List - Standalone project list component
|
|
5
5
|
*/
|
|
6
6
|
import {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
7
|
+
Button,
|
|
8
|
+
EmptyState,
|
|
9
|
+
EntityCard,
|
|
10
|
+
ErrorState,
|
|
11
|
+
LoaderBlock,
|
|
12
|
+
StatCard,
|
|
13
|
+
StatCardGroup,
|
|
14
|
+
StatusChip,
|
|
15
15
|
} from '@contractspec/lib.design-system';
|
|
16
|
-
import {
|
|
16
|
+
import { type Project, useProjectList } from './hooks/useProjectList';
|
|
17
17
|
|
|
18
18
|
interface SaasProjectListProps {
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
onProjectClick?: (projectId: string) => void;
|
|
20
|
+
onCreateProject?: () => void;
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
function getStatusTone(
|
|
24
|
-
|
|
24
|
+
status: Project['status']
|
|
25
25
|
): 'success' | 'warning' | 'neutral' | 'danger' {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
36
|
}
|
|
37
37
|
|
|
38
38
|
export function SaasProjectList({
|
|
39
|
-
|
|
40
|
-
|
|
39
|
+
onProjectClick,
|
|
40
|
+
onCreateProject,
|
|
41
41
|
}: SaasProjectListProps) {
|
|
42
|
-
|
|
42
|
+
const { data, loading, error, stats, refetch } = useProjectList();
|
|
43
43
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
44
|
+
if (loading && !data) {
|
|
45
|
+
return <LoaderBlock label="Loading projects..." />;
|
|
46
|
+
}
|
|
47
47
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
58
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
72
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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
82
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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
113
|
}
|
|
@@ -1,96 +1,96 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
+
import { Button } from '@contractspec/lib.design-system';
|
|
3
4
|
/**
|
|
4
5
|
* SaaS Settings Panel - Organization and user settings
|
|
5
6
|
*/
|
|
6
7
|
import { useState } from 'react';
|
|
7
|
-
import { Button } from '@contractspec/lib.design-system';
|
|
8
8
|
|
|
9
9
|
export function SaasSettingsPanel() {
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
const [orgName, setOrgName] = useState('Demo Organization');
|
|
11
|
+
const [timezone, setTimezone] = useState('UTC');
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
13
|
+
return (
|
|
14
|
+
<div className="space-y-6">
|
|
15
|
+
<div className="rounded-xl border border-border bg-card p-6">
|
|
16
|
+
<h3 className="mb-4 font-semibold text-lg">Organization Settings</h3>
|
|
17
|
+
<div className="space-y-4">
|
|
18
|
+
<div>
|
|
19
|
+
<label
|
|
20
|
+
htmlFor="setting-org-name"
|
|
21
|
+
className="block font-medium text-sm"
|
|
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="mt-1 block w-full rounded-md border border-input bg-background px-3 py-2"
|
|
31
|
+
/>
|
|
32
|
+
</div>
|
|
33
|
+
<div>
|
|
34
|
+
<label
|
|
35
|
+
htmlFor="setting-timezone"
|
|
36
|
+
className="block font-medium text-sm"
|
|
37
|
+
>
|
|
38
|
+
Default Timezone
|
|
39
|
+
</label>
|
|
40
|
+
<select
|
|
41
|
+
id="setting-timezone"
|
|
42
|
+
value={timezone}
|
|
43
|
+
onChange={(e) => setTimezone(e.target.value)}
|
|
44
|
+
className="mt-1 block w-full rounded-md border border-input bg-background 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
57
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
58
|
+
<div className="rounded-xl border border-border bg-card p-6">
|
|
59
|
+
<h3 className="mb-4 font-semibold text-lg">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="h-4 w-4 rounded border-input"
|
|
71
|
+
/>
|
|
72
|
+
<span className="text-sm">{item.label}</span>
|
|
73
|
+
</label>
|
|
74
|
+
))}
|
|
75
|
+
</div>
|
|
76
|
+
</div>
|
|
77
77
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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 font-semibold text-lg text-red-700 dark:text-red-400">
|
|
80
|
+
Danger Zone
|
|
81
|
+
</h3>
|
|
82
|
+
<p className="mb-4 text-red-600 text-sm 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
96
|
}
|
package/src/ui/hooks/index.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
export {
|
|
3
|
+
export { type UseProjectListOptions, useProjectList } from './useProjectList';
|
|
4
4
|
export {
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
type UseProjectMutationsOptions,
|
|
6
|
+
useProjectMutations,
|
|
7
7
|
} from './useProjectMutations';
|
|
8
8
|
|
|
9
9
|
// Note: For project types (CreateProjectInput, UpdateProjectInput, Project), import directly from:
|
|
@@ -3,12 +3,13 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Uses runtime-local database-backed handlers.
|
|
5
5
|
*/
|
|
6
|
-
|
|
6
|
+
|
|
7
7
|
import { useTemplateRuntime } from '@contractspec/lib.example-shared-ui';
|
|
8
|
+
import { useCallback, useEffect, useMemo, useState } from 'react';
|
|
8
9
|
import type {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
Project as RuntimeProject,
|
|
11
|
+
Subscription as RuntimeSubscription,
|
|
12
|
+
SaasHandlers,
|
|
12
13
|
} from '../../handlers/saas.handlers';
|
|
13
14
|
|
|
14
15
|
// Re-export types for convenience
|
|
@@ -16,80 +17,80 @@ export type Project = RuntimeProject;
|
|
|
16
17
|
export type Subscription = RuntimeSubscription;
|
|
17
18
|
|
|
18
19
|
export interface ListProjectsOutput {
|
|
19
|
-
|
|
20
|
-
|
|
20
|
+
items: Project[];
|
|
21
|
+
total: number;
|
|
21
22
|
}
|
|
22
23
|
|
|
23
24
|
export interface UseProjectListOptions {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
25
|
+
status?: 'DRAFT' | 'ACTIVE' | 'ARCHIVED' | 'all';
|
|
26
|
+
search?: string;
|
|
27
|
+
limit?: number;
|
|
27
28
|
}
|
|
28
29
|
|
|
29
30
|
export function useProjectList(options: UseProjectListOptions = {}) {
|
|
30
|
-
|
|
31
|
-
|
|
31
|
+
const { handlers, projectId } = useTemplateRuntime<{ saas: SaasHandlers }>();
|
|
32
|
+
const { saas } = handlers;
|
|
32
33
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
34
|
+
const [data, setData] = useState<ListProjectsOutput | null>(null);
|
|
35
|
+
const [subscription, setSubscription] = useState<Subscription | null>(null);
|
|
36
|
+
const [loading, setLoading] = useState(true);
|
|
37
|
+
const [error, setError] = useState<Error | null>(null);
|
|
38
|
+
const [page, setPage] = useState(1);
|
|
38
39
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
40
|
+
const fetchData = useCallback(async () => {
|
|
41
|
+
setLoading(true);
|
|
42
|
+
setError(null);
|
|
42
43
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
44
|
+
try {
|
|
45
|
+
const [projectsResult, subscriptionResult] = await Promise.all([
|
|
46
|
+
saas.listProjects({
|
|
47
|
+
projectId,
|
|
48
|
+
status: options.status === 'all' ? undefined : options.status,
|
|
49
|
+
search: options.search,
|
|
50
|
+
limit: options.limit ?? 20,
|
|
51
|
+
offset: (page - 1) * (options.limit ?? 20),
|
|
52
|
+
}),
|
|
53
|
+
saas.getSubscription({ projectId }),
|
|
54
|
+
]);
|
|
55
|
+
setData({
|
|
56
|
+
items: projectsResult.items,
|
|
57
|
+
total: projectsResult.total,
|
|
58
|
+
});
|
|
59
|
+
setSubscription(subscriptionResult);
|
|
60
|
+
} catch (err) {
|
|
61
|
+
setError(err instanceof Error ? err : new Error('Unknown error'));
|
|
62
|
+
} finally {
|
|
63
|
+
setLoading(false);
|
|
64
|
+
}
|
|
65
|
+
}, [saas, projectId, options.status, options.search, options.limit, page]);
|
|
65
66
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
67
|
+
useEffect(() => {
|
|
68
|
+
fetchData();
|
|
69
|
+
}, [fetchData]);
|
|
69
70
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
71
|
+
// Calculate stats
|
|
72
|
+
const stats = useMemo(() => {
|
|
73
|
+
if (!data) return null;
|
|
74
|
+
const items = data.items;
|
|
75
|
+
return {
|
|
76
|
+
total: data.total,
|
|
77
|
+
activeCount: items.filter((p) => p.status === 'ACTIVE').length,
|
|
78
|
+
draftCount: items.filter((p) => p.status === 'DRAFT').length,
|
|
79
|
+
// Subscription stats are optional since they may not be seeded
|
|
80
|
+
projectLimit: 10, // Default limit for demo
|
|
81
|
+
usagePercent: Math.min((data.total / 10) * 100, 100),
|
|
82
|
+
};
|
|
83
|
+
}, [data]);
|
|
83
84
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
85
|
+
return {
|
|
86
|
+
data,
|
|
87
|
+
subscription,
|
|
88
|
+
loading,
|
|
89
|
+
error,
|
|
90
|
+
stats,
|
|
91
|
+
page,
|
|
92
|
+
refetch: fetchData,
|
|
93
|
+
nextPage: () => setPage((p) => p + 1),
|
|
94
|
+
prevPage: () => page > 1 && setPage((p) => p - 1),
|
|
95
|
+
};
|
|
95
96
|
}
|