@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
@@ -1,5 +1,15 @@
1
1
  'use client';
2
2
 
3
+ import {
4
+ Button,
5
+ EmptyState,
6
+ EntityCard,
7
+ ErrorState,
8
+ LoaderBlock,
9
+ StatCard,
10
+ StatCardGroup,
11
+ StatusChip,
12
+ } from '@contractspec/lib.design-system';
3
13
  /**
4
14
  * SaaS Dashboard
5
15
  *
@@ -11,315 +21,313 @@
11
21
  * - UpdateProjectContract -> Edit project via modal
12
22
  * - DeleteProjectContract -> Delete project via modal
13
23
  */
14
- import { useState, useCallback } from 'react';
24
+ import { useCallback, useState } from 'react';
15
25
  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,
26
+ type Project,
27
+ type Subscription,
28
+ useProjectList,
29
29
  } from './hooks/useProjectList';
30
30
  import { useProjectMutations } from './hooks/useProjectMutations';
31
31
  import { CreateProjectModal } from './modals/CreateProjectModal';
32
32
  import { ProjectActionsModal } from './modals/ProjectActionsModal';
33
+ import { SaasVisualizationOverview } from './SaasDashboard.visualizations';
33
34
 
34
35
  type Tab = 'projects' | 'billing' | 'settings';
35
36
 
36
37
  function getStatusTone(
37
- status: Project['status']
38
+ status: Project['status']
38
39
  ): '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
- }
40
+ switch (status) {
41
+ case 'ACTIVE':
42
+ return 'success';
43
+ case 'DRAFT':
44
+ return 'neutral';
45
+ case 'ARCHIVED':
46
+ return 'warning';
47
+ default:
48
+ return 'neutral';
49
+ }
49
50
  }
50
51
 
51
52
  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);
53
+ const [activeTab, setActiveTab] = useState<Tab>('projects');
54
+ const [isCreateModalOpen, setIsCreateModalOpen] = useState(false);
55
+ const [selectedProject, setSelectedProject] = useState<Project | null>(null);
56
+ const [isProjectActionsOpen, setIsProjectActionsOpen] = useState(false);
57
+
58
+ const { data, subscription, loading, error, stats, refetch } =
59
+ useProjectList();
56
60
 
57
- const { data, subscription, loading, error, stats, refetch } =
58
- useProjectList();
61
+ const mutations = useProjectMutations({
62
+ onSuccess: () => {
63
+ refetch();
64
+ },
65
+ });
59
66
 
60
- const mutations = useProjectMutations({
61
- onSuccess: () => {
62
- refetch();
63
- },
64
- });
67
+ const handleProjectClick = useCallback((project: Project) => {
68
+ setSelectedProject(project);
69
+ setIsProjectActionsOpen(true);
70
+ }, []);
65
71
 
66
- const handleProjectClick = useCallback((project: Project) => {
67
- setSelectedProject(project);
68
- setIsProjectActionsOpen(true);
69
- }, []);
72
+ const tabs: { id: Tab; label: string; icon: string }[] = [
73
+ { id: 'projects', label: 'Projects', icon: '📁' },
74
+ { id: 'billing', label: 'Billing', icon: '💳' },
75
+ { id: 'settings', label: 'Settings', icon: '⚙️' },
76
+ ];
70
77
 
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
- ];
78
+ if (loading && !data) {
79
+ return <LoaderBlock label="Loading dashboard..." />;
80
+ }
76
81
 
77
- if (loading && !data) {
78
- return <LoaderBlock label="Loading dashboard..." />;
79
- }
82
+ if (error) {
83
+ return (
84
+ <ErrorState
85
+ title="Failed to load dashboard"
86
+ description={error.message}
87
+ onRetry={refetch}
88
+ retryLabel="Retry"
89
+ />
90
+ );
91
+ }
80
92
 
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
- }
93
+ return (
94
+ <div className="space-y-6">
95
+ {/* Header */}
96
+ <div className="flex items-center justify-between">
97
+ <h2 className="font-bold text-2xl">SaaS Dashboard</h2>
98
+ {activeTab === 'projects' && (
99
+ <Button onPress={() => setIsCreateModalOpen(true)}>
100
+ <span className="mr-2">+</span> New Project
101
+ </Button>
102
+ )}
103
+ </div>
91
104
 
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>
105
+ {/* Stats Row */}
106
+ {stats && subscription && (
107
+ <StatCardGroup>
108
+ <StatCard label="Projects" value={stats.total.toString()} />
109
+ <StatCard label="Active" value={stats.activeCount.toString()} />
110
+ <StatCard label="Draft" value={stats.draftCount.toString()} />
111
+ <StatCard
112
+ label="Plan"
113
+ value={subscription.plan}
114
+ hint={subscription.status}
115
+ />
116
+ </StatCardGroup>
117
+ )}
103
118
 
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
- )}
119
+ {data && stats && (
120
+ <SaasVisualizationOverview
121
+ projectLimit={stats.projectLimit}
122
+ projects={data.items}
123
+ />
124
+ )}
117
125
 
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>
126
+ {/* Navigation Tabs */}
127
+ <nav className="flex gap-1 rounded-lg bg-muted p-1" role="tablist">
128
+ {tabs.map((tab) => (
129
+ <button
130
+ key={tab.id}
131
+ type="button"
132
+ role="tab"
133
+ aria-selected={activeTab === tab.id}
134
+ onClick={() => setActiveTab(tab.id)}
135
+ className={`flex flex-1 items-center justify-center gap-2 rounded-md px-4 py-2 font-medium text-sm transition-colors ${
136
+ activeTab === tab.id
137
+ ? 'bg-background text-foreground shadow-sm'
138
+ : 'text-muted-foreground hover:text-foreground'
139
+ }`}
140
+ >
141
+ <span>{tab.icon}</span>
142
+ {tab.label}
143
+ </button>
144
+ ))}
145
+ </nav>
138
146
 
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
+ {/* Tab Content */}
148
+ <div className="min-h-[400px]" role="tabpanel">
149
+ {activeTab === 'projects' && (
150
+ <ProjectsTab data={data} onProjectClick={handleProjectClick} />
151
+ )}
152
+ {activeTab === 'billing' && <BillingTab subscription={subscription} />}
153
+ {activeTab === 'settings' && <SettingsTab />}
154
+ </div>
147
155
 
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
- />
156
+ {/* Create Project Modal */}
157
+ <CreateProjectModal
158
+ isOpen={isCreateModalOpen}
159
+ onClose={() => setIsCreateModalOpen(false)}
160
+ onSubmit={async (input) => {
161
+ await mutations.createProject(input);
162
+ }}
163
+ isLoading={mutations.createState.loading}
164
+ />
157
165
 
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
- );
166
+ {/* Project Actions Modal */}
167
+ <ProjectActionsModal
168
+ isOpen={isProjectActionsOpen}
169
+ project={selectedProject}
170
+ onClose={() => {
171
+ setIsProjectActionsOpen(false);
172
+ setSelectedProject(null);
173
+ }}
174
+ onUpdate={async (input) => {
175
+ await mutations.updateProject(input);
176
+ }}
177
+ onArchive={async (projectId) => {
178
+ await mutations.archiveProject(projectId);
179
+ }}
180
+ onActivate={async (projectId) => {
181
+ await mutations.activateProject(projectId);
182
+ }}
183
+ onDelete={async (projectId) => {
184
+ await mutations.deleteProject(projectId);
185
+ }}
186
+ isLoading={mutations.isLoading}
187
+ />
188
+ </div>
189
+ );
182
190
  }
183
191
 
184
192
  interface ProjectsTabProps {
185
- data: ReturnType<typeof useProjectList>['data'];
186
- onProjectClick?: (project: Project) => void;
193
+ data: ReturnType<typeof useProjectList>['data'];
194
+ onProjectClick?: (project: Project) => void;
187
195
  }
188
196
 
189
197
  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
+ if (!data?.items.length) {
199
+ return (
200
+ <EmptyState
201
+ title="No projects yet"
202
+ description="Create your first project to get started."
203
+ />
204
+ );
205
+ }
198
206
 
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
- );
207
+ return (
208
+ <div className="space-y-4">
209
+ <div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
210
+ {data.items.map((project: Project) => (
211
+ <EntityCard
212
+ key={project.id}
213
+ cardTitle={project.name}
214
+ cardSubtitle={project.tier}
215
+ meta={
216
+ <p className="text-muted-foreground text-sm">
217
+ {project.description}
218
+ </p>
219
+ }
220
+ chips={
221
+ <StatusChip
222
+ tone={getStatusTone(project.status)}
223
+ label={project.status}
224
+ />
225
+ }
226
+ footer={
227
+ <div className="flex w-full items-center justify-between">
228
+ <span className="text-muted-foreground text-xs">
229
+ {project.updatedAt.toLocaleDateString()}
230
+ </span>
231
+ <Button
232
+ variant="ghost"
233
+ size="sm"
234
+ onPress={() => onProjectClick?.(project)}
235
+ >
236
+ Actions
237
+ </Button>
238
+ </div>
239
+ }
240
+ />
241
+ ))}
242
+ </div>
243
+ </div>
244
+ );
237
245
  }
238
246
 
239
247
  function BillingTab({ subscription }: { subscription: Subscription | null }) {
240
- if (!subscription) return null;
248
+ if (!subscription) return null;
241
249
 
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>
250
+ return (
251
+ <div className="space-y-6">
252
+ <div className="rounded-xl border border-border bg-card p-6">
253
+ <div className="flex items-start justify-between">
254
+ <div>
255
+ <h3 className="font-semibold text-lg">{subscription.plan} Plan</h3>
256
+ <p className="text-muted-foreground text-sm">
257
+ Current period:{' '}
258
+ {subscription.currentPeriodStart.toLocaleDateString()} -{' '}
259
+ {subscription.currentPeriodEnd.toLocaleDateString()}
260
+ </p>
261
+ <p className="text-muted-foreground text-sm">
262
+ Billing cycle: {subscription.billingCycle}
263
+ </p>
264
+ </div>
265
+ <StatusChip tone="success" label={subscription.status} />
266
+ </div>
259
267
 
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>
268
+ <div className="mt-4 flex gap-3">
269
+ <Button variant="outline" onPress={() => alert('Upgrade clicked!')}>
270
+ Upgrade Plan
271
+ </Button>
272
+ <Button
273
+ variant="ghost"
274
+ onPress={() => alert('Manage Billing clicked!')}
275
+ >
276
+ Manage Billing
277
+ </Button>
278
+ </div>
279
+ </div>
272
280
 
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
- );
281
+ {subscription.cancelAtPeriodEnd && (
282
+ <div className="rounded-xl border border-border bg-destructive/10 p-4 text-destructive">
283
+ <p className="font-medium text-sm">
284
+ ⚠️ Your subscription will be cancelled at the end of the current
285
+ period.
286
+ </p>
287
+ </div>
288
+ )}
289
+ </div>
290
+ );
283
291
  }
284
292
 
285
293
  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
- );
294
+ return (
295
+ <div className="space-y-6">
296
+ <div className="rounded-xl border border-border bg-card p-6">
297
+ <h3 className="mb-4 font-semibold text-lg">Organization Settings</h3>
298
+ <div className="space-y-4">
299
+ <div>
300
+ <label htmlFor="org-name" className="font-medium text-sm">
301
+ Organization Name
302
+ </label>
303
+ <input
304
+ id="org-name"
305
+ type="text"
306
+ defaultValue="Demo Organization"
307
+ className="mt-1 block w-full rounded-md border border-input bg-background px-3 py-2"
308
+ />
309
+ </div>
310
+ <div>
311
+ <label htmlFor="timezone" className="font-medium text-sm">
312
+ Default Timezone
313
+ </label>
314
+ <select
315
+ id="timezone"
316
+ className="mt-1 block w-full rounded-md border border-input bg-background px-3 py-2"
317
+ >
318
+ <option>UTC</option>
319
+ <option>America/New_York</option>
320
+ <option>Europe/London</option>
321
+ <option>Asia/Tokyo</option>
322
+ </select>
323
+ </div>
324
+ <div className="pt-2">
325
+ <Button onPress={() => alert('Settings saved!')}>
326
+ Save Settings
327
+ </Button>
328
+ </div>
329
+ </div>
330
+ </div>
331
+ </div>
332
+ );
325
333
  }
@@ -0,0 +1,41 @@
1
+ 'use client';
2
+
3
+ import {
4
+ VisualizationCard,
5
+ VisualizationGrid,
6
+ } from '@contractspec/lib.design-system';
7
+ import type { Project } from '../handlers/saas.handlers';
8
+ import { createSaasVisualizationItems } from '../visualizations';
9
+
10
+ export function SaasVisualizationOverview({
11
+ projects,
12
+ projectLimit,
13
+ }: {
14
+ projects: Project[];
15
+ projectLimit: number;
16
+ }) {
17
+ const items = createSaasVisualizationItems(projects, projectLimit);
18
+
19
+ return (
20
+ <section className="space-y-3">
21
+ <div>
22
+ <h3 className="font-semibold text-lg">Portfolio Visualizations</h3>
23
+ <p className="text-muted-foreground text-sm">
24
+ Contract-backed charts for project mix, capacity, and activity.
25
+ </p>
26
+ </div>
27
+ <VisualizationGrid>
28
+ {items.map((item) => (
29
+ <VisualizationCard
30
+ key={item.key}
31
+ data={item.data}
32
+ description={item.description}
33
+ height={item.height}
34
+ spec={item.spec}
35
+ title={item.title}
36
+ />
37
+ ))}
38
+ </VisualizationGrid>
39
+ </section>
40
+ );
41
+ }