@contractspec/example.saas-boilerplate 3.7.6 → 3.8.2

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.
Files changed (143) hide show
  1. package/.turbo/turbo-build.log +39 -27
  2. package/AGENTS.md +50 -27
  3. package/CHANGELOG.md +36 -0
  4. package/README.md +65 -144
  5. package/dist/billing/billing.event.js +1 -1
  6. package/dist/billing/index.d.ts +6 -6
  7. package/dist/billing/index.js +1 -1
  8. package/dist/browser/billing/billing.event.js +1 -1
  9. package/dist/browser/billing/index.js +1 -1
  10. package/dist/browser/index.js +1147 -869
  11. package/dist/browser/project/index.js +209 -209
  12. package/dist/browser/project/project.event.js +1 -1
  13. package/dist/browser/saas-boilerplate.feature.js +208 -0
  14. package/dist/browser/ui/SaasDashboard.js +356 -105
  15. package/dist/browser/ui/SaasDashboard.visualizations.js +249 -0
  16. package/dist/browser/ui/SaasProjectList.js +7 -7
  17. package/dist/browser/ui/SaasSettingsPanel.js +12 -12
  18. package/dist/browser/ui/hooks/index.js +2 -2
  19. package/dist/browser/ui/hooks/useProjectList.js +1 -1
  20. package/dist/browser/ui/hooks/useProjectMutations.js +1 -1
  21. package/dist/browser/ui/index.js +790 -521
  22. package/dist/browser/ui/modals/CreateProjectModal.js +10 -10
  23. package/dist/browser/ui/modals/ProjectActionsModal.js +13 -13
  24. package/dist/browser/ui/modals/index.js +23 -23
  25. package/dist/browser/ui/renderers/index.js +341 -115
  26. package/dist/browser/ui/renderers/project-list.markdown.js +229 -3
  27. package/dist/browser/ui/renderers/project-list.renderer.js +7 -7
  28. package/dist/browser/visualizations/catalog.js +155 -0
  29. package/dist/browser/visualizations/index.js +217 -0
  30. package/dist/browser/visualizations/selectors.js +210 -0
  31. package/dist/handlers/index.d.ts +2 -2
  32. package/dist/index.d.ts +5 -4
  33. package/dist/index.js +1147 -869
  34. package/dist/node/billing/billing.event.js +1 -1
  35. package/dist/node/billing/index.js +1 -1
  36. package/dist/node/index.js +1147 -869
  37. package/dist/node/project/index.js +209 -209
  38. package/dist/node/project/project.event.js +1 -1
  39. package/dist/node/saas-boilerplate.feature.js +208 -0
  40. package/dist/node/ui/SaasDashboard.js +356 -105
  41. package/dist/node/ui/SaasDashboard.visualizations.js +249 -0
  42. package/dist/node/ui/SaasProjectList.js +7 -7
  43. package/dist/node/ui/SaasSettingsPanel.js +12 -12
  44. package/dist/node/ui/hooks/index.js +2 -2
  45. package/dist/node/ui/hooks/useProjectList.js +1 -1
  46. package/dist/node/ui/hooks/useProjectMutations.js +1 -1
  47. package/dist/node/ui/index.js +790 -521
  48. package/dist/node/ui/modals/CreateProjectModal.js +10 -10
  49. package/dist/node/ui/modals/ProjectActionsModal.js +13 -13
  50. package/dist/node/ui/modals/index.js +23 -23
  51. package/dist/node/ui/renderers/index.js +341 -115
  52. package/dist/node/ui/renderers/project-list.markdown.js +229 -3
  53. package/dist/node/ui/renderers/project-list.renderer.js +7 -7
  54. package/dist/node/visualizations/catalog.js +155 -0
  55. package/dist/node/visualizations/index.js +217 -0
  56. package/dist/node/visualizations/selectors.js +210 -0
  57. package/dist/presentations/index.d.ts +1 -1
  58. package/dist/project/index.d.ts +7 -7
  59. package/dist/project/index.js +209 -209
  60. package/dist/project/project.event.js +1 -1
  61. package/dist/saas-boilerplate.feature.js +208 -0
  62. package/dist/settings/index.d.ts +1 -1
  63. package/dist/ui/SaasDashboard.js +356 -105
  64. package/dist/ui/SaasDashboard.visualizations.d.ts +5 -0
  65. package/dist/ui/SaasDashboard.visualizations.js +250 -0
  66. package/dist/ui/SaasProjectList.js +7 -7
  67. package/dist/ui/SaasSettingsPanel.js +12 -12
  68. package/dist/ui/hooks/index.d.ts +2 -2
  69. package/dist/ui/hooks/index.js +2 -2
  70. package/dist/ui/hooks/useProjectList.d.ts +5 -0
  71. package/dist/ui/hooks/useProjectList.js +1 -1
  72. package/dist/ui/hooks/useProjectMutations.d.ts +8 -0
  73. package/dist/ui/hooks/useProjectMutations.js +1 -1
  74. package/dist/ui/index.d.ts +4 -4
  75. package/dist/ui/index.js +790 -521
  76. package/dist/ui/modals/CreateProjectModal.js +10 -10
  77. package/dist/ui/modals/ProjectActionsModal.js +13 -13
  78. package/dist/ui/modals/index.js +23 -23
  79. package/dist/ui/renderers/index.d.ts +1 -1
  80. package/dist/ui/renderers/index.js +341 -115
  81. package/dist/ui/renderers/project-list.markdown.js +229 -3
  82. package/dist/ui/renderers/project-list.renderer.d.ts +1 -1
  83. package/dist/ui/renderers/project-list.renderer.js +7 -7
  84. package/dist/visualizations/catalog.d.ts +11 -0
  85. package/dist/visualizations/catalog.js +156 -0
  86. package/dist/visualizations/index.d.ts +2 -0
  87. package/dist/visualizations/index.js +218 -0
  88. package/dist/visualizations/selectors.d.ts +8 -0
  89. package/dist/visualizations/selectors.js +211 -0
  90. package/dist/visualizations/selectors.test.d.ts +1 -0
  91. package/package.json +70 -14
  92. package/src/billing/billing.entity.ts +132 -132
  93. package/src/billing/billing.enum.ts +9 -9
  94. package/src/billing/billing.event.ts +71 -71
  95. package/src/billing/billing.handler.ts +87 -87
  96. package/src/billing/billing.operations.ts +158 -158
  97. package/src/billing/billing.presentation.ts +45 -45
  98. package/src/billing/billing.schema.ts +76 -76
  99. package/src/billing/index.ts +43 -48
  100. package/src/dashboard/dashboard.presentation.ts +45 -45
  101. package/src/dashboard/index.ts +2 -2
  102. package/src/docs/saas-boilerplate.docblock.ts +43 -43
  103. package/src/example.ts +32 -32
  104. package/src/handlers/index.ts +9 -9
  105. package/src/handlers/saas.handlers.ts +250 -249
  106. package/src/index.ts +41 -41
  107. package/src/presentations/index.ts +18 -20
  108. package/src/project/index.ts +45 -50
  109. package/src/project/project.entity.ts +68 -68
  110. package/src/project/project.enum.ts +8 -8
  111. package/src/project/project.event.ts +79 -79
  112. package/src/project/project.handler.ts +103 -103
  113. package/src/project/project.operations.ts +236 -236
  114. package/src/project/project.presentation.ts +46 -46
  115. package/src/project/project.schema.ts +90 -90
  116. package/src/saas-boilerplate.feature.ts +103 -100
  117. package/src/seeders/index.ts +20 -20
  118. package/src/settings/index.ts +2 -3
  119. package/src/settings/settings.entity.ts +65 -65
  120. package/src/settings/settings.enum.ts +4 -4
  121. package/src/shared/mock-data.ts +92 -92
  122. package/src/shared/overlay-types.ts +23 -23
  123. package/src/tests/operations.test-spec.ts +96 -96
  124. package/src/ui/SaasDashboard.tsx +278 -270
  125. package/src/ui/SaasDashboard.visualizations.tsx +41 -0
  126. package/src/ui/SaasProjectList.tsx +90 -90
  127. package/src/ui/SaasSettingsPanel.tsx +84 -84
  128. package/src/ui/hooks/index.ts +3 -3
  129. package/src/ui/hooks/useProjectList.ts +69 -68
  130. package/src/ui/hooks/useProjectMutations.ts +144 -143
  131. package/src/ui/index.ts +8 -12
  132. package/src/ui/modals/CreateProjectModal.tsx +154 -154
  133. package/src/ui/modals/ProjectActionsModal.tsx +321 -321
  134. package/src/ui/overlays/demo-overlays.ts +49 -49
  135. package/src/ui/renderers/index.ts +5 -4
  136. package/src/ui/renderers/project-list.markdown.ts +229 -205
  137. package/src/ui/renderers/project-list.renderer.tsx +14 -13
  138. package/src/visualizations/catalog.ts +153 -0
  139. package/src/visualizations/index.ts +2 -0
  140. package/src/visualizations/selectors.test.ts +25 -0
  141. package/src/visualizations/selectors.ts +85 -0
  142. package/tsconfig.json +7 -8
  143. 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
- StatCard,
8
- StatCardGroup,
9
- StatusChip,
10
- EntityCard,
11
- EmptyState,
12
- LoaderBlock,
13
- ErrorState,
14
- Button,
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 { useProjectList, type Project } from './hooks/useProjectList';
16
+ import { type Project, useProjectList } from './hooks/useProjectList';
17
17
 
18
18
  interface SaasProjectListProps {
19
- onProjectClick?: (projectId: string) => void;
20
- onCreateProject?: () => void;
19
+ onProjectClick?: (projectId: string) => void;
20
+ onCreateProject?: () => void;
21
21
  }
22
22
 
23
23
  function getStatusTone(
24
- status: Project['status']
24
+ status: Project['status']
25
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
- }
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
- onProjectClick,
40
- onCreateProject,
39
+ onProjectClick,
40
+ onCreateProject,
41
41
  }: SaasProjectListProps) {
42
- const { data, loading, error, stats, refetch } = useProjectList();
42
+ const { data, loading, error, stats, refetch } = useProjectList();
43
43
 
44
- if (loading && !data) {
45
- return <LoaderBlock label="Loading projects..." />;
46
- }
44
+ if (loading && !data) {
45
+ return <LoaderBlock label="Loading projects..." />;
46
+ }
47
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
- }
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
- 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
- }
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
- 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
- )}
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
- <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
- );
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
- const [orgName, setOrgName] = useState('Demo Organization');
11
- const [timezone, setTimezone] = useState('UTC');
10
+ const [orgName, setOrgName] = useState('Demo Organization');
11
+ const [timezone, setTimezone] = useState('UTC');
12
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>
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
- <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>
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
- <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
- );
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
  }
@@ -1,9 +1,9 @@
1
1
  'use client';
2
2
 
3
- export { useProjectList, type UseProjectListOptions } from './useProjectList';
3
+ export { type UseProjectListOptions, useProjectList } from './useProjectList';
4
4
  export {
5
- useProjectMutations,
6
- type UseProjectMutationsOptions,
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
- import { useCallback, useEffect, useMemo, useState } from 'react';
6
+
7
7
  import { useTemplateRuntime } from '@contractspec/lib.example-shared-ui';
8
+ import { useCallback, useEffect, useMemo, useState } from 'react';
8
9
  import type {
9
- Project as RuntimeProject,
10
- Subscription as RuntimeSubscription,
11
- SaasHandlers,
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
- items: Project[];
20
- total: number;
20
+ items: Project[];
21
+ total: number;
21
22
  }
22
23
 
23
24
  export interface UseProjectListOptions {
24
- status?: 'DRAFT' | 'ACTIVE' | 'ARCHIVED' | 'all';
25
- search?: string;
26
- limit?: number;
25
+ status?: 'DRAFT' | 'ACTIVE' | 'ARCHIVED' | 'all';
26
+ search?: string;
27
+ limit?: number;
27
28
  }
28
29
 
29
30
  export function useProjectList(options: UseProjectListOptions = {}) {
30
- const { handlers, projectId } = useTemplateRuntime<{ saas: SaasHandlers }>();
31
- const { saas } = handlers;
31
+ const { handlers, projectId } = useTemplateRuntime<{ saas: SaasHandlers }>();
32
+ const { saas } = handlers;
32
33
 
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);
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
- const fetchData = useCallback(async () => {
40
- setLoading(true);
41
- setError(null);
40
+ const fetchData = useCallback(async () => {
41
+ setLoading(true);
42
+ setError(null);
42
43
 
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]);
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
- useEffect(() => {
67
- fetchData();
68
- }, [fetchData]);
67
+ useEffect(() => {
68
+ fetchData();
69
+ }, [fetchData]);
69
70
 
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]);
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
- 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
- };
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
  }