@contractspec/example.saas-boilerplate 3.7.5 → 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/CHANGELOG.md +16 -0
- 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 +14 -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 +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
|
@@ -6,160 +6,161 @@
|
|
|
6
6
|
* - UpdateProjectContract
|
|
7
7
|
* - DeleteProjectContract
|
|
8
8
|
*/
|
|
9
|
-
|
|
9
|
+
|
|
10
10
|
import { useTemplateRuntime } from '@contractspec/lib.example-shared-ui';
|
|
11
|
+
import { useCallback, useState } from 'react';
|
|
11
12
|
import type {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
13
|
+
CreateProjectInput,
|
|
14
|
+
Project,
|
|
15
|
+
SaasHandlers,
|
|
16
|
+
UpdateProjectInput,
|
|
16
17
|
} from '../../handlers/saas.handlers';
|
|
17
18
|
|
|
18
19
|
export interface MutationState<T> {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
20
|
+
loading: boolean;
|
|
21
|
+
error: Error | null;
|
|
22
|
+
data: T | null;
|
|
22
23
|
}
|
|
23
24
|
|
|
24
25
|
export interface UseProjectMutationsOptions {
|
|
25
|
-
|
|
26
|
-
|
|
26
|
+
onSuccess?: () => void;
|
|
27
|
+
onError?: (error: Error) => void;
|
|
27
28
|
}
|
|
28
29
|
|
|
29
30
|
export function useProjectMutations(options: UseProjectMutationsOptions = {}) {
|
|
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
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
31
|
+
const { handlers, projectId } = useTemplateRuntime<{ saas: SaasHandlers }>();
|
|
32
|
+
const { saas } = handlers;
|
|
33
|
+
|
|
34
|
+
const [createState, setCreateState] = useState<MutationState<Project>>({
|
|
35
|
+
loading: false,
|
|
36
|
+
error: null,
|
|
37
|
+
data: null,
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const [updateState, setUpdateState] = useState<MutationState<Project>>({
|
|
41
|
+
loading: false,
|
|
42
|
+
error: null,
|
|
43
|
+
data: null,
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
const [deleteState, setDeleteState] = useState<
|
|
47
|
+
MutationState<{ success: boolean }>
|
|
48
|
+
>({
|
|
49
|
+
loading: false,
|
|
50
|
+
error: null,
|
|
51
|
+
data: null,
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Create a new project
|
|
56
|
+
*/
|
|
57
|
+
const createProject = useCallback(
|
|
58
|
+
async (input: CreateProjectInput): Promise<Project | null> => {
|
|
59
|
+
setCreateState({ loading: true, error: null, data: null });
|
|
60
|
+
try {
|
|
61
|
+
const result = await saas.createProject(input, {
|
|
62
|
+
projectId,
|
|
63
|
+
organizationId: 'demo-org',
|
|
64
|
+
});
|
|
65
|
+
setCreateState({ loading: false, error: null, data: result });
|
|
66
|
+
options.onSuccess?.();
|
|
67
|
+
return result;
|
|
68
|
+
} catch (err) {
|
|
69
|
+
const error =
|
|
70
|
+
err instanceof Error ? err : new Error('Failed to create project');
|
|
71
|
+
setCreateState({ loading: false, error, data: null });
|
|
72
|
+
options.onError?.(error);
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
[saas, projectId, options]
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Update a project
|
|
81
|
+
*/
|
|
82
|
+
const updateProject = useCallback(
|
|
83
|
+
async (input: UpdateProjectInput): Promise<Project | null> => {
|
|
84
|
+
setUpdateState({ loading: true, error: null, data: null });
|
|
85
|
+
try {
|
|
86
|
+
const result = await saas.updateProject(input);
|
|
87
|
+
setUpdateState({ loading: false, error: null, data: result });
|
|
88
|
+
options.onSuccess?.();
|
|
89
|
+
return result;
|
|
90
|
+
} catch (err) {
|
|
91
|
+
const error =
|
|
92
|
+
err instanceof Error ? err : new Error('Failed to update project');
|
|
93
|
+
setUpdateState({ loading: false, error, data: null });
|
|
94
|
+
options.onError?.(error);
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
},
|
|
98
|
+
[saas, options]
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Delete a project (soft delete)
|
|
103
|
+
*/
|
|
104
|
+
const deleteProject = useCallback(
|
|
105
|
+
async (id: string): Promise<boolean> => {
|
|
106
|
+
setDeleteState({ loading: true, error: null, data: null });
|
|
107
|
+
try {
|
|
108
|
+
await saas.deleteProject(id);
|
|
109
|
+
setDeleteState({
|
|
110
|
+
loading: false,
|
|
111
|
+
error: null,
|
|
112
|
+
data: { success: true },
|
|
113
|
+
});
|
|
114
|
+
options.onSuccess?.();
|
|
115
|
+
return true;
|
|
116
|
+
} catch (err) {
|
|
117
|
+
const error =
|
|
118
|
+
err instanceof Error ? err : new Error('Failed to delete project');
|
|
119
|
+
setDeleteState({ loading: false, error, data: null });
|
|
120
|
+
options.onError?.(error);
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
},
|
|
124
|
+
[saas, options]
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Archive a project (status change)
|
|
129
|
+
*/
|
|
130
|
+
const archiveProject = useCallback(
|
|
131
|
+
async (id: string): Promise<Project | null> => {
|
|
132
|
+
return updateProject({ id, status: 'ARCHIVED' });
|
|
133
|
+
},
|
|
134
|
+
[updateProject]
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Activate a project (status change)
|
|
139
|
+
*/
|
|
140
|
+
const activateProject = useCallback(
|
|
141
|
+
async (id: string): Promise<Project | null> => {
|
|
142
|
+
return updateProject({ id, status: 'ACTIVE' });
|
|
143
|
+
},
|
|
144
|
+
[updateProject]
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
return {
|
|
148
|
+
// Mutations
|
|
149
|
+
createProject,
|
|
150
|
+
updateProject,
|
|
151
|
+
deleteProject,
|
|
152
|
+
archiveProject,
|
|
153
|
+
activateProject,
|
|
154
|
+
|
|
155
|
+
// State
|
|
156
|
+
createState,
|
|
157
|
+
updateState,
|
|
158
|
+
deleteState,
|
|
159
|
+
|
|
160
|
+
// Convenience
|
|
161
|
+
isLoading:
|
|
162
|
+
createState.loading || updateState.loading || deleteState.loading,
|
|
163
|
+
};
|
|
163
164
|
}
|
|
164
165
|
|
|
165
166
|
// Note: Types are re-exported from the handlers package
|
package/src/ui/index.ts
CHANGED
|
@@ -1,18 +1,14 @@
|
|
|
1
1
|
// Main dashboard
|
|
2
|
-
export * from './SaasDashboard';
|
|
3
|
-
|
|
4
|
-
// Standalone components
|
|
5
|
-
export * from './SaasProjectList';
|
|
6
|
-
export * from './SaasSettingsPanel';
|
|
7
|
-
|
|
8
|
-
// Modals
|
|
9
|
-
export * from './modals';
|
|
10
2
|
|
|
11
3
|
// Hooks
|
|
12
4
|
export * from './hooks';
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
export * from './renderers';
|
|
16
|
-
|
|
5
|
+
// Modals
|
|
6
|
+
export * from './modals';
|
|
17
7
|
// Overlays
|
|
18
8
|
export * from './overlays';
|
|
9
|
+
// Renderers
|
|
10
|
+
export * from './renderers';
|
|
11
|
+
export * from './SaasDashboard';
|
|
12
|
+
// Standalone components
|
|
13
|
+
export * from './SaasProjectList';
|
|
14
|
+
export * from './SaasSettingsPanel';
|
|
@@ -1,176 +1,176 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
+
import { Button, Input } from '@contractspec/lib.design-system';
|
|
3
4
|
/**
|
|
4
5
|
* CreateProjectModal - Form for creating a new project
|
|
5
6
|
*
|
|
6
7
|
* Wires to CreateProjectContract via useProjectMutations hook.
|
|
7
8
|
*/
|
|
8
9
|
import { useState } from 'react';
|
|
9
|
-
import { Button, Input } from '@contractspec/lib.design-system';
|
|
10
10
|
|
|
11
11
|
// Local type definition for modal props
|
|
12
12
|
export interface CreateProjectInput {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
13
|
+
name: string;
|
|
14
|
+
description?: string;
|
|
15
|
+
tier: 'FREE' | 'PRO' | 'ENTERPRISE';
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
interface CreateProjectModalProps {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
19
|
+
isOpen: boolean;
|
|
20
|
+
onClose: () => void;
|
|
21
|
+
onSubmit: (input: CreateProjectInput) => Promise<void>;
|
|
22
|
+
isLoading?: boolean;
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
const TIERS: { value: CreateProjectInput['tier']; label: string }[] = [
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
26
|
+
{ value: 'FREE', label: 'Free' },
|
|
27
|
+
{ value: 'PRO', label: 'Pro' },
|
|
28
|
+
{ value: 'ENTERPRISE', label: 'Enterprise' },
|
|
29
29
|
];
|
|
30
30
|
|
|
31
31
|
export function CreateProjectModal({
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
32
|
+
isOpen,
|
|
33
|
+
onClose,
|
|
34
|
+
onSubmit,
|
|
35
|
+
isLoading = false,
|
|
36
36
|
}: CreateProjectModalProps) {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
37
|
+
const [name, setName] = useState('');
|
|
38
|
+
const [description, setDescription] = useState('');
|
|
39
|
+
const [tier, setTier] = useState<CreateProjectInput['tier']>('FREE');
|
|
40
|
+
const [error, setError] = useState<string | null>(null);
|
|
41
|
+
|
|
42
|
+
const handleSubmit = async (e: React.FormEvent) => {
|
|
43
|
+
e.preventDefault();
|
|
44
|
+
setError(null);
|
|
45
|
+
|
|
46
|
+
// Validation
|
|
47
|
+
if (!name.trim()) {
|
|
48
|
+
setError('Project name is required');
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
await onSubmit({
|
|
54
|
+
name: name.trim(),
|
|
55
|
+
description: description.trim() || undefined,
|
|
56
|
+
tier,
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
// Reset form
|
|
60
|
+
setName('');
|
|
61
|
+
setDescription('');
|
|
62
|
+
setTier('FREE');
|
|
63
|
+
onClose();
|
|
64
|
+
} catch (err) {
|
|
65
|
+
setError(err instanceof Error ? err.message : 'Failed to create project');
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
if (!isOpen) return null;
|
|
70
|
+
|
|
71
|
+
return (
|
|
72
|
+
<div className="fixed inset-0 z-50 flex items-center justify-center">
|
|
73
|
+
{/* Backdrop */}
|
|
74
|
+
<div
|
|
75
|
+
className="absolute inset-0 bg-background/80 backdrop-blur-sm"
|
|
76
|
+
onClick={onClose}
|
|
77
|
+
role="button"
|
|
78
|
+
tabIndex={0}
|
|
79
|
+
onKeyDown={(e) => {
|
|
80
|
+
if (e.key === 'Enter' || e.key === ' ') onClose();
|
|
81
|
+
}}
|
|
82
|
+
aria-label="Close modal"
|
|
83
|
+
/>
|
|
84
|
+
|
|
85
|
+
{/* Modal */}
|
|
86
|
+
<div className="relative z-10 w-full max-w-md rounded-xl border border-border bg-card p-6 shadow-xl">
|
|
87
|
+
<h2 className="mb-4 font-semibold text-xl">Create New Project</h2>
|
|
88
|
+
|
|
89
|
+
<form onSubmit={handleSubmit} className="space-y-4">
|
|
90
|
+
{/* Project Name */}
|
|
91
|
+
<div>
|
|
92
|
+
<label
|
|
93
|
+
htmlFor="project-name"
|
|
94
|
+
className="mb-1 block font-medium text-muted-foreground text-sm"
|
|
95
|
+
>
|
|
96
|
+
Project Name *
|
|
97
|
+
</label>
|
|
98
|
+
<Input
|
|
99
|
+
id="project-name"
|
|
100
|
+
value={name}
|
|
101
|
+
onChange={(e) => setName(e.target.value)}
|
|
102
|
+
placeholder="e.g., My Awesome Project"
|
|
103
|
+
disabled={isLoading}
|
|
104
|
+
/>
|
|
105
|
+
</div>
|
|
106
|
+
|
|
107
|
+
{/* Description */}
|
|
108
|
+
<div>
|
|
109
|
+
<label
|
|
110
|
+
htmlFor="project-description"
|
|
111
|
+
className="mb-1 block font-medium text-muted-foreground text-sm"
|
|
112
|
+
>
|
|
113
|
+
Description
|
|
114
|
+
</label>
|
|
115
|
+
<textarea
|
|
116
|
+
id="project-description"
|
|
117
|
+
value={description}
|
|
118
|
+
onChange={(e) => setDescription(e.target.value)}
|
|
119
|
+
placeholder="Describe what this project is about..."
|
|
120
|
+
rows={3}
|
|
121
|
+
disabled={isLoading}
|
|
122
|
+
className="w-full rounded-md border border-input bg-background px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-ring disabled:opacity-50"
|
|
123
|
+
/>
|
|
124
|
+
</div>
|
|
125
|
+
|
|
126
|
+
{/* Tier */}
|
|
127
|
+
<div>
|
|
128
|
+
<label
|
|
129
|
+
htmlFor="project-tier"
|
|
130
|
+
className="mb-1 block font-medium text-muted-foreground text-sm"
|
|
131
|
+
>
|
|
132
|
+
Tier
|
|
133
|
+
</label>
|
|
134
|
+
<select
|
|
135
|
+
id="project-tier"
|
|
136
|
+
value={tier}
|
|
137
|
+
onChange={(e) =>
|
|
138
|
+
setTier(e.target.value as CreateProjectInput['tier'])
|
|
139
|
+
}
|
|
140
|
+
disabled={isLoading}
|
|
141
|
+
className="h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-ring disabled:opacity-50"
|
|
142
|
+
>
|
|
143
|
+
{TIERS.map((t) => (
|
|
144
|
+
<option key={t.value} value={t.value}>
|
|
145
|
+
{t.label}
|
|
146
|
+
</option>
|
|
147
|
+
))}
|
|
148
|
+
</select>
|
|
149
|
+
</div>
|
|
150
|
+
|
|
151
|
+
{/* Error Message */}
|
|
152
|
+
{error && (
|
|
153
|
+
<div className="rounded-md bg-destructive/10 p-3 text-destructive text-sm">
|
|
154
|
+
{error}
|
|
155
|
+
</div>
|
|
156
|
+
)}
|
|
157
|
+
|
|
158
|
+
{/* Actions */}
|
|
159
|
+
<div className="flex justify-end gap-3 pt-2">
|
|
160
|
+
<Button
|
|
161
|
+
type="button"
|
|
162
|
+
variant="ghost"
|
|
163
|
+
onPress={onClose}
|
|
164
|
+
disabled={isLoading}
|
|
165
|
+
>
|
|
166
|
+
Cancel
|
|
167
|
+
</Button>
|
|
168
|
+
<Button type="submit" disabled={isLoading}>
|
|
169
|
+
{isLoading ? 'Creating...' : 'Create Project'}
|
|
170
|
+
</Button>
|
|
171
|
+
</div>
|
|
172
|
+
</form>
|
|
173
|
+
</div>
|
|
174
|
+
</div>
|
|
175
|
+
);
|
|
176
176
|
}
|