@ereactthohir/cli 1.1.0 → 1.2.0
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/README.md +113 -1
- package/dist/commands/create.d.ts.map +1 -1
- package/dist/commands/create.js +1711 -6
- package/dist/commands/create.js.map +1 -1
- package/dist/commands/generators.d.ts.map +1 -1
- package/dist/commands/generators.js +302 -41
- package/dist/commands/generators.js.map +1 -1
- package/package.json +1 -1
package/dist/commands/create.js
CHANGED
|
@@ -54,7 +54,19 @@ async function create(name) {
|
|
|
54
54
|
type: 'list',
|
|
55
55
|
name: 'uiSystem',
|
|
56
56
|
message: 'UI Style:',
|
|
57
|
-
choices: [
|
|
57
|
+
choices: [
|
|
58
|
+
'Rice UI (default, official)',
|
|
59
|
+
'Tailwind CSS',
|
|
60
|
+
'Bootstrap 5',
|
|
61
|
+
'JokoUI',
|
|
62
|
+
'DaisyUI',
|
|
63
|
+
'Mantine',
|
|
64
|
+
'Material UI',
|
|
65
|
+
'Ant Design',
|
|
66
|
+
'Chakra UI',
|
|
67
|
+
'Shadcn/ui',
|
|
68
|
+
'None (Custom CSS)'
|
|
69
|
+
]
|
|
58
70
|
},
|
|
59
71
|
{
|
|
60
72
|
type: 'list',
|
|
@@ -214,6 +226,11 @@ DB_DRIVER=${answers.database.includes('sawit') ? 'sawit' : answers.database.toLo
|
|
|
214
226
|
"App\\Providers\\AuthServiceProvider"
|
|
215
227
|
]
|
|
216
228
|
}, { spaces: 2 });
|
|
229
|
+
// Generate template structure based on selection
|
|
230
|
+
if (answers.template !== 'None (Blank)') {
|
|
231
|
+
spinner.text = `Generating ${answers.template} template...`;
|
|
232
|
+
await generateTemplateStructure(projectDir, answers.template, answers.uiSystem);
|
|
233
|
+
}
|
|
217
234
|
// Install Semouth if selected
|
|
218
235
|
if (answers.installSemouth) {
|
|
219
236
|
spinner.text = 'Installing Semouth components...';
|
|
@@ -221,14 +238,25 @@ DB_DRIVER=${answers.database.includes('sawit') ? 'sawit' : answers.database.toLo
|
|
|
221
238
|
}
|
|
222
239
|
// Write package.json
|
|
223
240
|
const dependencies = {
|
|
224
|
-
"@ereactthohir/core": "^1.
|
|
225
|
-
"@ereactthohir/rice-ui": "^1.
|
|
241
|
+
"@ereactthohir/core": "^1.2.0",
|
|
242
|
+
"@ereactthohir/rice-ui": "^1.2.0",
|
|
226
243
|
"react": "^18.0.0",
|
|
227
244
|
"react-dom": "^18.0.0"
|
|
228
245
|
};
|
|
246
|
+
// Add UI System dependencies
|
|
247
|
+
const uiDependencies = getUIDependencies(answers.uiSystem);
|
|
248
|
+
Object.assign(dependencies, uiDependencies);
|
|
249
|
+
// Add Template-specific dependencies
|
|
250
|
+
const templateDependencies = getTemplateDependencies(answers.template);
|
|
251
|
+
Object.assign(dependencies, templateDependencies);
|
|
229
252
|
// Add Semouth dependencies if installed
|
|
230
253
|
if (answers.installSemouth) {
|
|
231
|
-
dependencies["@ereactthohir/semouth"] = "^1.
|
|
254
|
+
dependencies["@ereactthohir/semouth"] = "^1.2.0";
|
|
255
|
+
}
|
|
256
|
+
// Add UI configuration file if not using default Rice UI
|
|
257
|
+
if (answers.uiSystem !== 'Rice UI (default, official)') {
|
|
258
|
+
spinner.text = 'Configuring UI system...';
|
|
259
|
+
await configureUISystem(projectDir, answers.uiSystem);
|
|
232
260
|
}
|
|
233
261
|
await fs_extra_1.default.writeJSON(path_1.default.join(projectDir, 'package.json'), {
|
|
234
262
|
name,
|
|
@@ -240,10 +268,11 @@ DB_DRIVER=${answers.database.includes('sawit') ? 'sawit' : answers.database.toLo
|
|
|
240
268
|
},
|
|
241
269
|
dependencies,
|
|
242
270
|
devDependencies: {
|
|
243
|
-
"@ereactthohir/cli": "^1.
|
|
271
|
+
"@ereactthohir/cli": "^1.2.0",
|
|
244
272
|
"typescript": "^5.0.0",
|
|
245
273
|
"@types/react": "^18.0.0",
|
|
246
|
-
"@types/react-dom": "^18.0.0"
|
|
274
|
+
"@types/react-dom": "^18.0.0",
|
|
275
|
+
...getUIDevDependencies(answers.uiSystem)
|
|
247
276
|
}
|
|
248
277
|
}, { spaces: 2 });
|
|
249
278
|
spinner.succeed(chalk_1.default.green('Project created successfully!'));
|
|
@@ -581,4 +610,1680 @@ const code = 2fa.generateCode(secret);
|
|
|
581
610
|
For more information, refer to the [Security Guide](../../docs/SECURITY.md).
|
|
582
611
|
`);
|
|
583
612
|
}
|
|
613
|
+
/**
|
|
614
|
+
* Generate template structure based on template selection
|
|
615
|
+
*/
|
|
616
|
+
async function generateTemplateStructure(projectDir, template, uiSystem) {
|
|
617
|
+
// Create template-specific directories
|
|
618
|
+
await fs_extra_1.default.ensureDir(path_1.default.join(projectDir, 'resources/components'));
|
|
619
|
+
await fs_extra_1.default.ensureDir(path_1.default.join(projectDir, 'resources/pages'));
|
|
620
|
+
await fs_extra_1.default.ensureDir(path_1.default.join(projectDir, 'resources/stores'));
|
|
621
|
+
await fs_extra_1.default.ensureDir(path_1.default.join(projectDir, 'resources/hooks'));
|
|
622
|
+
await fs_extra_1.default.ensureDir(path_1.default.join(projectDir, 'resources/services'));
|
|
623
|
+
switch (template) {
|
|
624
|
+
case 'Mobile App Starter':
|
|
625
|
+
await generateMobileAppTemplate(projectDir, uiSystem);
|
|
626
|
+
break;
|
|
627
|
+
case 'Admin Dashboard':
|
|
628
|
+
await generateAdminDashboardTemplate(projectDir, uiSystem);
|
|
629
|
+
break;
|
|
630
|
+
case 'Authentication Starter':
|
|
631
|
+
await generateAuthenticationTemplate(projectDir, uiSystem);
|
|
632
|
+
break;
|
|
633
|
+
case 'Enterprise App Template':
|
|
634
|
+
await generateEnterpriseTemplate(projectDir, uiSystem);
|
|
635
|
+
break;
|
|
636
|
+
case 'SaaS Template':
|
|
637
|
+
await generateSaaSTemplate(projectDir, uiSystem);
|
|
638
|
+
break;
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
/**
|
|
642
|
+
* Generate Mobile App Starter template
|
|
643
|
+
*/
|
|
644
|
+
async function generateMobileAppTemplate(projectDir, uiSystem) {
|
|
645
|
+
// Create API service
|
|
646
|
+
await fs_extra_1.default.writeFile(path_1.default.join(projectDir, 'resources/services/api.ts'), `import axios, { AxiosInstance } from 'axios';
|
|
647
|
+
|
|
648
|
+
class ApiService {
|
|
649
|
+
private api: AxiosInstance;
|
|
650
|
+
|
|
651
|
+
constructor() {
|
|
652
|
+
this.api = axios.create({
|
|
653
|
+
baseURL: process.env.REACT_APP_API_URL || 'http://localhost:3000/api',
|
|
654
|
+
timeout: 10000,
|
|
655
|
+
});
|
|
656
|
+
|
|
657
|
+
// Add request interceptor
|
|
658
|
+
this.api.interceptors.request.use((config) => {
|
|
659
|
+
const token = localStorage.getItem('auth_token');
|
|
660
|
+
if (token) {
|
|
661
|
+
config.headers.Authorization = \`Bearer \${token}\`;
|
|
662
|
+
}
|
|
663
|
+
return config;
|
|
664
|
+
});
|
|
665
|
+
|
|
666
|
+
// Add response interceptor
|
|
667
|
+
this.api.interceptors.response.use(
|
|
668
|
+
(response) => response,
|
|
669
|
+
(error) => {
|
|
670
|
+
if (error.response?.status === 401) {
|
|
671
|
+
localStorage.removeItem('auth_token');
|
|
672
|
+
window.location.href = '/login';
|
|
673
|
+
}
|
|
674
|
+
return Promise.reject(error);
|
|
675
|
+
}
|
|
676
|
+
);
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
async get(url: string, config?: any) {
|
|
680
|
+
return this.api.get(url, config);
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
async post(url: string, data?: any, config?: any) {
|
|
684
|
+
return this.api.post(url, data, config);
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
async put(url: string, data?: any, config?: any) {
|
|
688
|
+
return this.api.put(url, data, config);
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
async delete(url: string, config?: any) {
|
|
692
|
+
return this.api.delete(url, config);
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
export default new ApiService();
|
|
697
|
+
`);
|
|
698
|
+
// Create state management (Zustand)
|
|
699
|
+
await fs_extra_1.default.writeFile(path_1.default.join(projectDir, 'resources/stores/useAppStore.ts'), `import { create } from 'zustand';
|
|
700
|
+
|
|
701
|
+
interface AppState {
|
|
702
|
+
user: any | null;
|
|
703
|
+
isLoading: boolean;
|
|
704
|
+
notifications: any[];
|
|
705
|
+
|
|
706
|
+
setUser: (user: any) => void;
|
|
707
|
+
setLoading: (loading: boolean) => void;
|
|
708
|
+
addNotification: (notification: any) => void;
|
|
709
|
+
removeNotification: (id: string) => void;
|
|
710
|
+
logout: () => void;
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
export const useAppStore = create<AppState>((set) => ({
|
|
714
|
+
user: localStorage.getItem('user') ? JSON.parse(localStorage.getItem('user')!) : null,
|
|
715
|
+
isLoading: false,
|
|
716
|
+
notifications: [],
|
|
717
|
+
|
|
718
|
+
setUser: (user) => {
|
|
719
|
+
localStorage.setItem('user', JSON.stringify(user));
|
|
720
|
+
set({ user });
|
|
721
|
+
},
|
|
722
|
+
|
|
723
|
+
setLoading: (loading) => set({ isLoading: loading }),
|
|
724
|
+
|
|
725
|
+
addNotification: (notification) =>
|
|
726
|
+
set((state) => ({ notifications: [...state.notifications, notification] })),
|
|
727
|
+
|
|
728
|
+
removeNotification: (id) =>
|
|
729
|
+
set((state) => ({
|
|
730
|
+
notifications: state.notifications.filter((n) => n.id !== id),
|
|
731
|
+
})),
|
|
732
|
+
|
|
733
|
+
logout: () => {
|
|
734
|
+
localStorage.removeItem('auth_token');
|
|
735
|
+
localStorage.removeItem('user');
|
|
736
|
+
set({ user: null });
|
|
737
|
+
},
|
|
738
|
+
}));
|
|
739
|
+
`);
|
|
740
|
+
// Create Home page
|
|
741
|
+
await fs_extra_1.default.writeFile(path_1.default.join(projectDir, 'resources/pages/Home.tsx'), `import React, { useEffect, useState } from 'react';
|
|
742
|
+
import { useAppStore } from '../stores/useAppStore';
|
|
743
|
+
import apiService from '../services/api';
|
|
744
|
+
|
|
745
|
+
export default function Home() {
|
|
746
|
+
const { user, isLoading, setLoading } = useAppStore();
|
|
747
|
+
const [data, setData] = useState<any[]>([]);
|
|
748
|
+
|
|
749
|
+
useEffect(() => {
|
|
750
|
+
fetchData();
|
|
751
|
+
}, []);
|
|
752
|
+
|
|
753
|
+
const fetchData = async () => {
|
|
754
|
+
try {
|
|
755
|
+
setLoading(true);
|
|
756
|
+
const response = await apiService.get('/data');
|
|
757
|
+
setData(response.data);
|
|
758
|
+
} catch (error) {
|
|
759
|
+
console.error('Failed to fetch data:', error);
|
|
760
|
+
} finally {
|
|
761
|
+
setLoading(false);
|
|
762
|
+
}
|
|
763
|
+
};
|
|
764
|
+
|
|
765
|
+
return (
|
|
766
|
+
<div className="p-4">
|
|
767
|
+
<h1 className="text-2xl font-bold mb-4">Welcome, {user?.name || 'Guest'}</h1>
|
|
768
|
+
{isLoading && <p>Loading...</p>}
|
|
769
|
+
<div className="grid gap-4">
|
|
770
|
+
{data.map((item, idx) => (
|
|
771
|
+
<div key={idx} className="border rounded p-4">
|
|
772
|
+
<h2 className="font-semibold">{item.title}</h2>
|
|
773
|
+
<p>{item.description}</p>
|
|
774
|
+
</div>
|
|
775
|
+
))}
|
|
776
|
+
</div>
|
|
777
|
+
</div>
|
|
778
|
+
);
|
|
779
|
+
}
|
|
780
|
+
`);
|
|
781
|
+
// Create routes
|
|
782
|
+
await fs_extra_1.default.writeFile(path_1.default.join(projectDir, 'routes/mobile.ts'), `import { Route } from '@ereactthohir/core';
|
|
783
|
+
|
|
784
|
+
Route.screen('Home', () => import('../resources/pages/Home'));
|
|
785
|
+
Route.screen('Profile', () => import('../resources/pages/Profile'));
|
|
786
|
+
Route.screen('Settings', () => import('../resources/pages/Settings'));
|
|
787
|
+
`);
|
|
788
|
+
}
|
|
789
|
+
/**
|
|
790
|
+
* Generate Admin Dashboard template
|
|
791
|
+
*/
|
|
792
|
+
async function generateAdminDashboardTemplate(projectDir, uiSystem) {
|
|
793
|
+
// Create API service
|
|
794
|
+
await fs_extra_1.default.writeFile(path_1.default.join(projectDir, 'resources/services/api.ts'), `import axios from 'axios';
|
|
795
|
+
|
|
796
|
+
const api = axios.create({
|
|
797
|
+
baseURL: process.env.REACT_APP_API_URL || 'http://localhost:3000/api',
|
|
798
|
+
});
|
|
799
|
+
|
|
800
|
+
api.interceptors.request.use((config) => {
|
|
801
|
+
const token = localStorage.getItem('auth_token');
|
|
802
|
+
if (token) config.headers.Authorization = \`Bearer \${token}\`;
|
|
803
|
+
return config;
|
|
804
|
+
});
|
|
805
|
+
|
|
806
|
+
export default api;
|
|
807
|
+
`);
|
|
808
|
+
// Create Admin Store
|
|
809
|
+
await fs_extra_1.default.writeFile(path_1.default.join(projectDir, 'resources/stores/useAdminStore.ts'), `import { create } from 'zustand';
|
|
810
|
+
|
|
811
|
+
interface AdminState {
|
|
812
|
+
activeMenu: string;
|
|
813
|
+
users: any[];
|
|
814
|
+
products: any[];
|
|
815
|
+
settings: any;
|
|
816
|
+
stats: any;
|
|
817
|
+
|
|
818
|
+
setActiveMenu: (menu: string) => void;
|
|
819
|
+
setUsers: (users: any[]) => void;
|
|
820
|
+
setProducts: (products: any[]) => void;
|
|
821
|
+
setSettings: (settings: any) => void;
|
|
822
|
+
setStats: (stats: any) => void;
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
export const useAdminStore = create<AdminState>((set) => ({
|
|
826
|
+
activeMenu: 'dashboard',
|
|
827
|
+
users: [],
|
|
828
|
+
products: [],
|
|
829
|
+
settings: {},
|
|
830
|
+
stats: {},
|
|
831
|
+
|
|
832
|
+
setActiveMenu: (menu) => set({ activeMenu: menu }),
|
|
833
|
+
setUsers: (users) => set({ users }),
|
|
834
|
+
setProducts: (products) => set({ products }),
|
|
835
|
+
setSettings: (settings) => set({ settings }),
|
|
836
|
+
setStats: (stats) => set({ stats }),
|
|
837
|
+
}));
|
|
838
|
+
`);
|
|
839
|
+
// Create Dashboard component
|
|
840
|
+
await fs_extra_1.default.writeFile(path_1.default.join(projectDir, 'resources/components/Dashboard.tsx'), `import React, { useEffect } from 'react';
|
|
841
|
+
import { useAdminStore } from '../stores/useAdminStore';
|
|
842
|
+
import apiService from '../services/api';
|
|
843
|
+
|
|
844
|
+
export default function Dashboard() {
|
|
845
|
+
const { stats, setStats } = useAdminStore();
|
|
846
|
+
|
|
847
|
+
useEffect(() => {
|
|
848
|
+
loadStats();
|
|
849
|
+
}, []);
|
|
850
|
+
|
|
851
|
+
const loadStats = async () => {
|
|
852
|
+
try {
|
|
853
|
+
const response = await apiService.get('/admin/stats');
|
|
854
|
+
setStats(response.data);
|
|
855
|
+
} catch (error) {
|
|
856
|
+
console.error('Failed to load stats:', error);
|
|
857
|
+
}
|
|
858
|
+
};
|
|
859
|
+
|
|
860
|
+
return (
|
|
861
|
+
<div className="p-6">
|
|
862
|
+
<h1 className="text-3xl font-bold mb-6">Dashboard</h1>
|
|
863
|
+
|
|
864
|
+
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-6">
|
|
865
|
+
<StatCard title="Total Users" value={stats?.totalUsers || 0} />
|
|
866
|
+
<StatCard title="Total Products" value={stats?.totalProducts || 0} />
|
|
867
|
+
<StatCard title="Revenue" value={\`\$\${stats?.revenue || 0}\`} />
|
|
868
|
+
<StatCard title="Active Sessions" value={stats?.activeSessions || 0} />
|
|
869
|
+
</div>
|
|
870
|
+
|
|
871
|
+
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
|
872
|
+
<ChartSection title="Sales Overview" />
|
|
873
|
+
<ChartSection title="User Activity" />
|
|
874
|
+
</div>
|
|
875
|
+
</div>
|
|
876
|
+
);
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
function StatCard({ title, value }: { title: string; value: any }) {
|
|
880
|
+
return (
|
|
881
|
+
<div className="bg-white p-4 rounded-lg shadow">
|
|
882
|
+
<h3 className="text-gray-500 text-sm">{title}</h3>
|
|
883
|
+
<p className="text-2xl font-bold mt-2">{value}</p>
|
|
884
|
+
</div>
|
|
885
|
+
);
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
function ChartSection({ title }: { title: string }) {
|
|
889
|
+
return (
|
|
890
|
+
<div className="bg-white p-6 rounded-lg shadow">
|
|
891
|
+
<h2 className="text-lg font-semibold mb-4">{title}</h2>
|
|
892
|
+
<div className="h-64 bg-gray-100 rounded flex items-center justify-center">
|
|
893
|
+
<p className="text-gray-400">Chart placeholder</p>
|
|
894
|
+
</div>
|
|
895
|
+
</div>
|
|
896
|
+
);
|
|
897
|
+
}
|
|
898
|
+
`);
|
|
899
|
+
// Create Users management page
|
|
900
|
+
await fs_extra_1.default.writeFile(path_1.default.join(projectDir, 'resources/pages/Users.tsx'), `import React, { useEffect } from 'react';
|
|
901
|
+
import { useAdminStore } from '../stores/useAdminStore';
|
|
902
|
+
import apiService from '../services/api';
|
|
903
|
+
|
|
904
|
+
export default function Users() {
|
|
905
|
+
const { users, setUsers } = useAdminStore();
|
|
906
|
+
|
|
907
|
+
useEffect(() => {
|
|
908
|
+
loadUsers();
|
|
909
|
+
}, []);
|
|
910
|
+
|
|
911
|
+
const loadUsers = async () => {
|
|
912
|
+
try {
|
|
913
|
+
const response = await apiService.get('/admin/users');
|
|
914
|
+
setUsers(response.data);
|
|
915
|
+
} catch (error) {
|
|
916
|
+
console.error('Failed to load users:', error);
|
|
917
|
+
}
|
|
918
|
+
};
|
|
919
|
+
|
|
920
|
+
return (
|
|
921
|
+
<div className="p-6">
|
|
922
|
+
<div className="flex justify-between items-center mb-6">
|
|
923
|
+
<h1 className="text-3xl font-bold">Users Management</h1>
|
|
924
|
+
<button className="bg-blue-500 text-white px-4 py-2 rounded">Add User</button>
|
|
925
|
+
</div>
|
|
926
|
+
|
|
927
|
+
<div className="bg-white rounded-lg shadow overflow-hidden">
|
|
928
|
+
<table className="w-full">
|
|
929
|
+
<thead className="bg-gray-100">
|
|
930
|
+
<tr>
|
|
931
|
+
<th className="px-6 py-3 text-left">Name</th>
|
|
932
|
+
<th className="px-6 py-3 text-left">Email</th>
|
|
933
|
+
<th className="px-6 py-3 text-left">Role</th>
|
|
934
|
+
<th className="px-6 py-3 text-left">Status</th>
|
|
935
|
+
<th className="px-6 py-3 text-left">Actions</th>
|
|
936
|
+
</tr>
|
|
937
|
+
</thead>
|
|
938
|
+
<tbody>
|
|
939
|
+
{users.map((user) => (
|
|
940
|
+
<tr key={user.id} className="border-t hover:bg-gray-50">
|
|
941
|
+
<td className="px-6 py-3">{user.name}</td>
|
|
942
|
+
<td className="px-6 py-3">{user.email}</td>
|
|
943
|
+
<td className="px-6 py-3">{user.role}</td>
|
|
944
|
+
<td className="px-6 py-3">
|
|
945
|
+
<span className={user.active ? 'text-green-600' : 'text-red-600'}>
|
|
946
|
+
{user.active ? 'Active' : 'Inactive'}
|
|
947
|
+
</span>
|
|
948
|
+
</td>
|
|
949
|
+
<td className="px-6 py-3">
|
|
950
|
+
<button className="text-blue-500 hover:underline mr-3">Edit</button>
|
|
951
|
+
<button className="text-red-500 hover:underline">Delete</button>
|
|
952
|
+
</td>
|
|
953
|
+
</tr>
|
|
954
|
+
))}
|
|
955
|
+
</tbody>
|
|
956
|
+
</table>
|
|
957
|
+
</div>
|
|
958
|
+
</div>
|
|
959
|
+
);
|
|
960
|
+
}
|
|
961
|
+
`);
|
|
962
|
+
// Create admin routes
|
|
963
|
+
await fs_extra_1.default.writeFile(path_1.default.join(projectDir, 'routes/admin.ts'), `import { Route } from '@ereactthohir/core';
|
|
964
|
+
|
|
965
|
+
Route.middleware(['auth', 'admin']).group(() => {
|
|
966
|
+
Route.get('/dashboard', 'AdminController@dashboard').name('admin.dashboard');
|
|
967
|
+
Route.get('/users', 'AdminController@users').name('admin.users');
|
|
968
|
+
Route.get('/products', 'AdminController@products').name('admin.products');
|
|
969
|
+
Route.get('/settings', 'AdminController@settings').name('admin.settings');
|
|
970
|
+
});
|
|
971
|
+
`);
|
|
972
|
+
}
|
|
973
|
+
/**
|
|
974
|
+
* Generate Authentication Starter template
|
|
975
|
+
*/
|
|
976
|
+
async function generateAuthenticationTemplate(projectDir, uiSystem) {
|
|
977
|
+
// Create Auth store
|
|
978
|
+
await fs_extra_1.default.writeFile(path_1.default.join(projectDir, 'resources/stores/useAuthStore.ts'), `import { create } from 'zustand';
|
|
979
|
+
import apiService from '../services/api';
|
|
980
|
+
|
|
981
|
+
interface AuthState {
|
|
982
|
+
user: any | null;
|
|
983
|
+
isAuthenticated: boolean;
|
|
984
|
+
isLoading: boolean;
|
|
985
|
+
error: string | null;
|
|
986
|
+
|
|
987
|
+
login: (email: string, password: string) => Promise<void>;
|
|
988
|
+
register: (data: any) => Promise<void>;
|
|
989
|
+
logout: () => Promise<void>;
|
|
990
|
+
refresh: () => Promise<void>;
|
|
991
|
+
setError: (error: string | null) => void;
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
export const useAuthStore = create<AuthState>((set) => ({
|
|
995
|
+
user: localStorage.getItem('user') ? JSON.parse(localStorage.getItem('user')!) : null,
|
|
996
|
+
isAuthenticated: !!localStorage.getItem('auth_token'),
|
|
997
|
+
isLoading: false,
|
|
998
|
+
error: null,
|
|
999
|
+
|
|
1000
|
+
login: async (email, password) => {
|
|
1001
|
+
set({ isLoading: true, error: null });
|
|
1002
|
+
try {
|
|
1003
|
+
const response = await apiService.post('/auth/login', { email, password });
|
|
1004
|
+
localStorage.setItem('auth_token', response.data.token);
|
|
1005
|
+
localStorage.setItem('user', JSON.stringify(response.data.user));
|
|
1006
|
+
set({ user: response.data.user, isAuthenticated: true });
|
|
1007
|
+
} catch (error: any) {
|
|
1008
|
+
set({ error: error.response?.data?.message || 'Login failed' });
|
|
1009
|
+
throw error;
|
|
1010
|
+
} finally {
|
|
1011
|
+
set({ isLoading: false });
|
|
1012
|
+
}
|
|
1013
|
+
},
|
|
1014
|
+
|
|
1015
|
+
register: async (data) => {
|
|
1016
|
+
set({ isLoading: true, error: null });
|
|
1017
|
+
try {
|
|
1018
|
+
const response = await apiService.post('/auth/register', data);
|
|
1019
|
+
localStorage.setItem('auth_token', response.data.token);
|
|
1020
|
+
localStorage.setItem('user', JSON.stringify(response.data.user));
|
|
1021
|
+
set({ user: response.data.user, isAuthenticated: true });
|
|
1022
|
+
} catch (error: any) {
|
|
1023
|
+
set({ error: error.response?.data?.message || 'Registration failed' });
|
|
1024
|
+
throw error;
|
|
1025
|
+
} finally {
|
|
1026
|
+
set({ isLoading: false });
|
|
1027
|
+
}
|
|
1028
|
+
},
|
|
1029
|
+
|
|
1030
|
+
logout: async () => {
|
|
1031
|
+
try {
|
|
1032
|
+
await apiService.post('/auth/logout');
|
|
1033
|
+
} finally {
|
|
1034
|
+
localStorage.removeItem('auth_token');
|
|
1035
|
+
localStorage.removeItem('user');
|
|
1036
|
+
set({ user: null, isAuthenticated: false });
|
|
1037
|
+
}
|
|
1038
|
+
},
|
|
1039
|
+
|
|
1040
|
+
refresh: async () => {
|
|
1041
|
+
try {
|
|
1042
|
+
const response = await apiService.post('/auth/refresh');
|
|
1043
|
+
localStorage.setItem('auth_token', response.data.token);
|
|
1044
|
+
localStorage.setItem('user', JSON.stringify(response.data.user));
|
|
1045
|
+
set({ user: response.data.user });
|
|
1046
|
+
} catch (error) {
|
|
1047
|
+
localStorage.removeItem('auth_token');
|
|
1048
|
+
set({ user: null, isAuthenticated: false });
|
|
1049
|
+
}
|
|
1050
|
+
},
|
|
1051
|
+
|
|
1052
|
+
setError: (error) => set({ error }),
|
|
1053
|
+
}));
|
|
1054
|
+
`);
|
|
1055
|
+
// Create API service
|
|
1056
|
+
await fs_extra_1.default.writeFile(path_1.default.join(projectDir, 'resources/services/api.ts'), `import axios from 'axios';
|
|
1057
|
+
|
|
1058
|
+
const api = axios.create({
|
|
1059
|
+
baseURL: process.env.REACT_APP_API_URL || 'http://localhost:3000/api',
|
|
1060
|
+
});
|
|
1061
|
+
|
|
1062
|
+
api.interceptors.request.use((config) => {
|
|
1063
|
+
const token = localStorage.getItem('auth_token');
|
|
1064
|
+
if (token) config.headers.Authorization = \`Bearer \${token}\`;
|
|
1065
|
+
return config;
|
|
1066
|
+
});
|
|
1067
|
+
|
|
1068
|
+
api.interceptors.response.use(
|
|
1069
|
+
(response) => response,
|
|
1070
|
+
async (error) => {
|
|
1071
|
+
if (error.response?.status === 401) {
|
|
1072
|
+
localStorage.removeItem('auth_token');
|
|
1073
|
+
window.location.href = '/login';
|
|
1074
|
+
}
|
|
1075
|
+
return Promise.reject(error);
|
|
1076
|
+
}
|
|
1077
|
+
);
|
|
1078
|
+
|
|
1079
|
+
export default api;
|
|
1080
|
+
`);
|
|
1081
|
+
// Create Login page
|
|
1082
|
+
await fs_extra_1.default.writeFile(path_1.default.join(projectDir, 'resources/pages/Login.tsx'), `import React, { useState } from 'react';
|
|
1083
|
+
import { useAuthStore } from '../stores/useAuthStore';
|
|
1084
|
+
import { useNavigate } from 'react-router-dom';
|
|
1085
|
+
|
|
1086
|
+
export default function Login() {
|
|
1087
|
+
const [email, setEmail] = useState('');
|
|
1088
|
+
const [password, setPassword] = useState('');
|
|
1089
|
+
const { login, isLoading, error } = useAuthStore();
|
|
1090
|
+
const navigate = useNavigate();
|
|
1091
|
+
|
|
1092
|
+
const handleSubmit = async (e: React.FormEvent) => {
|
|
1093
|
+
e.preventDefault();
|
|
1094
|
+
try {
|
|
1095
|
+
await login(email, password);
|
|
1096
|
+
navigate('/dashboard');
|
|
1097
|
+
} catch (error) {
|
|
1098
|
+
console.error('Login failed');
|
|
1099
|
+
}
|
|
1100
|
+
};
|
|
1101
|
+
|
|
1102
|
+
return (
|
|
1103
|
+
<div className="flex items-center justify-center min-h-screen bg-gray-100">
|
|
1104
|
+
<div className="w-full max-w-md bg-white rounded-lg shadow">
|
|
1105
|
+
<div className="p-8">
|
|
1106
|
+
<h1 className="text-2xl font-bold text-center mb-6">Login</h1>
|
|
1107
|
+
|
|
1108
|
+
{error && (
|
|
1109
|
+
<div className="mb-4 p-4 bg-red-100 text-red-700 rounded">
|
|
1110
|
+
{error}
|
|
1111
|
+
</div>
|
|
1112
|
+
)}
|
|
1113
|
+
|
|
1114
|
+
<form onSubmit={handleSubmit}>
|
|
1115
|
+
<div className="mb-4">
|
|
1116
|
+
<label className="block text-gray-700 text-sm font-semibold mb-2">
|
|
1117
|
+
Email
|
|
1118
|
+
</label>
|
|
1119
|
+
<input
|
|
1120
|
+
type="email"
|
|
1121
|
+
value={email}
|
|
1122
|
+
onChange={(e) => setEmail(e.target.value)}
|
|
1123
|
+
className="w-full px-4 py-2 border rounded-lg focus:outline-none focus:border-blue-500"
|
|
1124
|
+
required
|
|
1125
|
+
/>
|
|
1126
|
+
</div>
|
|
1127
|
+
|
|
1128
|
+
<div className="mb-6">
|
|
1129
|
+
<label className="block text-gray-700 text-sm font-semibold mb-2">
|
|
1130
|
+
Password
|
|
1131
|
+
</label>
|
|
1132
|
+
<input
|
|
1133
|
+
type="password"
|
|
1134
|
+
value={password}
|
|
1135
|
+
onChange={(e) => setPassword(e.target.value)}
|
|
1136
|
+
className="w-full px-4 py-2 border rounded-lg focus:outline-none focus:border-blue-500"
|
|
1137
|
+
required
|
|
1138
|
+
/>
|
|
1139
|
+
</div>
|
|
1140
|
+
|
|
1141
|
+
<button
|
|
1142
|
+
type="submit"
|
|
1143
|
+
disabled={isLoading}
|
|
1144
|
+
className="w-full bg-blue-500 text-white py-2 rounded-lg hover:bg-blue-600 disabled:bg-gray-400"
|
|
1145
|
+
>
|
|
1146
|
+
{isLoading ? 'Logging in...' : 'Login'}
|
|
1147
|
+
</button>
|
|
1148
|
+
</form>
|
|
1149
|
+
|
|
1150
|
+
<p className="text-center mt-4 text-gray-600">
|
|
1151
|
+
Don't have an account? <a href="/register" className="text-blue-500 hover:underline">Register</a>
|
|
1152
|
+
</p>
|
|
1153
|
+
</div>
|
|
1154
|
+
</div>
|
|
1155
|
+
</div>
|
|
1156
|
+
);
|
|
1157
|
+
}
|
|
1158
|
+
`);
|
|
1159
|
+
// Create Register page
|
|
1160
|
+
await fs_extra_1.default.writeFile(path_1.default.join(projectDir, 'resources/pages/Register.tsx'), `import React, { useState } from 'react';
|
|
1161
|
+
import { useAuthStore } from '../stores/useAuthStore';
|
|
1162
|
+
import { useNavigate } from 'react-router-dom';
|
|
1163
|
+
|
|
1164
|
+
export default function Register() {
|
|
1165
|
+
const [formData, setFormData] = useState({ name: '', email: '', password: '', password_confirmation: '' });
|
|
1166
|
+
const { register, isLoading, error } = useAuthStore();
|
|
1167
|
+
const navigate = useNavigate();
|
|
1168
|
+
|
|
1169
|
+
const handleSubmit = async (e: React.FormEvent) => {
|
|
1170
|
+
e.preventDefault();
|
|
1171
|
+
if (formData.password !== formData.password_confirmation) {
|
|
1172
|
+
alert('Passwords do not match');
|
|
1173
|
+
return;
|
|
1174
|
+
}
|
|
1175
|
+
try {
|
|
1176
|
+
await register(formData);
|
|
1177
|
+
navigate('/dashboard');
|
|
1178
|
+
} catch (error) {
|
|
1179
|
+
console.error('Registration failed');
|
|
1180
|
+
}
|
|
1181
|
+
};
|
|
1182
|
+
|
|
1183
|
+
return (
|
|
1184
|
+
<div className="flex items-center justify-center min-h-screen bg-gray-100">
|
|
1185
|
+
<div className="w-full max-w-md bg-white rounded-lg shadow">
|
|
1186
|
+
<div className="p-8">
|
|
1187
|
+
<h1 className="text-2xl font-bold text-center mb-6">Register</h1>
|
|
1188
|
+
|
|
1189
|
+
{error && (
|
|
1190
|
+
<div className="mb-4 p-4 bg-red-100 text-red-700 rounded">
|
|
1191
|
+
{error}
|
|
1192
|
+
</div>
|
|
1193
|
+
)}
|
|
1194
|
+
|
|
1195
|
+
<form onSubmit={handleSubmit}>
|
|
1196
|
+
<div className="mb-4">
|
|
1197
|
+
<label className="block text-gray-700 text-sm font-semibold mb-2">Name</label>
|
|
1198
|
+
<input
|
|
1199
|
+
type="text"
|
|
1200
|
+
value={formData.name}
|
|
1201
|
+
onChange={(e) => setFormData({...formData, name: e.target.value})}
|
|
1202
|
+
className="w-full px-4 py-2 border rounded-lg focus:outline-none focus:border-blue-500"
|
|
1203
|
+
required
|
|
1204
|
+
/>
|
|
1205
|
+
</div>
|
|
1206
|
+
|
|
1207
|
+
<div className="mb-4">
|
|
1208
|
+
<label className="block text-gray-700 text-sm font-semibold mb-2">Email</label>
|
|
1209
|
+
<input
|
|
1210
|
+
type="email"
|
|
1211
|
+
value={formData.email}
|
|
1212
|
+
onChange={(e) => setFormData({...formData, email: e.target.value})}
|
|
1213
|
+
className="w-full px-4 py-2 border rounded-lg focus:outline-none focus:border-blue-500"
|
|
1214
|
+
required
|
|
1215
|
+
/>
|
|
1216
|
+
</div>
|
|
1217
|
+
|
|
1218
|
+
<div className="mb-4">
|
|
1219
|
+
<label className="block text-gray-700 text-sm font-semibold mb-2">Password</label>
|
|
1220
|
+
<input
|
|
1221
|
+
type="password"
|
|
1222
|
+
value={formData.password}
|
|
1223
|
+
onChange={(e) => setFormData({...formData, password: e.target.value})}
|
|
1224
|
+
className="w-full px-4 py-2 border rounded-lg focus:outline-none focus:border-blue-500"
|
|
1225
|
+
required
|
|
1226
|
+
/>
|
|
1227
|
+
</div>
|
|
1228
|
+
|
|
1229
|
+
<div className="mb-6">
|
|
1230
|
+
<label className="block text-gray-700 text-sm font-semibold mb-2">Confirm Password</label>
|
|
1231
|
+
<input
|
|
1232
|
+
type="password"
|
|
1233
|
+
value={formData.password_confirmation}
|
|
1234
|
+
onChange={(e) => setFormData({...formData, password_confirmation: e.target.value})}
|
|
1235
|
+
className="w-full px-4 py-2 border rounded-lg focus:outline-none focus:border-blue-500"
|
|
1236
|
+
required
|
|
1237
|
+
/>
|
|
1238
|
+
</div>
|
|
1239
|
+
|
|
1240
|
+
<button
|
|
1241
|
+
type="submit"
|
|
1242
|
+
disabled={isLoading}
|
|
1243
|
+
className="w-full bg-blue-500 text-white py-2 rounded-lg hover:bg-blue-600 disabled:bg-gray-400"
|
|
1244
|
+
>
|
|
1245
|
+
{isLoading ? 'Registering...' : 'Register'}
|
|
1246
|
+
</button>
|
|
1247
|
+
</form>
|
|
1248
|
+
|
|
1249
|
+
<p className="text-center mt-4 text-gray-600">
|
|
1250
|
+
Already have an account? <a href="/login" className="text-blue-500 hover:underline">Login</a>
|
|
1251
|
+
</p>
|
|
1252
|
+
</div>
|
|
1253
|
+
</div>
|
|
1254
|
+
</div>
|
|
1255
|
+
);
|
|
1256
|
+
}
|
|
1257
|
+
`);
|
|
1258
|
+
// Create auth routes
|
|
1259
|
+
await fs_extra_1.default.writeFile(path_1.default.join(projectDir, 'routes/auth.ts'), `import { Route } from '@ereactthohir/core';
|
|
1260
|
+
|
|
1261
|
+
Route.get('/login', 'AuthController@showLogin').name('login');
|
|
1262
|
+
Route.post('/login', 'AuthController@login').name('login.store');
|
|
1263
|
+
|
|
1264
|
+
Route.get('/register', 'AuthController@showRegister').name('register');
|
|
1265
|
+
Route.post('/register', 'AuthController@register').name('register.store');
|
|
1266
|
+
|
|
1267
|
+
Route.middleware(['auth']).group(() => {
|
|
1268
|
+
Route.post('/logout', 'AuthController@logout').name('logout');
|
|
1269
|
+
});
|
|
1270
|
+
`);
|
|
1271
|
+
}
|
|
1272
|
+
/**
|
|
1273
|
+
* Generate Enterprise App Template
|
|
1274
|
+
*/
|
|
1275
|
+
async function generateEnterpriseTemplate(projectDir, uiSystem) {
|
|
1276
|
+
// Create API service with advanced interceptors
|
|
1277
|
+
await fs_extra_1.default.writeFile(path_1.default.join(projectDir, 'resources/services/api.ts'), `import axios, { AxiosInstance } from 'axios';
|
|
1278
|
+
|
|
1279
|
+
class EnterpriseApiService {
|
|
1280
|
+
private api: AxiosInstance;
|
|
1281
|
+
private requestQueue: any[] = [];
|
|
1282
|
+
private isRefreshing = false;
|
|
1283
|
+
|
|
1284
|
+
constructor() {
|
|
1285
|
+
this.api = axios.create({
|
|
1286
|
+
baseURL: process.env.REACT_APP_API_URL || 'http://localhost:3000/api',
|
|
1287
|
+
timeout: 15000,
|
|
1288
|
+
});
|
|
1289
|
+
|
|
1290
|
+
this.setupInterceptors();
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1293
|
+
private setupInterceptors() {
|
|
1294
|
+
this.api.interceptors.request.use((config) => {
|
|
1295
|
+
const token = localStorage.getItem('auth_token');
|
|
1296
|
+
if (token) {
|
|
1297
|
+
config.headers.Authorization = \`Bearer \${token}\`;
|
|
1298
|
+
}
|
|
1299
|
+
config.headers['X-Client-Version'] = '1.0.0';
|
|
1300
|
+
return config;
|
|
1301
|
+
});
|
|
1302
|
+
|
|
1303
|
+
this.api.interceptors.response.use(
|
|
1304
|
+
(response) => response,
|
|
1305
|
+
async (error) => {
|
|
1306
|
+
const originalRequest = error.config;
|
|
1307
|
+
|
|
1308
|
+
if (error.response?.status === 401 && !originalRequest._retry) {
|
|
1309
|
+
if (!this.isRefreshing) {
|
|
1310
|
+
this.isRefreshing = true;
|
|
1311
|
+
try {
|
|
1312
|
+
const response = await this.api.post('/auth/refresh');
|
|
1313
|
+
localStorage.setItem('auth_token', response.data.token);
|
|
1314
|
+
this.isRefreshing = false;
|
|
1315
|
+
this.processQueue(null);
|
|
1316
|
+
|
|
1317
|
+
originalRequest._retry = true;
|
|
1318
|
+
return this.api(originalRequest);
|
|
1319
|
+
} catch (refreshError) {
|
|
1320
|
+
this.isRefreshing = false;
|
|
1321
|
+
this.processQueue(refreshError);
|
|
1322
|
+
localStorage.removeItem('auth_token');
|
|
1323
|
+
window.location.href = '/login';
|
|
1324
|
+
}
|
|
1325
|
+
}
|
|
1326
|
+
|
|
1327
|
+
return new Promise((resolve, reject) => {
|
|
1328
|
+
this.requestQueue.push({ resolve, reject });
|
|
1329
|
+
}).then(() => this.api(originalRequest));
|
|
1330
|
+
}
|
|
1331
|
+
|
|
1332
|
+
return Promise.reject(error);
|
|
1333
|
+
}
|
|
1334
|
+
);
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1337
|
+
private processQueue(error: any) {
|
|
1338
|
+
this.requestQueue.forEach((req) => {
|
|
1339
|
+
if (error) {
|
|
1340
|
+
req.reject(error);
|
|
1341
|
+
} else {
|
|
1342
|
+
req.resolve();
|
|
1343
|
+
}
|
|
1344
|
+
});
|
|
1345
|
+
this.requestQueue = [];
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
async get(url: string, config?: any) { return this.api.get(url, config); }
|
|
1349
|
+
async post(url: string, data?: any, config?: any) { return this.api.post(url, data, config); }
|
|
1350
|
+
async put(url: string, data?: any, config?: any) { return this.api.put(url, data, config); }
|
|
1351
|
+
async patch(url: string, data?: any, config?: any) { return this.api.patch(url, data, config); }
|
|
1352
|
+
async delete(url: string, config?: any) { return this.api.delete(url, config); }
|
|
1353
|
+
}
|
|
1354
|
+
|
|
1355
|
+
export default new EnterpriseApiService();
|
|
1356
|
+
`);
|
|
1357
|
+
// Create advanced store with persistence
|
|
1358
|
+
await fs_extra_1.default.writeFile(path_1.default.join(projectDir, 'resources/stores/useEnterpriseStore.ts'), `import { create } from 'zustand';
|
|
1359
|
+
import { persist } from 'zustand/middleware';
|
|
1360
|
+
|
|
1361
|
+
interface User {
|
|
1362
|
+
id: number;
|
|
1363
|
+
name: string;
|
|
1364
|
+
email: string;
|
|
1365
|
+
role: string;
|
|
1366
|
+
permissions: string[];
|
|
1367
|
+
}
|
|
1368
|
+
|
|
1369
|
+
interface EnterpriseState {
|
|
1370
|
+
user: User | null;
|
|
1371
|
+
organization: any | null;
|
|
1372
|
+
features: string[];
|
|
1373
|
+
settings: Record<string, any>;
|
|
1374
|
+
|
|
1375
|
+
setUser: (user: User) => void;
|
|
1376
|
+
setOrganization: (org: any) => void;
|
|
1377
|
+
setFeatures: (features: string[]) => void;
|
|
1378
|
+
updateSettings: (settings: Record<string, any>) => void;
|
|
1379
|
+
hasPermission: (permission: string) => boolean;
|
|
1380
|
+
}
|
|
1381
|
+
|
|
1382
|
+
export const useEnterpriseStore = create<EnterpriseState>()(
|
|
1383
|
+
persist(
|
|
1384
|
+
(set, get) => ({
|
|
1385
|
+
user: null,
|
|
1386
|
+
organization: null,
|
|
1387
|
+
features: [],
|
|
1388
|
+
settings: {},
|
|
1389
|
+
|
|
1390
|
+
setUser: (user) => set({ user }),
|
|
1391
|
+
setOrganization: (organization) => set({ organization }),
|
|
1392
|
+
setFeatures: (features) => set({ features }),
|
|
1393
|
+
updateSettings: (settings) => set((state) => ({
|
|
1394
|
+
settings: { ...state.settings, ...settings }
|
|
1395
|
+
})),
|
|
1396
|
+
hasPermission: (permission: string) => {
|
|
1397
|
+
const { user } = get();
|
|
1398
|
+
return user?.permissions.includes(permission) || false;
|
|
1399
|
+
},
|
|
1400
|
+
}),
|
|
1401
|
+
{ name: 'enterprise-store' }
|
|
1402
|
+
)
|
|
1403
|
+
);
|
|
1404
|
+
`);
|
|
1405
|
+
// Create modules structure
|
|
1406
|
+
await fs_extra_1.default.ensureDir(path_1.default.join(projectDir, 'resources/modules'));
|
|
1407
|
+
await fs_extra_1.default.writeFile(path_1.default.join(projectDir, 'resources/modules/dashboard.ts'), `export const dashboardModule = {
|
|
1408
|
+
routes: [
|
|
1409
|
+
{ path: '/dashboard', component: 'Dashboard', name: 'dashboard' },
|
|
1410
|
+
{ path: '/analytics', component: 'Analytics', name: 'analytics' },
|
|
1411
|
+
],
|
|
1412
|
+
permissions: ['view.dashboard', 'view.analytics'],
|
|
1413
|
+
};
|
|
1414
|
+
`);
|
|
1415
|
+
await fs_extra_1.default.writeFile(path_1.default.join(projectDir, 'resources/modules/users.ts'), `export const usersModule = {
|
|
1416
|
+
routes: [
|
|
1417
|
+
{ path: '/users', component: 'UsersList', name: 'users.list' },
|
|
1418
|
+
{ path: '/users/:id', component: 'UserDetail', name: 'users.show' },
|
|
1419
|
+
{ path: '/users/create', component: 'UserForm', name: 'users.create' },
|
|
1420
|
+
],
|
|
1421
|
+
permissions: ['manage.users'],
|
|
1422
|
+
};
|
|
1423
|
+
`);
|
|
1424
|
+
// Create advanced hooks
|
|
1425
|
+
await fs_extra_1.default.writeFile(path_1.default.join(projectDir, 'resources/hooks/useApi.ts'), `import { useState, useCallback } from 'react';
|
|
1426
|
+
import apiService from '../services/api';
|
|
1427
|
+
|
|
1428
|
+
export function useApi<T>(url: string, options?: any) {
|
|
1429
|
+
const [data, setData] = useState<T | null>(null);
|
|
1430
|
+
const [loading, setLoading] = useState(false);
|
|
1431
|
+
const [error, setError] = useState<any>(null);
|
|
1432
|
+
|
|
1433
|
+
const fetch = useCallback(async () => {
|
|
1434
|
+
setLoading(true);
|
|
1435
|
+
setError(null);
|
|
1436
|
+
try {
|
|
1437
|
+
const response = await apiService.get(url, options);
|
|
1438
|
+
setData(response.data);
|
|
1439
|
+
} catch (err) {
|
|
1440
|
+
setError(err);
|
|
1441
|
+
} finally {
|
|
1442
|
+
setLoading(false);
|
|
1443
|
+
}
|
|
1444
|
+
}, [url, options]);
|
|
1445
|
+
|
|
1446
|
+
return { data, loading, error, fetch };
|
|
1447
|
+
}
|
|
1448
|
+
`);
|
|
1449
|
+
// Create main routes
|
|
1450
|
+
await fs_extra_1.default.writeFile(path_1.default.join(projectDir, 'routes/enterprise.ts'), `import { Route } from '@ereactthohir/core';
|
|
1451
|
+
|
|
1452
|
+
Route.middleware(['auth', 'verified']).group(() => {
|
|
1453
|
+
// Dashboard
|
|
1454
|
+
Route.get('/dashboard', 'DashboardController@index').name('dashboard');
|
|
1455
|
+
|
|
1456
|
+
// Users Management
|
|
1457
|
+
Route.get('/users', 'UserController@index').name('users.index');
|
|
1458
|
+
Route.get('/users/create', 'UserController@create').name('users.create');
|
|
1459
|
+
Route.post('/users', 'UserController@store').name('users.store');
|
|
1460
|
+
Route.get('/users/:id', 'UserController@show').name('users.show');
|
|
1461
|
+
Route.get('/users/:id/edit', 'UserController@edit').name('users.edit');
|
|
1462
|
+
Route.put('/users/:id', 'UserController@update').name('users.update');
|
|
1463
|
+
Route.delete('/users/:id', 'UserController@destroy').name('users.destroy');
|
|
1464
|
+
|
|
1465
|
+
// Settings
|
|
1466
|
+
Route.get('/settings', 'SettingsController@index').name('settings');
|
|
1467
|
+
Route.put('/settings', 'SettingsController@update').name('settings.update');
|
|
1468
|
+
});
|
|
1469
|
+
`);
|
|
1470
|
+
}
|
|
1471
|
+
/**
|
|
1472
|
+
* Generate SaaS Template
|
|
1473
|
+
*/
|
|
1474
|
+
async function generateSaaSTemplate(projectDir, uiSystem) {
|
|
1475
|
+
// Create subscription service
|
|
1476
|
+
await fs_extra_1.default.writeFile(path_1.default.join(projectDir, 'resources/services/subscription.ts'), `import apiService from './api';
|
|
1477
|
+
|
|
1478
|
+
export class SubscriptionService {
|
|
1479
|
+
async getPlans() {
|
|
1480
|
+
const response = await apiService.get('/subscription/plans');
|
|
1481
|
+
return response.data;
|
|
1482
|
+
}
|
|
1483
|
+
|
|
1484
|
+
async subscribe(planId: number, billingCycle: 'monthly' | 'yearly') {
|
|
1485
|
+
const response = await apiService.post('/subscription/subscribe', {
|
|
1486
|
+
plan_id: planId,
|
|
1487
|
+
billing_cycle: billingCycle,
|
|
1488
|
+
});
|
|
1489
|
+
return response.data;
|
|
1490
|
+
}
|
|
1491
|
+
|
|
1492
|
+
async getCurrentSubscription() {
|
|
1493
|
+
const response = await apiService.get('/subscription/current');
|
|
1494
|
+
return response.data;
|
|
1495
|
+
}
|
|
1496
|
+
|
|
1497
|
+
async cancelSubscription() {
|
|
1498
|
+
const response = await apiService.post('/subscription/cancel');
|
|
1499
|
+
return response.data;
|
|
1500
|
+
}
|
|
1501
|
+
|
|
1502
|
+
async upgradeSubscription(planId: number) {
|
|
1503
|
+
const response = await apiService.post('/subscription/upgrade', { plan_id: planId });
|
|
1504
|
+
return response.data;
|
|
1505
|
+
}
|
|
1506
|
+
|
|
1507
|
+
async getInvoices() {
|
|
1508
|
+
const response = await apiService.get('/subscription/invoices');
|
|
1509
|
+
return response.data;
|
|
1510
|
+
}
|
|
1511
|
+
}
|
|
1512
|
+
|
|
1513
|
+
export default new SubscriptionService();
|
|
1514
|
+
`);
|
|
1515
|
+
// Create SaaS store
|
|
1516
|
+
await fs_extra_1.default.writeFile(path_1.default.join(projectDir, 'resources/stores/useSaaSStore.ts'), `import { create } from 'zustand';
|
|
1517
|
+
import subscriptionService from '../services/subscription';
|
|
1518
|
+
|
|
1519
|
+
interface Subscription {
|
|
1520
|
+
id: number;
|
|
1521
|
+
plan: any;
|
|
1522
|
+
status: 'active' | 'canceled' | 'expired';
|
|
1523
|
+
billing_cycle: 'monthly' | 'yearly';
|
|
1524
|
+
current_period_start: string;
|
|
1525
|
+
current_period_end: string;
|
|
1526
|
+
}
|
|
1527
|
+
|
|
1528
|
+
interface SaaSState {
|
|
1529
|
+
subscription: Subscription | null;
|
|
1530
|
+
plans: any[];
|
|
1531
|
+
invoices: any[];
|
|
1532
|
+
isLoading: boolean;
|
|
1533
|
+
|
|
1534
|
+
loadPlans: () => Promise<void>;
|
|
1535
|
+
loadSubscription: () => Promise<void>;
|
|
1536
|
+
loadInvoices: () => Promise<void>;
|
|
1537
|
+
subscribe: (planId: number, cycle: string) => Promise<void>;
|
|
1538
|
+
cancelSubscription: () => Promise<void>;
|
|
1539
|
+
}
|
|
1540
|
+
|
|
1541
|
+
export const useSaaSStore = create<SaaSState>((set) => ({
|
|
1542
|
+
subscription: null,
|
|
1543
|
+
plans: [],
|
|
1544
|
+
invoices: [],
|
|
1545
|
+
isLoading: false,
|
|
1546
|
+
|
|
1547
|
+
loadPlans: async () => {
|
|
1548
|
+
set({ isLoading: true });
|
|
1549
|
+
try {
|
|
1550
|
+
const plans = await subscriptionService.getPlans();
|
|
1551
|
+
set({ plans });
|
|
1552
|
+
} finally {
|
|
1553
|
+
set({ isLoading: false });
|
|
1554
|
+
}
|
|
1555
|
+
},
|
|
1556
|
+
|
|
1557
|
+
loadSubscription: async () => {
|
|
1558
|
+
try {
|
|
1559
|
+
const subscription = await subscriptionService.getCurrentSubscription();
|
|
1560
|
+
set({ subscription });
|
|
1561
|
+
} catch (error) {
|
|
1562
|
+
console.error('Failed to load subscription:', error);
|
|
1563
|
+
}
|
|
1564
|
+
},
|
|
1565
|
+
|
|
1566
|
+
loadInvoices: async () => {
|
|
1567
|
+
try {
|
|
1568
|
+
const invoices = await subscriptionService.getInvoices();
|
|
1569
|
+
set({ invoices });
|
|
1570
|
+
} catch (error) {
|
|
1571
|
+
console.error('Failed to load invoices:', error);
|
|
1572
|
+
}
|
|
1573
|
+
},
|
|
1574
|
+
|
|
1575
|
+
subscribe: async (planId: number, cycle: string) => {
|
|
1576
|
+
set({ isLoading: true });
|
|
1577
|
+
try {
|
|
1578
|
+
await subscriptionService.subscribe(planId, cycle as any);
|
|
1579
|
+
await subscriptionService.getCurrentSubscription();
|
|
1580
|
+
} finally {
|
|
1581
|
+
set({ isLoading: false });
|
|
1582
|
+
}
|
|
1583
|
+
},
|
|
1584
|
+
|
|
1585
|
+
cancelSubscription: async () => {
|
|
1586
|
+
set({ isLoading: true });
|
|
1587
|
+
try {
|
|
1588
|
+
await subscriptionService.cancelSubscription();
|
|
1589
|
+
set({ subscription: null });
|
|
1590
|
+
} finally {
|
|
1591
|
+
set({ isLoading: false });
|
|
1592
|
+
}
|
|
1593
|
+
},
|
|
1594
|
+
}));
|
|
1595
|
+
`);
|
|
1596
|
+
// Create Pricing page
|
|
1597
|
+
await fs_extra_1.default.writeFile(path_1.default.join(projectDir, 'resources/pages/Pricing.tsx'), `import React, { useEffect } from 'react';
|
|
1598
|
+
import { useSaaSStore } from '../stores/useSaaSStore';
|
|
1599
|
+
|
|
1600
|
+
export default function Pricing() {
|
|
1601
|
+
const { plans, loadPlans, isLoading } = useSaaSStore();
|
|
1602
|
+
|
|
1603
|
+
useEffect(() => {
|
|
1604
|
+
loadPlans();
|
|
1605
|
+
}, []);
|
|
1606
|
+
|
|
1607
|
+
return (
|
|
1608
|
+
<div className="py-12 px-4">
|
|
1609
|
+
<div className="text-center mb-12">
|
|
1610
|
+
<h1 className="text-4xl font-bold mb-4">Simple, Transparent Pricing</h1>
|
|
1611
|
+
<p className="text-gray-600">Choose the plan that fits your needs</p>
|
|
1612
|
+
</div>
|
|
1613
|
+
|
|
1614
|
+
{isLoading ? (
|
|
1615
|
+
<p className="text-center">Loading plans...</p>
|
|
1616
|
+
) : (
|
|
1617
|
+
<div className="grid grid-cols-1 md:grid-cols-3 gap-8 max-w-5xl mx-auto">
|
|
1618
|
+
{plans.map((plan) => (
|
|
1619
|
+
<PricingCard key={plan.id} plan={plan} />
|
|
1620
|
+
))}
|
|
1621
|
+
</div>
|
|
1622
|
+
)}
|
|
1623
|
+
</div>
|
|
1624
|
+
);
|
|
1625
|
+
}
|
|
1626
|
+
|
|
1627
|
+
function PricingCard({ plan }: { plan: any }) {
|
|
1628
|
+
const { subscribe } = useSaaSStore();
|
|
1629
|
+
|
|
1630
|
+
return (
|
|
1631
|
+
<div className={'rounded-lg p-8 ' + (plan.featured ? 'bg-blue-50 border-2 border-blue-500' : 'bg-white border border-gray-200')}>
|
|
1632
|
+
<h3 className="text-xl font-bold mb-2">{plan.name}</h3>
|
|
1633
|
+
<p className="text-gray-600 text-sm mb-4">{plan.description}</p>
|
|
1634
|
+
|
|
1635
|
+
<div className="mb-6">
|
|
1636
|
+
<span className="text-3xl font-bold">\${plan.price}</span>
|
|
1637
|
+
<span className="text-gray-600">/month</span>
|
|
1638
|
+
</div>
|
|
1639
|
+
|
|
1640
|
+
<ul className="mb-6 space-y-2">
|
|
1641
|
+
{plan.features.map((feature: string) => (
|
|
1642
|
+
<li key={feature} className="flex items-center">
|
|
1643
|
+
<span className="mr-2">✓</span>
|
|
1644
|
+
<span>{feature}</span>
|
|
1645
|
+
</li>
|
|
1646
|
+
))}
|
|
1647
|
+
</ul>
|
|
1648
|
+
|
|
1649
|
+
<button
|
|
1650
|
+
onClick={() => subscribe(plan.id, 'monthly')}
|
|
1651
|
+
className={plan.featured ? 'w-full bg-blue-500 text-white py-2 rounded' : 'w-full border border-gray-300 py-2 rounded'}
|
|
1652
|
+
>
|
|
1653
|
+
Choose Plan
|
|
1654
|
+
</button>
|
|
1655
|
+
</div>
|
|
1656
|
+
);
|
|
1657
|
+
}
|
|
1658
|
+
`);
|
|
1659
|
+
// Create Subscription Management page
|
|
1660
|
+
await fs_extra_1.default.writeFile(path_1.default.join(projectDir, 'resources/pages/Subscription.tsx'), `import React, { useEffect } from 'react';
|
|
1661
|
+
import { useSaaSStore } from '../stores/useSaaSStore';
|
|
1662
|
+
|
|
1663
|
+
export default function Subscription() {
|
|
1664
|
+
const { subscription, invoices, loadSubscription, loadInvoices } = useSaaSStore();
|
|
1665
|
+
|
|
1666
|
+
useEffect(() => {
|
|
1667
|
+
loadSubscription();
|
|
1668
|
+
loadInvoices();
|
|
1669
|
+
}, []);
|
|
1670
|
+
|
|
1671
|
+
return (
|
|
1672
|
+
<div className="p-6 max-w-4xl mx-auto">
|
|
1673
|
+
<h1 className="text-3xl font-bold mb-8">Subscription Management</h1>
|
|
1674
|
+
|
|
1675
|
+
{subscription && (
|
|
1676
|
+
<div className="bg-white rounded-lg shadow p-6 mb-8">
|
|
1677
|
+
<h2 className="text-xl font-semibold mb-4">Current Plan</h2>
|
|
1678
|
+
<div className="grid grid-cols-2 gap-4">
|
|
1679
|
+
<div>
|
|
1680
|
+
<p className="text-gray-600">Plan</p>
|
|
1681
|
+
<p className="font-semibold">{subscription.plan.name}</p>
|
|
1682
|
+
</div>
|
|
1683
|
+
<div>
|
|
1684
|
+
<p className="text-gray-600">Status</p>
|
|
1685
|
+
<p className="font-semibold text-green-600">{subscription.status}</p>
|
|
1686
|
+
</div>
|
|
1687
|
+
<div>
|
|
1688
|
+
<p className="text-gray-600">Renewal Date</p>
|
|
1689
|
+
<p className="font-semibold">{new Date(subscription.current_period_end).toLocaleDateString()}</p>
|
|
1690
|
+
</div>
|
|
1691
|
+
</div>
|
|
1692
|
+
</div>
|
|
1693
|
+
)}
|
|
1694
|
+
|
|
1695
|
+
<div className="bg-white rounded-lg shadow overflow-hidden">
|
|
1696
|
+
<div className="p-6 border-b">
|
|
1697
|
+
<h2 className="text-xl font-semibold">Invoices</h2>
|
|
1698
|
+
</div>
|
|
1699
|
+
<table className="w-full">
|
|
1700
|
+
<thead className="bg-gray-50">
|
|
1701
|
+
<tr>
|
|
1702
|
+
<th className="px-6 py-3 text-left">Date</th>
|
|
1703
|
+
<th className="px-6 py-3 text-left">Amount</th>
|
|
1704
|
+
<th className="px-6 py-3 text-left">Status</th>
|
|
1705
|
+
<th className="px-6 py-3 text-left">Action</th>
|
|
1706
|
+
</tr>
|
|
1707
|
+
</thead>
|
|
1708
|
+
<tbody>
|
|
1709
|
+
{invoices.map((invoice) => (
|
|
1710
|
+
<tr key={invoice.id} className="border-t">
|
|
1711
|
+
<td className="px-6 py-3">{new Date(invoice.date).toLocaleDateString()}</td>
|
|
1712
|
+
<td className="px-6 py-3">\${invoice.amount}</td>
|
|
1713
|
+
<td className="px-6 py-3">{invoice.status}</td>
|
|
1714
|
+
<td className="px-6 py-3">
|
|
1715
|
+
<a href={invoice.url} className="text-blue-500 hover:underline">Download</a>
|
|
1716
|
+
</td>
|
|
1717
|
+
</tr>
|
|
1718
|
+
))}
|
|
1719
|
+
</tbody>
|
|
1720
|
+
</table>
|
|
1721
|
+
</div>
|
|
1722
|
+
</div>
|
|
1723
|
+
);
|
|
1724
|
+
}
|
|
1725
|
+
`);
|
|
1726
|
+
// Create SaaS routes
|
|
1727
|
+
await fs_extra_1.default.writeFile(path_1.default.join(projectDir, 'routes/saas.ts'), `import { Route } from '@ereactthohir/core';
|
|
1728
|
+
|
|
1729
|
+
// Public pricing
|
|
1730
|
+
Route.get('/pricing', 'PricingController@index').name('pricing');
|
|
1731
|
+
|
|
1732
|
+
Route.middleware(['auth']).group(() => {
|
|
1733
|
+
// Subscription management
|
|
1734
|
+
Route.get('/subscription', 'SubscriptionController@index').name('subscription');
|
|
1735
|
+
Route.post('/subscription/subscribe', 'SubscriptionController@subscribe').name('subscription.subscribe');
|
|
1736
|
+
Route.post('/subscription/cancel', 'SubscriptionController@cancel').name('subscription.cancel');
|
|
1737
|
+
Route.post('/subscription/upgrade', 'SubscriptionController@upgrade').name('subscription.upgrade');
|
|
1738
|
+
|
|
1739
|
+
// Invoices
|
|
1740
|
+
Route.get('/invoices', 'InvoiceController@index').name('invoices');
|
|
1741
|
+
Route.get('/invoices/:id', 'InvoiceController@show').name('invoices.show');
|
|
1742
|
+
});
|
|
1743
|
+
`);
|
|
1744
|
+
}
|
|
1745
|
+
/**
|
|
1746
|
+
* Get UI Framework dependencies based on selection
|
|
1747
|
+
*/
|
|
1748
|
+
function getUIDependencies(uiSystem) {
|
|
1749
|
+
const deps = {};
|
|
1750
|
+
switch (uiSystem) {
|
|
1751
|
+
case 'Tailwind CSS':
|
|
1752
|
+
deps['tailwindcss'] = '^3.3.0';
|
|
1753
|
+
break;
|
|
1754
|
+
case 'Bootstrap 5':
|
|
1755
|
+
deps['bootstrap'] = '^5.3.0';
|
|
1756
|
+
deps['react-bootstrap'] = '^2.8.0';
|
|
1757
|
+
break;
|
|
1758
|
+
case 'JokoUI':
|
|
1759
|
+
deps['jokoui'] = '^1.0.0';
|
|
1760
|
+
break;
|
|
1761
|
+
case 'DaisyUI':
|
|
1762
|
+
deps['daisyui'] = '^3.9.0';
|
|
1763
|
+
deps['tailwindcss'] = '^3.3.0';
|
|
1764
|
+
break;
|
|
1765
|
+
case 'Mantine':
|
|
1766
|
+
deps['@mantine/core'] = '^7.0.0';
|
|
1767
|
+
deps['@mantine/hooks'] = '^7.0.0';
|
|
1768
|
+
break;
|
|
1769
|
+
case 'Chakra UI':
|
|
1770
|
+
deps['@chakra-ui/react'] = '^2.8.0';
|
|
1771
|
+
deps['@emotion/react'] = '^11.11.0';
|
|
1772
|
+
deps['@emotion/styled'] = '^11.11.0';
|
|
1773
|
+
deps['framer-motion'] = '^10.12.0';
|
|
1774
|
+
break;
|
|
1775
|
+
case 'Shadcn/ui':
|
|
1776
|
+
deps['@radix-ui/primitives'] = '^1.0.0';
|
|
1777
|
+
deps['class-variance-authority'] = '^0.7.0';
|
|
1778
|
+
deps['clsx'] = '^2.0.0';
|
|
1779
|
+
deps['tailwindcss'] = '^3.3.0';
|
|
1780
|
+
break;
|
|
1781
|
+
case 'None (Custom CSS)':
|
|
1782
|
+
// No additional dependencies
|
|
1783
|
+
break;
|
|
1784
|
+
}
|
|
1785
|
+
return deps;
|
|
1786
|
+
}
|
|
1787
|
+
/**
|
|
1788
|
+
* Get UI Framework dev dependencies based on selection
|
|
1789
|
+
*/
|
|
1790
|
+
function getUIDevDependencies(uiSystem) {
|
|
1791
|
+
const devDeps = {};
|
|
1792
|
+
switch (uiSystem) {
|
|
1793
|
+
case 'Tailwind CSS':
|
|
1794
|
+
case 'DaisyUI': // DaisyUI uses Tailwind
|
|
1795
|
+
devDeps['tailwindcss'] = '^3.3.0';
|
|
1796
|
+
devDeps['postcss'] = '^8.4.0';
|
|
1797
|
+
devDeps['autoprefixer'] = '^10.4.0';
|
|
1798
|
+
break;
|
|
1799
|
+
case 'Bootstrap 5':
|
|
1800
|
+
devDeps['sass'] = '^1.66.0';
|
|
1801
|
+
devDeps['sass-loader'] = '^13.3.0';
|
|
1802
|
+
break;
|
|
1803
|
+
case 'Shadcn/ui':
|
|
1804
|
+
devDeps['tailwindcss'] = '^3.3.0';
|
|
1805
|
+
devDeps['postcss'] = '^8.4.0';
|
|
1806
|
+
devDeps['autoprefixer'] = '^10.4.0';
|
|
1807
|
+
break;
|
|
1808
|
+
case 'Mantine':
|
|
1809
|
+
devDeps['postcss'] = '^8.4.0';
|
|
1810
|
+
devDeps['postcss-preset-mantine'] = '^1.0.0';
|
|
1811
|
+
devDeps['postcss-simple-vars'] = '^7.0.0';
|
|
1812
|
+
break;
|
|
1813
|
+
}
|
|
1814
|
+
return devDeps;
|
|
1815
|
+
}
|
|
1816
|
+
/**
|
|
1817
|
+
* Get Template-specific dependencies
|
|
1818
|
+
*/
|
|
1819
|
+
function getTemplateDependencies(template) {
|
|
1820
|
+
const deps = {};
|
|
1821
|
+
// Common dependencies for all templates (except Blank)
|
|
1822
|
+
if (template !== 'None (Blank)') {
|
|
1823
|
+
deps['axios'] = '^1.4.0';
|
|
1824
|
+
deps['zustand'] = '^4.4.0';
|
|
1825
|
+
deps['react-router-dom'] = '^6.14.0';
|
|
1826
|
+
}
|
|
1827
|
+
switch (template) {
|
|
1828
|
+
case 'Mobile App Starter':
|
|
1829
|
+
deps['react-native'] = '^0.72.0';
|
|
1830
|
+
deps['react-native-navigation'] = '^7.0.0';
|
|
1831
|
+
break;
|
|
1832
|
+
case 'Admin Dashboard':
|
|
1833
|
+
deps['recharts'] = '^2.8.0';
|
|
1834
|
+
deps['lucide-react'] = '^0.263.0';
|
|
1835
|
+
break;
|
|
1836
|
+
case 'Enterprise App Template':
|
|
1837
|
+
deps['lodash'] = '^4.17.21';
|
|
1838
|
+
deps['date-fns'] = '^2.30.0';
|
|
1839
|
+
deps['react-hook-form'] = '^7.45.0';
|
|
1840
|
+
break;
|
|
1841
|
+
case 'SaaS Template':
|
|
1842
|
+
deps['stripe'] = '^12.10.0';
|
|
1843
|
+
deps['date-fns'] = '^2.30.0';
|
|
1844
|
+
deps['react-hook-form'] = '^7.45.0';
|
|
1845
|
+
break;
|
|
1846
|
+
case 'Authentication Starter':
|
|
1847
|
+
deps['jsonwebtoken'] = '^9.0.2';
|
|
1848
|
+
deps['bcryptjs'] = '^2.4.3';
|
|
1849
|
+
break;
|
|
1850
|
+
}
|
|
1851
|
+
return deps;
|
|
1852
|
+
}
|
|
1853
|
+
/**
|
|
1854
|
+
* Configure UI System files and configurations
|
|
1855
|
+
*/
|
|
1856
|
+
async function configureUISystem(projectDir, uiSystem) {
|
|
1857
|
+
switch (uiSystem) {
|
|
1858
|
+
case 'Tailwind CSS':
|
|
1859
|
+
await configureTailwind(projectDir);
|
|
1860
|
+
break;
|
|
1861
|
+
case 'Bootstrap 5':
|
|
1862
|
+
await configureBootstrap(projectDir);
|
|
1863
|
+
break;
|
|
1864
|
+
case 'JokoUI':
|
|
1865
|
+
await configureJokoUI(projectDir);
|
|
1866
|
+
break;
|
|
1867
|
+
case 'DaisyUI':
|
|
1868
|
+
await configureDaisyUI(projectDir);
|
|
1869
|
+
break;
|
|
1870
|
+
case 'Mantine':
|
|
1871
|
+
await configureMantine(projectDir);
|
|
1872
|
+
break;
|
|
1873
|
+
case 'Material UI':
|
|
1874
|
+
await configureMaterialUI(projectDir);
|
|
1875
|
+
break;
|
|
1876
|
+
case 'Ant Design':
|
|
1877
|
+
await configureAntDesign(projectDir);
|
|
1878
|
+
break;
|
|
1879
|
+
case 'Chakra UI':
|
|
1880
|
+
await configureChakraUI(projectDir);
|
|
1881
|
+
break;
|
|
1882
|
+
case 'Shadcn/ui':
|
|
1883
|
+
await configureShadcn(projectDir);
|
|
1884
|
+
break;
|
|
1885
|
+
}
|
|
1886
|
+
}
|
|
1887
|
+
/**
|
|
1888
|
+
* Configure Tailwind CSS
|
|
1889
|
+
*/
|
|
1890
|
+
async function configureTailwind(projectDir) {
|
|
1891
|
+
// Create tailwind.config.js
|
|
1892
|
+
await fs_extra_1.default.writeFile(path_1.default.join(projectDir, 'tailwind.config.js'), `/** @type {import('tailwindcss').Config} */
|
|
1893
|
+
export default {
|
|
1894
|
+
content: [
|
|
1895
|
+
"./index.html",
|
|
1896
|
+
"./src/**/*.{js,ts,jsx,tsx}",
|
|
1897
|
+
"./resources/**/*.{js,ts,jsx,tsx}",
|
|
1898
|
+
],
|
|
1899
|
+
theme: {
|
|
1900
|
+
extend: {},
|
|
1901
|
+
},
|
|
1902
|
+
plugins: [],
|
|
1903
|
+
}
|
|
1904
|
+
`);
|
|
1905
|
+
// Create postcss.config.js
|
|
1906
|
+
await fs_extra_1.default.writeFile(path_1.default.join(projectDir, 'postcss.config.js'), `export default {
|
|
1907
|
+
plugins: {
|
|
1908
|
+
tailwindcss: {},
|
|
1909
|
+
autoprefixer: {},
|
|
1910
|
+
},
|
|
1911
|
+
}
|
|
1912
|
+
`);
|
|
1913
|
+
// Create tailwind.css
|
|
1914
|
+
await fs_extra_1.default.ensureDir(path_1.default.join(projectDir, 'resources/css'));
|
|
1915
|
+
await fs_extra_1.default.writeFile(path_1.default.join(projectDir, 'resources/css/tailwind.css'), `@tailwind base;
|
|
1916
|
+
@tailwind components;
|
|
1917
|
+
@tailwind utilities;
|
|
1918
|
+
`);
|
|
1919
|
+
// Create example component with Tailwind
|
|
1920
|
+
await fs_extra_1.default.writeFile(path_1.default.join(projectDir, 'resources/views/WelcomeComponent.tsx'), `import React from 'react';
|
|
1921
|
+
|
|
1922
|
+
export default function WelcomeComponent() {
|
|
1923
|
+
return (
|
|
1924
|
+
<div className="flex items-center justify-center min-h-screen bg-gradient-to-r from-blue-500 to-purple-600">
|
|
1925
|
+
<div className="text-center">
|
|
1926
|
+
<h1 className="text-5xl font-bold text-white mb-4">Welcome to EreactThohir</h1>
|
|
1927
|
+
<p className="text-xl text-gray-100 mb-8">With Tailwind CSS</p>
|
|
1928
|
+
<button className="px-8 py-3 bg-white text-blue-600 font-semibold rounded-lg hover:bg-gray-100 transition">
|
|
1929
|
+
Get Started
|
|
1930
|
+
</button>
|
|
1931
|
+
</div>
|
|
1932
|
+
</div>
|
|
1933
|
+
);
|
|
1934
|
+
}
|
|
1935
|
+
`);
|
|
1936
|
+
}
|
|
1937
|
+
/**
|
|
1938
|
+
* Configure Bootstrap 5
|
|
1939
|
+
*/
|
|
1940
|
+
async function configureBootstrap(projectDir) {
|
|
1941
|
+
// Create example component with Bootstrap
|
|
1942
|
+
await fs_extra_1.default.writeFile(path_1.default.join(projectDir, 'resources/views/WelcomeComponent.tsx'), `import React from 'react';
|
|
1943
|
+
import { Container, Row, Col, Button } from 'react-bootstrap';
|
|
1944
|
+
import 'bootstrap/dist/css/bootstrap.min.css';
|
|
1945
|
+
|
|
1946
|
+
export default function WelcomeComponent() {
|
|
1947
|
+
return (
|
|
1948
|
+
<Container className="d-flex align-items-center justify-content-center" style={{ minHeight: '100vh' }}>
|
|
1949
|
+
<Row className="text-center">
|
|
1950
|
+
<Col>
|
|
1951
|
+
<h1 className="display-4 fw-bold mb-4">Welcome to EreactThohir</h1>
|
|
1952
|
+
<p className="lead mb-4">With Bootstrap 5</p>
|
|
1953
|
+
<Button variant="primary" size="lg">Get Started</Button>
|
|
1954
|
+
</Col>
|
|
1955
|
+
</Row>
|
|
1956
|
+
</Container>
|
|
1957
|
+
);
|
|
1958
|
+
}
|
|
1959
|
+
`);
|
|
1960
|
+
}
|
|
1961
|
+
/**
|
|
1962
|
+
* Configure Material UI
|
|
1963
|
+
*/
|
|
1964
|
+
async function configureMaterialUI(projectDir) {
|
|
1965
|
+
// Create example component with Material UI
|
|
1966
|
+
await fs_extra_1.default.writeFile(path_1.default.join(projectDir, 'resources/views/WelcomeComponent.tsx'), `import React from 'react';
|
|
1967
|
+
import { Box, Container, Typography, Button, Stack } from '@mui/material';
|
|
1968
|
+
|
|
1969
|
+
export default function WelcomeComponent() {
|
|
1970
|
+
return (
|
|
1971
|
+
<Container maxWidth="md">
|
|
1972
|
+
<Box
|
|
1973
|
+
sx={{
|
|
1974
|
+
display: 'flex',
|
|
1975
|
+
alignItems: 'center',
|
|
1976
|
+
justifyContent: 'center',
|
|
1977
|
+
minHeight: '100vh',
|
|
1978
|
+
textAlign: 'center',
|
|
1979
|
+
}}
|
|
1980
|
+
>
|
|
1981
|
+
<Stack spacing={3}>
|
|
1982
|
+
<Typography variant="h1" component="h1">
|
|
1983
|
+
Welcome to EreactThohir
|
|
1984
|
+
</Typography>
|
|
1985
|
+
<Typography variant="h5" color="textSecondary">
|
|
1986
|
+
With Material UI
|
|
1987
|
+
</Typography>
|
|
1988
|
+
<Button variant="contained" size="large">
|
|
1989
|
+
Get Started
|
|
1990
|
+
</Button>
|
|
1991
|
+
</Stack>
|
|
1992
|
+
</Box>
|
|
1993
|
+
</Container>
|
|
1994
|
+
);
|
|
1995
|
+
}
|
|
1996
|
+
`);
|
|
1997
|
+
}
|
|
1998
|
+
/**
|
|
1999
|
+
* Configure Ant Design
|
|
2000
|
+
*/
|
|
2001
|
+
async function configureAntDesign(projectDir) {
|
|
2002
|
+
// Create example component with Ant Design
|
|
2003
|
+
await fs_extra_1.default.writeFile(path_1.default.join(projectDir, 'resources/views/WelcomeComponent.tsx'), `import React from 'react';
|
|
2004
|
+
import { Layout, Button, Space, Typography } from 'antd';
|
|
2005
|
+
import 'antd/dist/reset.css';
|
|
2006
|
+
|
|
2007
|
+
const { Content } = Layout;
|
|
2008
|
+
const { Title, Text } = Typography;
|
|
2009
|
+
|
|
2010
|
+
export default function WelcomeComponent() {
|
|
2011
|
+
return (
|
|
2012
|
+
<Layout style={{ minHeight: '100vh' }}>
|
|
2013
|
+
<Content style={{ display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
|
2014
|
+
<Space direction="vertical" align="center" style={{ textAlign: 'center' }}>
|
|
2015
|
+
<Title level={1}>Welcome to EreactThohir</Title>
|
|
2016
|
+
<Text type="secondary" style={{ fontSize: '18px' }}>
|
|
2017
|
+
With Ant Design
|
|
2018
|
+
</Text>
|
|
2019
|
+
<Button type="primary" size="large">
|
|
2020
|
+
Get Started
|
|
2021
|
+
</Button>
|
|
2022
|
+
</Space>
|
|
2023
|
+
</Content>
|
|
2024
|
+
</Layout>
|
|
2025
|
+
);
|
|
2026
|
+
}
|
|
2027
|
+
`);
|
|
2028
|
+
}
|
|
2029
|
+
/**
|
|
2030
|
+
* Configure Chakra UI
|
|
2031
|
+
*/
|
|
2032
|
+
async function configureChakraUI(projectDir) {
|
|
2033
|
+
// Create example component with Chakra UI
|
|
2034
|
+
await fs_extra_1.default.writeFile(path_1.default.join(projectDir, 'resources/views/WelcomeComponent.tsx'), `import React from 'react';
|
|
2035
|
+
import { Box, Container, Heading, Text, Button, VStack, Center } from '@chakra-ui/react';
|
|
2036
|
+
|
|
2037
|
+
export default function WelcomeComponent() {
|
|
2038
|
+
return (
|
|
2039
|
+
<Center minH="100vh">
|
|
2040
|
+
<Container maxW="md">
|
|
2041
|
+
<VStack spacing={8} textAlign="center">
|
|
2042
|
+
<Heading as="h1" size="2xl">
|
|
2043
|
+
Welcome to EreactThohir
|
|
2044
|
+
</Heading>
|
|
2045
|
+
<Text fontSize="lg" color="gray.600">
|
|
2046
|
+
With Chakra UI
|
|
2047
|
+
</Text>
|
|
2048
|
+
<Button colorScheme="blue" size="lg">
|
|
2049
|
+
Get Started
|
|
2050
|
+
</Button>
|
|
2051
|
+
</VStack>
|
|
2052
|
+
</Container>
|
|
2053
|
+
</Center>
|
|
2054
|
+
);
|
|
2055
|
+
}
|
|
2056
|
+
`);
|
|
2057
|
+
}
|
|
2058
|
+
/**
|
|
2059
|
+
* Configure Shadcn/ui
|
|
2060
|
+
*/
|
|
2061
|
+
async function configureShadcn(projectDir) {
|
|
2062
|
+
// Create shadcn.json config
|
|
2063
|
+
await fs_extra_1.default.writeJSON(path_1.default.join(projectDir, 'components.json'), {
|
|
2064
|
+
"$schema": "https://ui.shadcn.com/schema.json",
|
|
2065
|
+
"style": "default",
|
|
2066
|
+
"rsc": true,
|
|
2067
|
+
"tsx": true,
|
|
2068
|
+
"tailwind": {
|
|
2069
|
+
"config": "tailwind.config.js",
|
|
2070
|
+
"css": "resources/css/globals.css",
|
|
2071
|
+
"baseColor": "slate"
|
|
2072
|
+
},
|
|
2073
|
+
"aliases": {
|
|
2074
|
+
"@/components": "./resources/components",
|
|
2075
|
+
"@/lib/utils": "./resources/lib/utils"
|
|
2076
|
+
}
|
|
2077
|
+
}, { spaces: 2 });
|
|
2078
|
+
// Create tailwind config for Shadcn
|
|
2079
|
+
await fs_extra_1.default.writeFile(path_1.default.join(projectDir, 'tailwind.config.js'), `/** @type {import('tailwindcss').Config} */
|
|
2080
|
+
export default {
|
|
2081
|
+
content: [
|
|
2082
|
+
"./index.html",
|
|
2083
|
+
"./src/**/*.{js,ts,jsx,tsx}",
|
|
2084
|
+
"./resources/**/*.{js,ts,jsx,tsx}",
|
|
2085
|
+
],
|
|
2086
|
+
theme: {
|
|
2087
|
+
extend: {
|
|
2088
|
+
colors: {
|
|
2089
|
+
border: "hsl(var(--border))",
|
|
2090
|
+
input: "hsl(var(--input))",
|
|
2091
|
+
ring: "hsl(var(--ring))",
|
|
2092
|
+
background: "hsl(var(--background))",
|
|
2093
|
+
foreground: "hsl(var(--foreground))",
|
|
2094
|
+
primary: {
|
|
2095
|
+
DEFAULT: "hsl(var(--primary))",
|
|
2096
|
+
foreground: "hsl(var(--primary-foreground))",
|
|
2097
|
+
},
|
|
2098
|
+
secondary: {
|
|
2099
|
+
DEFAULT: "hsl(var(--secondary))",
|
|
2100
|
+
foreground: "hsl(var(--secondary-foreground))",
|
|
2101
|
+
},
|
|
2102
|
+
},
|
|
2103
|
+
},
|
|
2104
|
+
},
|
|
2105
|
+
plugins: [require("tailwindcss-animate")],
|
|
2106
|
+
}
|
|
2107
|
+
`);
|
|
2108
|
+
// Create globals.css
|
|
2109
|
+
await fs_extra_1.default.ensureDir(path_1.default.join(projectDir, 'resources/css'));
|
|
2110
|
+
await fs_extra_1.default.writeFile(path_1.default.join(projectDir, 'resources/css/globals.css'), `@tailwind base;
|
|
2111
|
+
@tailwind components;
|
|
2112
|
+
@tailwind utilities;
|
|
2113
|
+
|
|
2114
|
+
@layer base {
|
|
2115
|
+
:root {
|
|
2116
|
+
--background: 0 0% 100%;
|
|
2117
|
+
--foreground: 0 0% 3.6%;
|
|
2118
|
+
--card: 0 0% 100%;
|
|
2119
|
+
--card-foreground: 0 0% 3.6%;
|
|
2120
|
+
--popover: 0 0% 100%;
|
|
2121
|
+
--popover-foreground: 0 0% 3.6%;
|
|
2122
|
+
--muted: 0 0% 96.1%;
|
|
2123
|
+
--muted-foreground: 0 0% 45.1%;
|
|
2124
|
+
--accent: 0 0% 9.0%;
|
|
2125
|
+
--accent-foreground: 0 0% 100%;
|
|
2126
|
+
--destructive: 0 84.2% 60.2%;
|
|
2127
|
+
--destructive-foreground: 0 0% 100%;
|
|
2128
|
+
--border: 0 0% 89.8%;
|
|
2129
|
+
--input: 0 0% 89.8%;
|
|
2130
|
+
--primary: 0 0% 9.0%;
|
|
2131
|
+
--primary-foreground: 0 0% 100%;
|
|
2132
|
+
--secondary: 0 0% 96.1%;
|
|
2133
|
+
--secondary-foreground: 0 0% 9.0%;
|
|
2134
|
+
--ring: 0 0% 3.6%;
|
|
2135
|
+
}
|
|
2136
|
+
}
|
|
2137
|
+
`);
|
|
2138
|
+
// Create example component
|
|
2139
|
+
await fs_extra_1.default.writeFile(path_1.default.join(projectDir, 'resources/views/WelcomeComponent.tsx'), `import React from 'react';
|
|
2140
|
+
import { Button } from '@/components/ui/button';
|
|
2141
|
+
|
|
2142
|
+
export default function WelcomeComponent() {
|
|
2143
|
+
return (
|
|
2144
|
+
<div className="flex items-center justify-center min-h-screen">
|
|
2145
|
+
<div className="text-center">
|
|
2146
|
+
<h1 className="text-5xl font-bold mb-4">Welcome to EreactThohir</h1>
|
|
2147
|
+
<p className="text-xl text-muted-foreground mb-8">With Shadcn/ui</p>
|
|
2148
|
+
<Button size="lg">Get Started</Button>
|
|
2149
|
+
</div>
|
|
2150
|
+
</div>
|
|
2151
|
+
);
|
|
2152
|
+
}
|
|
2153
|
+
`);
|
|
2154
|
+
}
|
|
2155
|
+
/**
|
|
2156
|
+
* Configure JokoUI
|
|
2157
|
+
*/
|
|
2158
|
+
async function configureJokoUI(projectDir) {
|
|
2159
|
+
// Create jokoui.config.js
|
|
2160
|
+
await fs_extra_1.default.writeFile(path_1.default.join(projectDir, 'jokoui.config.js'), `export default {
|
|
2161
|
+
theme: {
|
|
2162
|
+
primary: '#3b82f6',
|
|
2163
|
+
secondary: '#10b981',
|
|
2164
|
+
},
|
|
2165
|
+
components: {
|
|
2166
|
+
button: {
|
|
2167
|
+
borderRadius: '0.5rem',
|
|
2168
|
+
}
|
|
2169
|
+
}
|
|
2170
|
+
}
|
|
2171
|
+
`);
|
|
2172
|
+
// Create example component
|
|
2173
|
+
await fs_extra_1.default.writeFile(path_1.default.join(projectDir, 'resources/views/WelcomeComponent.tsx'), `import React from 'react';
|
|
2174
|
+
import { Button, Card } from 'jokoui';
|
|
2175
|
+
|
|
2176
|
+
export default function WelcomeComponent() {
|
|
2177
|
+
return (
|
|
2178
|
+
<div style= {{ display: 'flex', justifyContent: 'center', alignItems: 'center', minHeight: '100vh', backgroundColor: '#f3f4f6' }
|
|
2179
|
+
}>
|
|
2180
|
+
<Card style={ { padding: '2rem', textAlign: 'center', maxWidth: '500px' } }>
|
|
2181
|
+
<h1 style={ { fontSize: '2.5rem', fontWeight: 'bold', marginBottom: '1rem' } }> Welcome to EreactThohir </h1>
|
|
2182
|
+
< p style = {{ color: '#666', marginBottom: '2rem' }
|
|
2183
|
+
}> With JokoUI </p>
|
|
2184
|
+
< Button variant = "primary" size = "lg" > Get Started </Button>
|
|
2185
|
+
</Card>
|
|
2186
|
+
</div>
|
|
2187
|
+
);
|
|
2188
|
+
}
|
|
2189
|
+
`);
|
|
2190
|
+
}
|
|
2191
|
+
/**
|
|
2192
|
+
* Configure DaisyUI
|
|
2193
|
+
*/
|
|
2194
|
+
async function configureDaisyUI(projectDir) {
|
|
2195
|
+
// Configure Tailwind + DaisyUI
|
|
2196
|
+
await fs_extra_1.default.writeFile(path_1.default.join(projectDir, 'tailwind.config.js'), `/** @type {import('tailwindcss').Config} */
|
|
2197
|
+
export default {
|
|
2198
|
+
content: [
|
|
2199
|
+
"./index.html",
|
|
2200
|
+
"./src/**/*.{js,ts,jsx,tsx}",
|
|
2201
|
+
"./resources/**/*.{js,ts,jsx,tsx}",
|
|
2202
|
+
],
|
|
2203
|
+
theme: {
|
|
2204
|
+
extend: {},
|
|
2205
|
+
},
|
|
2206
|
+
plugins: [require("daisyui")],
|
|
2207
|
+
daisyui: {
|
|
2208
|
+
themes: ["light", "dark", "cupcake"],
|
|
2209
|
+
},
|
|
2210
|
+
}
|
|
2211
|
+
`);
|
|
2212
|
+
// Create postcss.config.js
|
|
2213
|
+
await fs_extra_1.default.writeFile(path_1.default.join(projectDir, 'postcss.config.js'), `export default {
|
|
2214
|
+
plugins: {
|
|
2215
|
+
tailwindcss: {},
|
|
2216
|
+
autoprefixer: {},
|
|
2217
|
+
},
|
|
2218
|
+
}
|
|
2219
|
+
`);
|
|
2220
|
+
// Create tailwind.css
|
|
2221
|
+
await fs_extra_1.default.ensureDir(path_1.default.join(projectDir, 'resources/css'));
|
|
2222
|
+
await fs_extra_1.default.writeFile(path_1.default.join(projectDir, 'resources/css/tailwind.css'), `@tailwind base;
|
|
2223
|
+
@tailwind components;
|
|
2224
|
+
@tailwind utilities;
|
|
2225
|
+
`);
|
|
2226
|
+
// Create example component
|
|
2227
|
+
await fs_extra_1.default.writeFile(path_1.default.join(projectDir, 'resources/views/WelcomeComponent.tsx'), `import React from 'react';
|
|
2228
|
+
|
|
2229
|
+
export default function WelcomeComponent() {
|
|
2230
|
+
return (
|
|
2231
|
+
<div className= "hero min-h-screen bg-base-200" >
|
|
2232
|
+
<div className="hero-content text-center" >
|
|
2233
|
+
<div className="max-w-md" >
|
|
2234
|
+
<h1 className="text-5xl font-bold" > Welcome to EreactThohir </h1>
|
|
2235
|
+
< p className = "py-6" > With DaisyUI + Tailwind CSS </p>
|
|
2236
|
+
< button className = "btn btn-primary" > Get Started </button>
|
|
2237
|
+
< button className = "btn btn-secondary ml-2" > Documentation </button>
|
|
2238
|
+
</div>
|
|
2239
|
+
</div>
|
|
2240
|
+
</div>
|
|
2241
|
+
);
|
|
2242
|
+
}
|
|
2243
|
+
`);
|
|
2244
|
+
}
|
|
2245
|
+
/**
|
|
2246
|
+
* Configure Mantine
|
|
2247
|
+
*/
|
|
2248
|
+
async function configureMantine(projectDir) {
|
|
2249
|
+
// Create postcss.config.js for Mantine
|
|
2250
|
+
await fs_extra_1.default.writeFile(path_1.default.join(projectDir, 'postcss.config.cjs'), `module.exports = {
|
|
2251
|
+
plugins: {
|
|
2252
|
+
'postcss-preset-mantine': {},
|
|
2253
|
+
'postcss-simple-vars': {
|
|
2254
|
+
variables: {
|
|
2255
|
+
'mantine-breakpoint-xs': '36em',
|
|
2256
|
+
'mantine-breakpoint-sm': '48em',
|
|
2257
|
+
'mantine-breakpoint-md': '62em',
|
|
2258
|
+
'mantine-breakpoint-lg': '75em',
|
|
2259
|
+
'mantine-breakpoint-xl': '88em',
|
|
2260
|
+
},
|
|
2261
|
+
},
|
|
2262
|
+
},
|
|
2263
|
+
};
|
|
2264
|
+
`);
|
|
2265
|
+
// Create Example Component
|
|
2266
|
+
await fs_extra_1.default.writeFile(path_1.default.join(projectDir, 'resources/views/WelcomeComponent.tsx'), `import React from 'react';
|
|
2267
|
+
import { Container, Title, Text, Button, Group } from '@mantine/core';
|
|
2268
|
+
|
|
2269
|
+
export default function WelcomeComponent() {
|
|
2270
|
+
return (
|
|
2271
|
+
<Container size= "md" h = "100vh" style = {{ display: 'flex', flexDirection: 'column', justifyContent: 'center', alignItems: 'center' }
|
|
2272
|
+
}>
|
|
2273
|
+
<Title order={ 1 } mb = "md" > Welcome to EreactThohir </Title>
|
|
2274
|
+
< Text size = "lg" c = "dimmed" mb = "xl" > With Mantine UI </Text>
|
|
2275
|
+
|
|
2276
|
+
< Group >
|
|
2277
|
+
<Button size="lg" variant = "filled" color = "blue" > Get Started </Button>
|
|
2278
|
+
< Button size = "lg" variant = "light" color = "blue" > Learn More </Button>
|
|
2279
|
+
</Group>
|
|
2280
|
+
</Container>
|
|
2281
|
+
);
|
|
2282
|
+
}
|
|
2283
|
+
`);
|
|
2284
|
+
// Note: User needs to wrap App with MantineProvider, but we can't easily edit App.tsx/index.tsx here without parsing it.
|
|
2285
|
+
// We assume the user or the template generator handles the provider wrapping if possible,
|
|
2286
|
+
// or we could overwrite the main entry point if we knew where it was.
|
|
2287
|
+
// For now, we'll just provide the component.
|
|
2288
|
+
}
|
|
584
2289
|
//# sourceMappingURL=create.js.map
|