@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.
- package/.turbo/turbo-build.log +39 -27
- package/AGENTS.md +50 -27
- package/CHANGELOG.md +36 -0
- package/README.md +65 -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 +1147 -869
- package/dist/browser/project/index.js +209 -209
- package/dist/browser/project/project.event.js +1 -1
- package/dist/browser/saas-boilerplate.feature.js +208 -0
- package/dist/browser/ui/SaasDashboard.js +356 -105
- package/dist/browser/ui/SaasDashboard.visualizations.js +249 -0
- 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 +790 -521
- 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 +341 -115
- package/dist/browser/ui/renderers/project-list.markdown.js +229 -3
- package/dist/browser/ui/renderers/project-list.renderer.js +7 -7
- package/dist/browser/visualizations/catalog.js +155 -0
- package/dist/browser/visualizations/index.js +217 -0
- package/dist/browser/visualizations/selectors.js +210 -0
- package/dist/handlers/index.d.ts +2 -2
- package/dist/index.d.ts +5 -4
- package/dist/index.js +1147 -869
- package/dist/node/billing/billing.event.js +1 -1
- package/dist/node/billing/index.js +1 -1
- package/dist/node/index.js +1147 -869
- package/dist/node/project/index.js +209 -209
- package/dist/node/project/project.event.js +1 -1
- package/dist/node/saas-boilerplate.feature.js +208 -0
- package/dist/node/ui/SaasDashboard.js +356 -105
- package/dist/node/ui/SaasDashboard.visualizations.js +249 -0
- 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 +790 -521
- 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 +341 -115
- package/dist/node/ui/renderers/project-list.markdown.js +229 -3
- package/dist/node/ui/renderers/project-list.renderer.js +7 -7
- package/dist/node/visualizations/catalog.js +155 -0
- package/dist/node/visualizations/index.js +217 -0
- package/dist/node/visualizations/selectors.js +210 -0
- 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/saas-boilerplate.feature.js +208 -0
- package/dist/settings/index.d.ts +1 -1
- package/dist/ui/SaasDashboard.js +356 -105
- package/dist/ui/SaasDashboard.visualizations.d.ts +5 -0
- package/dist/ui/SaasDashboard.visualizations.js +250 -0
- 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 +790 -521
- 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 +341 -115
- package/dist/ui/renderers/project-list.markdown.js +229 -3
- package/dist/ui/renderers/project-list.renderer.d.ts +1 -1
- package/dist/ui/renderers/project-list.renderer.js +7 -7
- package/dist/visualizations/catalog.d.ts +11 -0
- package/dist/visualizations/catalog.js +156 -0
- package/dist/visualizations/index.d.ts +2 -0
- package/dist/visualizations/index.js +218 -0
- package/dist/visualizations/selectors.d.ts +8 -0
- package/dist/visualizations/selectors.js +211 -0
- package/dist/visualizations/selectors.test.d.ts +1 -0
- package/package.json +70 -14
- 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 +41 -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 +103 -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 +278 -270
- package/src/ui/SaasDashboard.visualizations.tsx +41 -0
- 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 +229 -205
- package/src/ui/renderers/project-list.renderer.tsx +14 -13
- package/src/visualizations/catalog.ts +153 -0
- package/src/visualizations/index.ts +2 -0
- package/src/visualizations/selectors.test.ts +25 -0
- package/src/visualizations/selectors.ts +85 -0
- package/tsconfig.json +7 -8
- package/tsdown.config.js +7 -3
package/src/ui/SaasDashboard.tsx
CHANGED
|
@@ -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 {
|
|
24
|
+
import { useCallback, useState } from 'react';
|
|
15
25
|
import {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
-
|
|
38
|
+
status: Project['status']
|
|
38
39
|
): 'success' | 'warning' | 'neutral' | 'danger' {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
-
|
|
58
|
-
|
|
61
|
+
const mutations = useProjectMutations({
|
|
62
|
+
onSuccess: () => {
|
|
63
|
+
refetch();
|
|
64
|
+
},
|
|
65
|
+
});
|
|
59
66
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
});
|
|
67
|
+
const handleProjectClick = useCallback((project: Project) => {
|
|
68
|
+
setSelectedProject(project);
|
|
69
|
+
setIsProjectActionsOpen(true);
|
|
70
|
+
}, []);
|
|
65
71
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
{ id: 'settings', label: 'Settings', icon: '⚙️' },
|
|
75
|
-
];
|
|
78
|
+
if (loading && !data) {
|
|
79
|
+
return <LoaderBlock label="Loading dashboard..." />;
|
|
80
|
+
}
|
|
76
81
|
|
|
77
|
-
|
|
78
|
-
|
|
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
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
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
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
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
|
-
|
|
186
|
-
|
|
193
|
+
data: ReturnType<typeof useProjectList>['data'];
|
|
194
|
+
onProjectClick?: (project: Project) => void;
|
|
187
195
|
}
|
|
188
196
|
|
|
189
197
|
function ProjectsTab({ data, onProjectClick }: ProjectsTabProps) {
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
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
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
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
|
-
|
|
248
|
+
if (!subscription) return null;
|
|
241
249
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
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
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
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
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
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
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
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
|
+
}
|