ntc-dev-kit 1.0.0__py3-none-any.whl
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.
- ntc/__init__.py +5 -0
- ntc/app_creator.py +296 -0
- ntc/cli.py +4 -0
- ntc/cli_enhanced.py +820 -0
- ntc/composer/__init__.py +0 -0
- ntc/composer/main.py +443 -0
- ntc/deployment/__init__.py +0 -0
- ntc/templates/__init__.py +2 -0
- ntc/templates/auth_app/__init__.py +0 -0
- ntc/templates/auth_app/admin.py +24 -0
- ntc/templates/auth_app/apps.py +6 -0
- ntc/templates/auth_app/models.py +30 -0
- ntc/templates/auth_app/serializers.py +101 -0
- ntc/templates/auth_app/urls.py +19 -0
- ntc/templates/auth_app/views.py +175 -0
- ntc/templates/drf_structure/__init__.py +1 -0
- ntc/templates/drf_structure/drf_structure.md +83 -0
- ntc/templates/drf_structure/main.py +389 -0
- ntc/templates/drf_structure/structure.json +126 -0
- ntc/templates/drf_structure/templates/.entry/start_core_backend.sh +37 -0
- ntc/templates/drf_structure/templates/.entry/start_tasks_backend.sh +30 -0
- ntc/templates/drf_structure/templates/CLAUDE.md +194 -0
- ntc/templates/drf_structure/templates/README.md +117 -0
- ntc/templates/drf_structure/templates/apps/app_admin.py +3 -0
- ntc/templates/drf_structure/templates/apps/app_apps.py +6 -0
- ntc/templates/drf_structure/templates/apps/app_models.py +4 -0
- ntc/templates/drf_structure/templates/apps/app_urls.py +8 -0
- ntc/templates/drf_structure/templates/apps/app_views.py +6 -0
- ntc/templates/drf_structure/templates/apps/common_apps.py +6 -0
- ntc/templates/drf_structure/templates/apps/common_models.py +8 -0
- ntc/templates/drf_structure/templates/config/gunicorn_config.py +9 -0
- ntc/templates/drf_structure/templates/config/http.conf +33 -0
- ntc/templates/drf_structure/templates/core/base_model.py +9 -0
- ntc/templates/drf_structure/templates/core/formatter.py +5 -0
- ntc/templates/drf_structure/templates/core/logger.py +8 -0
- ntc/templates/drf_structure/templates/core/pagination.py +6 -0
- ntc/templates/drf_structure/templates/core/response_wrapper.py +7 -0
- ntc/templates/drf_structure/templates/core/services/azure/email/backend.py +108 -0
- ntc/templates/drf_structure/templates/core/services/azure/email/client.py +296 -0
- ntc/templates/drf_structure/templates/core/services/azure/email/notifier.py +102 -0
- ntc/templates/drf_structure/templates/core/services/object_storage.py +285 -0
- ntc/templates/drf_structure/templates/django_project/asgi.py +5 -0
- ntc/templates/drf_structure/templates/django_project/celery.py +8 -0
- ntc/templates/drf_structure/templates/django_project/dev_settings.py +2 -0
- ntc/templates/drf_structure/templates/django_project/django_settings.txt +322 -0
- ntc/templates/drf_structure/templates/django_project/management_command.py +114 -0
- ntc/templates/drf_structure/templates/django_project/prod_settings.py +3 -0
- ntc/templates/drf_structure/templates/django_project/schema.py +7 -0
- ntc/templates/drf_structure/templates/django_project/urls.py +6 -0
- ntc/templates/drf_structure/templates/django_project/wsgi.py +5 -0
- ntc/templates/drf_structure/templates/docker/docker-compose.yml +11 -0
- ntc/templates/drf_structure/templates/knowledge/knowledge_description.md +201 -0
- ntc/templates/drf_structure/templates/knowledge/knowledge_design_pattern.md +722 -0
- ntc/templates/drf_structure/templates/knowledge/knowledge_structure.md +151 -0
- ntc/templates/drf_structure/templates/root/README.md +307 -0
- ntc/templates/drf_structure/templates/root/django_env.txt +98 -0
- ntc/templates/drf_structure/templates/root/django_requirements.txt +24 -0
- ntc/templates/drf_structure/templates/root/gitignore.txt +6 -0
- ntc/templates/drf_structure/templates/root/manage.py +11 -0
- ntc/templates/flutter/.gitignore +44 -0
- ntc/templates/flutter/.knowledge/knowledge_description.md +215 -0
- ntc/templates/flutter/.knowledge/knowledge_design_pattern.md +310 -0
- ntc/templates/flutter/.knowledge/knowledge_structure.md +83 -0
- ntc/templates/flutter/CLAUDE.md +190 -0
- ntc/templates/flutter/__init__.py +0 -0
- ntc/templates/flutter/analysis_options.yaml +9 -0
- ntc/templates/flutter/main.py +187 -0
- ntc/templates/flutter/pubspec.yaml +65 -0
- ntc/templates/flutter/structure.json +51 -0
- ntc/templates/nextjs/.env.example +10 -0
- ntc/templates/nextjs/.gitignore +36 -0
- ntc/templates/nextjs/CLAUDE.md +181 -0
- ntc/templates/nextjs/README.md +94 -0
- ntc/templates/nextjs/__init__.py +0 -0
- ntc/templates/nextjs/knowledge/knowledge_description.md +208 -0
- ntc/templates/nextjs/knowledge/knowledge_design_pattern.md +460 -0
- ntc/templates/nextjs/knowledge/knowledge_structure.md +75 -0
- ntc/templates/nextjs/main.py +224 -0
- ntc/templates/nextjs/package.json +25 -0
- ntc/templates/nextjs/src/app/globals.css +26 -0
- ntc/templates/nextjs/src/app/layout.tsx +59 -0
- ntc/templates/nextjs/src/app/page.tsx +21 -0
- ntc/templates/nextjs/src/components/Header.tsx +16 -0
- ntc/templates/nextjs/src/services/api.ts +81 -0
- ntc/templates/nextjs/src/utils/constants.ts +1 -0
- ntc/templates/nextjs/structure.json +32 -0
- ntc/templates/nextjs/tsconfig.json +27 -0
- ntc/templates/react_admin/.env.example +1 -0
- ntc/templates/react_admin/.gitignore +10 -0
- ntc/templates/react_admin/CLAUDE.md +210 -0
- ntc/templates/react_admin/README.md +72 -0
- ntc/templates/react_admin/__init__.py +0 -0
- ntc/templates/react_admin/index.html +13 -0
- ntc/templates/react_admin/knowledge/knowledge_description.md +202 -0
- ntc/templates/react_admin/knowledge/knowledge_design_pattern.md +448 -0
- ntc/templates/react_admin/knowledge/knowledge_structure.md +97 -0
- ntc/templates/react_admin/main.py +208 -0
- ntc/templates/react_admin/package.json +35 -0
- ntc/templates/react_admin/src/App.tsx +60 -0
- ntc/templates/react_admin/src/components/ProtectedRoute.tsx +16 -0
- ntc/templates/react_admin/src/components/Sidebar.tsx +90 -0
- ntc/templates/react_admin/src/components/TopNav.tsx +44 -0
- ntc/templates/react_admin/src/context/AuthContext.tsx +107 -0
- ntc/templates/react_admin/src/context/ThemeContext.tsx +34 -0
- ntc/templates/react_admin/src/context/WorkspaceContext.tsx +28 -0
- ntc/templates/react_admin/src/hooks/useApi.ts +39 -0
- ntc/templates/react_admin/src/index.css +34 -0
- ntc/templates/react_admin/src/layouts/AuthLayout.tsx +23 -0
- ntc/templates/react_admin/src/layouts/DashboardLayout.tsx +24 -0
- ntc/templates/react_admin/src/main.tsx +5 -0
- ntc/templates/react_admin/src/pages/DashboardPage.tsx +10 -0
- ntc/templates/react_admin/src/pages/auth/LoginPage.tsx +79 -0
- ntc/templates/react_admin/src/pages/auth/RegisterPage.tsx +93 -0
- ntc/templates/react_admin/src/services/api.service.ts +44 -0
- ntc/templates/react_admin/src/services/base.service.ts +193 -0
- ntc/templates/react_admin/structure.json +52 -0
- ntc/templates/react_admin/tsconfig.app.json +25 -0
- ntc/templates/react_admin/tsconfig.json +7 -0
- ntc/templates/react_admin/tsconfig.node.json +19 -0
- ntc/templates/react_admin/vite.config.ts +13 -0
- ntc/templates/react_structure/CLAUDE.md +176 -0
- ntc/templates/react_structure/__init__.py +1 -0
- ntc/templates/react_structure/index.css +21 -0
- ntc/templates/react_structure/index.html +13 -0
- ntc/templates/react_structure/knowledge/knowledge_description.md +170 -0
- ntc/templates/react_structure/knowledge/knowledge_design_pattern.md +411 -0
- ntc/templates/react_structure/knowledge/knowledge_structure.md +74 -0
- ntc/templates/react_structure/main.py +217 -0
- ntc/templates/react_structure/package.json +32 -0
- ntc/templates/react_structure/src/App.tsx +63 -0
- ntc/templates/react_structure/src/components/Header.tsx +51 -0
- ntc/templates/react_structure/src/main.tsx +5 -0
- ntc/templates/react_structure/src/services/api.ts +90 -0
- ntc/templates/react_structure/src/utils/constants.ts +1 -0
- ntc/templates/react_structure/structure.json +65 -0
- ntc/templates/react_structure/tsconfig.app.json +25 -0
- ntc/templates/react_structure/tsconfig.json +7 -0
- ntc/templates/react_structure/tsconfig.node.json +19 -0
- ntc/templates/react_structure/vite.config.ts +12 -0
- ntc/utils/__init__.py +1 -0
- ntc/utils/environment.py +67 -0
- ntc/utils/output.py +41 -0
- ntc_dev_kit-1.0.0.dist-info/METADATA +188 -0
- ntc_dev_kit-1.0.0.dist-info/RECORD +148 -0
- ntc_dev_kit-1.0.0.dist-info/WHEEL +5 -0
- ntc_dev_kit-1.0.0.dist-info/entry_points.txt +2 -0
- ntc_dev_kit-1.0.0.dist-info/licenses/LICENSE +21 -0
- ntc_dev_kit-1.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import subprocess
|
|
3
|
+
import sys
|
|
4
|
+
import json
|
|
5
|
+
import shutil
|
|
6
|
+
from colorama import Fore, Style
|
|
7
|
+
from ntc.utils.output import print_success, print_error, print_warning, print_info
|
|
8
|
+
|
|
9
|
+
def replace_variables(content, variables):
|
|
10
|
+
"""Replace placeholders with actual variables."""
|
|
11
|
+
for key, value in variables.items():
|
|
12
|
+
content = content.replace(f"{{{key}}}", value)
|
|
13
|
+
return content
|
|
14
|
+
|
|
15
|
+
def get_template_content(template_name, variables):
|
|
16
|
+
"""Load template content and replace variables."""
|
|
17
|
+
if template_name.startswith("react_admin/"):
|
|
18
|
+
template_name = template_name[len("react_admin/"):]
|
|
19
|
+
|
|
20
|
+
template_path = os.path.join(os.path.dirname(__file__), template_name)
|
|
21
|
+
if not os.path.exists(template_path):
|
|
22
|
+
return ""
|
|
23
|
+
with open(template_path, "r", encoding="utf-8") as f:
|
|
24
|
+
content = f.read()
|
|
25
|
+
return replace_variables(content, variables)
|
|
26
|
+
|
|
27
|
+
def create_from_json(base_path, structure, variables):
|
|
28
|
+
"""Recursively create directories and files from JSON."""
|
|
29
|
+
for name, content in structure.items():
|
|
30
|
+
name = replace_variables(name, variables)
|
|
31
|
+
full_path = os.path.join(base_path, name)
|
|
32
|
+
|
|
33
|
+
if isinstance(content, dict):
|
|
34
|
+
if "template" in content:
|
|
35
|
+
os.makedirs(os.path.dirname(full_path), exist_ok=True)
|
|
36
|
+
with open(full_path, "w", encoding="utf-8") as f:
|
|
37
|
+
f.write(get_template_content(content["template"], variables))
|
|
38
|
+
else:
|
|
39
|
+
os.makedirs(full_path, exist_ok=True)
|
|
40
|
+
create_from_json(full_path, content, variables)
|
|
41
|
+
else:
|
|
42
|
+
os.makedirs(os.path.dirname(full_path), exist_ok=True)
|
|
43
|
+
with open(full_path, "w", encoding="utf-8") as f:
|
|
44
|
+
f.write(replace_variables(content or "", variables))
|
|
45
|
+
|
|
46
|
+
def install_npm_dependencies(project_root):
|
|
47
|
+
"""Install npm dependencies for React admin project."""
|
|
48
|
+
print_info("Installing npm dependencies...")
|
|
49
|
+
|
|
50
|
+
if not shutil.which("npm"):
|
|
51
|
+
print_error("npm not found. Please install Node.js and npm first.")
|
|
52
|
+
print_info("Visit https://nodejs.org/ to download Node.js")
|
|
53
|
+
return False
|
|
54
|
+
|
|
55
|
+
try:
|
|
56
|
+
result = subprocess.run(
|
|
57
|
+
["npm", "install"],
|
|
58
|
+
cwd=project_root,
|
|
59
|
+
check=True,
|
|
60
|
+
capture_output=True,
|
|
61
|
+
text=True
|
|
62
|
+
)
|
|
63
|
+
print_success("npm dependencies installed successfully!")
|
|
64
|
+
return True
|
|
65
|
+
except subprocess.CalledProcessError as e:
|
|
66
|
+
print_error(f"Failed to install npm dependencies: {e}")
|
|
67
|
+
print_info("You can manually run 'npm install' in the project directory later.")
|
|
68
|
+
return False
|
|
69
|
+
|
|
70
|
+
def init_react_admin_project(base_path, project_name):
|
|
71
|
+
"""Initialize React admin dashboard project structure and setup environment."""
|
|
72
|
+
structure_file = os.path.join(os.path.dirname(__file__), "structure.json")
|
|
73
|
+
|
|
74
|
+
if not os.path.exists(structure_file):
|
|
75
|
+
print_error(f"Structure file not found: {structure_file}")
|
|
76
|
+
return
|
|
77
|
+
|
|
78
|
+
with open(structure_file, "r", encoding="utf-8") as f:
|
|
79
|
+
structure = json.load(f)
|
|
80
|
+
|
|
81
|
+
project_root = os.path.join(base_path, project_name)
|
|
82
|
+
os.makedirs(project_root, exist_ok=True)
|
|
83
|
+
|
|
84
|
+
variables = {
|
|
85
|
+
"project_name": project_name,
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
create_from_json(project_root, structure["{project_name}"], variables)
|
|
89
|
+
print_success(f"React admin dashboard '{project_name}' initialized at: {project_root}")
|
|
90
|
+
|
|
91
|
+
# Ask user for dependency installation
|
|
92
|
+
print(f"\n{Fore.CYAN}{Style.BRIGHT}📦 NODE.JS DEPENDENCY SETUP{Style.RESET_ALL}")
|
|
93
|
+
print("─" * 40)
|
|
94
|
+
print(f"{Fore.YELLOW}{Style.BRIGHT}[1]{Style.RESET_ALL} {Fore.WHITE}Install dependencies now (npm install){Style.RESET_ALL}")
|
|
95
|
+
print(f"{Fore.YELLOW}{Style.BRIGHT}[2]{Style.RESET_ALL} {Fore.WHITE}Use existing node_modules (if available){Style.RESET_ALL}")
|
|
96
|
+
print(f"{Fore.YELLOW}{Style.BRIGHT}[3]{Style.RESET_ALL} {Fore.WHITE}Skip dependency installation{Style.RESET_ALL}")
|
|
97
|
+
|
|
98
|
+
while True:
|
|
99
|
+
deps_choice = input(f"{Fore.WHITE}{Style.BRIGHT}❓ Select dependency option (1-3): {Style.RESET_ALL}").strip()
|
|
100
|
+
|
|
101
|
+
# Clear screen after selection
|
|
102
|
+
os.system('cls' if os.name == 'nt' else 'clear')
|
|
103
|
+
|
|
104
|
+
if deps_choice == "1":
|
|
105
|
+
install_npm_dependencies(project_root)
|
|
106
|
+
break
|
|
107
|
+
elif deps_choice == "2":
|
|
108
|
+
node_modules_path = os.path.join(project_root, "node_modules")
|
|
109
|
+
if os.path.exists(node_modules_path):
|
|
110
|
+
print_success("Found existing node_modules directory!")
|
|
111
|
+
print_info("Using existing dependencies. Run 'npm install' if you need to update them.")
|
|
112
|
+
else:
|
|
113
|
+
print_warning("No existing node_modules found in this location.")
|
|
114
|
+
print_info("You may need to run 'npm install' manually later.")
|
|
115
|
+
break
|
|
116
|
+
elif deps_choice == "3":
|
|
117
|
+
print_info("Skipping dependency installation.")
|
|
118
|
+
print_info("Remember to run 'npm install' before starting development.")
|
|
119
|
+
break
|
|
120
|
+
else:
|
|
121
|
+
print_error("Invalid choice. Please select 1, 2, or 3.")
|
|
122
|
+
|
|
123
|
+
# Show next steps
|
|
124
|
+
print(f"\n{Fore.GREEN}{Style.BRIGHT}🎉 PROJECT READY!{Style.RESET_ALL}")
|
|
125
|
+
print("─" * 30)
|
|
126
|
+
print_info("Next steps:")
|
|
127
|
+
print(f" 1. cd {project_name}")
|
|
128
|
+
if deps_choice != "1":
|
|
129
|
+
print(" 2. npm install")
|
|
130
|
+
print(" 3. npm run dev")
|
|
131
|
+
else:
|
|
132
|
+
print(" 2. npm run dev")
|
|
133
|
+
print_info("Your React admin dashboard will be available at http://localhost:5173")
|
|
134
|
+
print_info("Make sure your backend API is running on http://localhost:8000")
|
|
135
|
+
|
|
136
|
+
def init_react_admin_project_in_current_dir(project_name):
|
|
137
|
+
"""Initialize React admin dashboard project structure in the current directory."""
|
|
138
|
+
structure_file = os.path.join(os.path.dirname(__file__), "structure.json")
|
|
139
|
+
|
|
140
|
+
if not os.path.exists(structure_file):
|
|
141
|
+
print_error(f"Structure file not found: {structure_file}")
|
|
142
|
+
return
|
|
143
|
+
|
|
144
|
+
with open(structure_file, "r", encoding="utf-8") as f:
|
|
145
|
+
structure = json.load(f)
|
|
146
|
+
|
|
147
|
+
project_root = os.getcwd()
|
|
148
|
+
|
|
149
|
+
variables = {
|
|
150
|
+
"project_name": project_name,
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
existing_files = [f for f in os.listdir(project_root) if not f.startswith('.')]
|
|
154
|
+
if existing_files:
|
|
155
|
+
print_warning(f"Current directory is not empty. Found {len(existing_files)} files/folders.")
|
|
156
|
+
print_info("Existing items: " + ", ".join(existing_files[:5]) + ("..." if len(existing_files) > 5 else ""))
|
|
157
|
+
|
|
158
|
+
confirm = input(f"{Fore.WHITE}{Style.BRIGHT}❓ Continue anyway? (y/N): {Style.RESET_ALL}").strip().lower()
|
|
159
|
+
if confirm != 'y':
|
|
160
|
+
print_info("Project initialization cancelled.")
|
|
161
|
+
return
|
|
162
|
+
|
|
163
|
+
create_from_json(project_root, structure["{project_name}"], variables)
|
|
164
|
+
print_success(f"React admin dashboard '{project_name}' initialized in current directory: {project_root}")
|
|
165
|
+
|
|
166
|
+
# Ask user for dependency installation
|
|
167
|
+
print(f"\n{Fore.CYAN}{Style.BRIGHT}📦 NODE.JS DEPENDENCY SETUP{Style.RESET_ALL}")
|
|
168
|
+
print("─" * 40)
|
|
169
|
+
print(f"{Fore.YELLOW}{Style.BRIGHT}[1]{Style.RESET_ALL} {Fore.WHITE}Install dependencies now (npm install){Style.RESET_ALL}")
|
|
170
|
+
print(f"{Fore.YELLOW}{Style.BRIGHT}[2]{Style.RESET_ALL} {Fore.WHITE}Use existing node_modules (if available){Style.RESET_ALL}")
|
|
171
|
+
print(f"{Fore.YELLOW}{Style.BRIGHT}[3]{Style.RESET_ALL} {Fore.WHITE}Skip dependency installation{Style.RESET_ALL}")
|
|
172
|
+
|
|
173
|
+
while True:
|
|
174
|
+
deps_choice = input(f"{Fore.WHITE}{Style.BRIGHT}❓ Select dependency option (1-3): {Style.RESET_ALL}").strip()
|
|
175
|
+
|
|
176
|
+
# Clear screen after selection
|
|
177
|
+
os.system('cls' if os.name == 'nt' else 'clear')
|
|
178
|
+
|
|
179
|
+
if deps_choice == "1":
|
|
180
|
+
install_npm_dependencies(project_root)
|
|
181
|
+
break
|
|
182
|
+
elif deps_choice == "2":
|
|
183
|
+
node_modules_path = os.path.join(project_root, "node_modules")
|
|
184
|
+
if os.path.exists(node_modules_path):
|
|
185
|
+
print_success("Found existing node_modules directory!")
|
|
186
|
+
print_info("Using existing dependencies. Run 'npm install' if you need to update them.")
|
|
187
|
+
else:
|
|
188
|
+
print_warning("No existing node_modules found in this location.")
|
|
189
|
+
print_info("You may need to run 'npm install' manually later.")
|
|
190
|
+
break
|
|
191
|
+
elif deps_choice == "3":
|
|
192
|
+
print_info("Skipping dependency installation.")
|
|
193
|
+
print_info("Remember to run 'npm install' before starting development.")
|
|
194
|
+
break
|
|
195
|
+
else:
|
|
196
|
+
print_error("Invalid choice. Please select 1, 2, or 3.")
|
|
197
|
+
|
|
198
|
+
# Show next steps
|
|
199
|
+
print(f"\n{Fore.GREEN}{Style.BRIGHT}🎉 PROJECT READY!{Style.RESET_ALL}")
|
|
200
|
+
print("─" * 30)
|
|
201
|
+
print_info("Next steps:")
|
|
202
|
+
if deps_choice != "1":
|
|
203
|
+
print(" 1. npm install")
|
|
204
|
+
print(" 2. npm run dev")
|
|
205
|
+
else:
|
|
206
|
+
print(" 1. npm run dev")
|
|
207
|
+
print_info("Your React admin dashboard will be available at http://localhost:5173")
|
|
208
|
+
print_info("Make sure your backend API is running on http://localhost:8000")
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "{project_name}",
|
|
3
|
+
"private": true,
|
|
4
|
+
"version": "0.0.0",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "vite --host=127.0.0.1",
|
|
8
|
+
"build": "tsc -b && vite build",
|
|
9
|
+
"lint": "eslint .",
|
|
10
|
+
"preview": "vite preview"
|
|
11
|
+
},
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"@tanstack/react-query": "^5.99.0",
|
|
14
|
+
"axios": "^1.13.6",
|
|
15
|
+
"lucide-react": "^0.577.0",
|
|
16
|
+
"react": "^19.2.4",
|
|
17
|
+
"react-dom": "^19.2.4",
|
|
18
|
+
"react-router-dom": "^7.13.1",
|
|
19
|
+
"tailwindcss": "^4.2.2"
|
|
20
|
+
},
|
|
21
|
+
"devDependencies": {
|
|
22
|
+
"@eslint/js": "^9.39.4",
|
|
23
|
+
"@types/node": "^24.12.0",
|
|
24
|
+
"@types/react": "^19.2.14",
|
|
25
|
+
"@types/react-dom": "^19.2.3",
|
|
26
|
+
"@vitejs/plugin-react": "^4.3.4",
|
|
27
|
+
"eslint": "^9.39.4",
|
|
28
|
+
"eslint-plugin-react-hooks": "^7.0.1",
|
|
29
|
+
"eslint-plugin-react-refresh": "^0.5.2",
|
|
30
|
+
"globals": "^17.4.0",
|
|
31
|
+
"typescript": "~5.9.3",
|
|
32
|
+
"typescript-eslint": "^8.56.1",
|
|
33
|
+
"vite": "^5.4.17"
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { BrowserRouter, Routes, Route } from 'react-router-dom'
|
|
2
|
+
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
|
3
|
+
import { ThemeProvider } from '@/context/ThemeContext'
|
|
4
|
+
import { AuthProvider, useAuth } from '@/context/AuthContext'
|
|
5
|
+
import { DashboardLayout } from '@/layouts/DashboardLayout'
|
|
6
|
+
import { AuthLayout } from '@/layouts/AuthLayout'
|
|
7
|
+
import { ProtectedRoute } from '@/components/ProtectedRoute'
|
|
8
|
+
import { DashboardPage } from '@/pages/DashboardPage'
|
|
9
|
+
import { LoginPage } from '@/pages/auth/LoginPage'
|
|
10
|
+
import { RegisterPage } from '@/pages/auth/RegisterPage'
|
|
11
|
+
|
|
12
|
+
const queryClient = new QueryClient({
|
|
13
|
+
defaultOptions: {
|
|
14
|
+
queries: {
|
|
15
|
+
staleTime: 1000 * 60 * 2, // 2 minutes
|
|
16
|
+
gcTime: 1000 * 60 * 10, // 10 minutes
|
|
17
|
+
refetchOnWindowFocus: false,
|
|
18
|
+
retry: 1,
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
function AppShell() {
|
|
24
|
+
return (
|
|
25
|
+
<Routes>
|
|
26
|
+
{/* Auth routes */}
|
|
27
|
+
<Route path="/auth" element={<AuthLayout />}>
|
|
28
|
+
<Route path="login" element={<LoginPage />} />
|
|
29
|
+
<Route path="register" element={<RegisterPage />} />
|
|
30
|
+
</Route>
|
|
31
|
+
|
|
32
|
+
{/* Protected Dashboard routes */}
|
|
33
|
+
<Route
|
|
34
|
+
element={
|
|
35
|
+
<ProtectedRoute>
|
|
36
|
+
<DashboardLayout />
|
|
37
|
+
</ProtectedRoute>
|
|
38
|
+
}
|
|
39
|
+
>
|
|
40
|
+
<Route path="/" element={<DashboardPage />} />
|
|
41
|
+
</Route>
|
|
42
|
+
</Routes>
|
|
43
|
+
)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function App() {
|
|
47
|
+
return (
|
|
48
|
+
<QueryClientProvider client={queryClient}>
|
|
49
|
+
<BrowserRouter>
|
|
50
|
+
<ThemeProvider>
|
|
51
|
+
<AuthProvider>
|
|
52
|
+
<AppShell />
|
|
53
|
+
</AuthProvider>
|
|
54
|
+
</ThemeProvider>
|
|
55
|
+
</BrowserRouter>
|
|
56
|
+
</QueryClientProvider>
|
|
57
|
+
)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export default App
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { Navigate } from 'react-router-dom'
|
|
2
|
+
import { useAuth } from '@/context/AuthContext'
|
|
3
|
+
|
|
4
|
+
export function ProtectedRoute({ children }: { children: React.ReactNode }) {
|
|
5
|
+
const { authStatus } = useAuth()
|
|
6
|
+
|
|
7
|
+
if (authStatus === 'loading') {
|
|
8
|
+
return <div className="flex items-center justify-center min-h-screen">Loading...</div>
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
if (authStatus === 'unauthenticated') {
|
|
12
|
+
return <Navigate to="/auth/login" replace />
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
return <>{children}</>
|
|
16
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { NavLink } from 'react-router-dom'
|
|
2
|
+
import { useWorkspace } from '@/context/WorkspaceContext'
|
|
3
|
+
import { useAuth } from '@/context/AuthContext'
|
|
4
|
+
import {
|
|
5
|
+
LayoutDashboard,
|
|
6
|
+
Settings,
|
|
7
|
+
LogOut,
|
|
8
|
+
Menu,
|
|
9
|
+
ChevronRight,
|
|
10
|
+
} from 'lucide-react'
|
|
11
|
+
|
|
12
|
+
interface NavItem {
|
|
13
|
+
to: string
|
|
14
|
+
icon: React.ElementType
|
|
15
|
+
label: string
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const workspaceNav: NavItem[] = [
|
|
19
|
+
{ to: '/', icon: LayoutDashboard, label: 'Dashboard' },
|
|
20
|
+
{ to: '/settings', icon: Settings, label: 'Settings' },
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
export function Sidebar() {
|
|
24
|
+
const { sidebarCollapsed, setSidebarCollapsed } = useWorkspace()
|
|
25
|
+
const { user, logout } = useAuth()
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<aside
|
|
29
|
+
className={`fixed left-0 top-0 z-40 flex h-screen flex-col border-r border-gray-200 bg-white transition-all duration-300 ${
|
|
30
|
+
sidebarCollapsed ? 'w-[72px]' : 'w-[260px]'
|
|
31
|
+
}`}
|
|
32
|
+
>
|
|
33
|
+
{/* Logo */}
|
|
34
|
+
<div className="flex h-16 items-center border-b border-gray-200 px-4">
|
|
35
|
+
<button
|
|
36
|
+
onClick={() => setSidebarCollapsed(!sidebarCollapsed)}
|
|
37
|
+
className="rounded-lg p-2 hover:bg-gray-100 transition-colors"
|
|
38
|
+
>
|
|
39
|
+
<Menu className="h-5 w-5 text-gray-600" />
|
|
40
|
+
</button>
|
|
41
|
+
{!sidebarCollapsed && (
|
|
42
|
+
<span className="ml-3 text-lg font-semibold text-gray-800">
|
|
43
|
+
Admin
|
|
44
|
+
</span>
|
|
45
|
+
)}
|
|
46
|
+
</div>
|
|
47
|
+
|
|
48
|
+
{/* Navigation */}
|
|
49
|
+
<nav className="flex-1 overflow-y-auto p-4">
|
|
50
|
+
<ul className="space-y-1">
|
|
51
|
+
{workspaceNav.map((item) => {
|
|
52
|
+
const Icon = item.icon
|
|
53
|
+
return (
|
|
54
|
+
<li key={item.to}>
|
|
55
|
+
<NavLink
|
|
56
|
+
to={item.to}
|
|
57
|
+
className={({ isActive }) =>
|
|
58
|
+
`flex items-center rounded-lg px-3 py-2 transition-colors ${
|
|
59
|
+
isActive
|
|
60
|
+
? 'bg-blue-50 text-blue-600'
|
|
61
|
+
: 'text-gray-700 hover:bg-gray-100'
|
|
62
|
+
} ${sidebarCollapsed ? 'justify-center' : ''}`
|
|
63
|
+
}
|
|
64
|
+
>
|
|
65
|
+
<Icon className="h-5 w-5 flex-shrink-0" />
|
|
66
|
+
{!sidebarCollapsed && (
|
|
67
|
+
<span className="ml-3">{item.label}</span>
|
|
68
|
+
)}
|
|
69
|
+
</NavLink>
|
|
70
|
+
</li>
|
|
71
|
+
)
|
|
72
|
+
})}
|
|
73
|
+
</ul>
|
|
74
|
+
</nav>
|
|
75
|
+
|
|
76
|
+
{/* User Section */}
|
|
77
|
+
<div className="border-t border-gray-200 p-4">
|
|
78
|
+
<button
|
|
79
|
+
onClick={logout}
|
|
80
|
+
className={`flex items-center rounded-lg px-3 py-2 text-gray-700 hover:bg-gray-100 transition-colors ${
|
|
81
|
+
sidebarCollapsed ? 'justify-center' : 'w-full'
|
|
82
|
+
}`}
|
|
83
|
+
>
|
|
84
|
+
<LogOut className="h-5 w-5 flex-shrink-0" />
|
|
85
|
+
{!sidebarCollapsed && <span className="ml-3">Logout</span>}
|
|
86
|
+
</button>
|
|
87
|
+
</div>
|
|
88
|
+
</aside>
|
|
89
|
+
)
|
|
90
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { useWorkspace } from '@/context/WorkspaceContext'
|
|
2
|
+
import { useAuth } from '@/context/AuthContext'
|
|
3
|
+
import { useTheme } from '@/context/ThemeContext'
|
|
4
|
+
import { User, LogOut, Menu } from 'lucide-react'
|
|
5
|
+
|
|
6
|
+
export function TopNav() {
|
|
7
|
+
const { setSidebarCollapsed } = useWorkspace()
|
|
8
|
+
const { user, logout } = useAuth()
|
|
9
|
+
const { theme, toggleTheme } = useTheme()
|
|
10
|
+
|
|
11
|
+
return (
|
|
12
|
+
<header className="h-16 border-b border-gray-200 bg-white px-6 flex items-center justify-between">
|
|
13
|
+
<button
|
|
14
|
+
onClick={() => setSidebarCollapsed(true)}
|
|
15
|
+
className="lg:hidden p-2 rounded-lg hover:bg-gray-100"
|
|
16
|
+
>
|
|
17
|
+
<Menu className="h-5 w-5 text-gray-600" />
|
|
18
|
+
</button>
|
|
19
|
+
|
|
20
|
+
<div className="flex items-center gap-4">
|
|
21
|
+
<button
|
|
22
|
+
onClick={toggleTheme}
|
|
23
|
+
className="p-2 rounded-lg hover:bg-gray-100 transition-colors"
|
|
24
|
+
title="Toggle theme"
|
|
25
|
+
>
|
|
26
|
+
{theme === 'light' ? '🌙' : '☀️'}
|
|
27
|
+
</button>
|
|
28
|
+
|
|
29
|
+
<div className="flex items-center gap-2">
|
|
30
|
+
<User className="h-5 w-5 text-gray-600" />
|
|
31
|
+
<span className="text-sm text-gray-700">{user?.name || 'User'}</span>
|
|
32
|
+
</div>
|
|
33
|
+
|
|
34
|
+
<button
|
|
35
|
+
onClick={logout}
|
|
36
|
+
className="p-2 rounded-lg hover:bg-gray-100 transition-colors"
|
|
37
|
+
title="Logout"
|
|
38
|
+
>
|
|
39
|
+
<LogOut className="h-5 w-5 text-gray-600" />
|
|
40
|
+
</button>
|
|
41
|
+
</div>
|
|
42
|
+
</header>
|
|
43
|
+
)
|
|
44
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { createContext, useContext, useState, useCallback } from 'react'
|
|
2
|
+
import type { ReactNode } from 'react'
|
|
3
|
+
import { useNavigate } from 'react-router-dom'
|
|
4
|
+
import { authAxios } from '@/services/base.service'
|
|
5
|
+
import { authEndpoints } from '@/services/api.service'
|
|
6
|
+
import type { User as ApiUser, AuthResult } from '@/services/api.service'
|
|
7
|
+
|
|
8
|
+
export interface User {
|
|
9
|
+
id: string
|
|
10
|
+
name: string
|
|
11
|
+
email: string
|
|
12
|
+
avatar?: string | null
|
|
13
|
+
role?: string
|
|
14
|
+
created_at?: string
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export type AuthStatus = 'loading' | 'authenticated' | 'unauthenticated'
|
|
18
|
+
|
|
19
|
+
export interface AuthContextType {
|
|
20
|
+
user: User | null
|
|
21
|
+
accessToken: string | null
|
|
22
|
+
authStatus: AuthStatus
|
|
23
|
+
isAuthenticated: boolean
|
|
24
|
+
isLoading: boolean
|
|
25
|
+
|
|
26
|
+
setAccessToken: (token: string | null) => void
|
|
27
|
+
setUser: (user: User | null) => void
|
|
28
|
+
|
|
29
|
+
handleAuthSuccess: (result: AuthResult) => void
|
|
30
|
+
tryRefreshToken: () => Promise<boolean>
|
|
31
|
+
handleLogout: () => void
|
|
32
|
+
login: (email: string, password: string) => Promise<void>
|
|
33
|
+
register: (name: string, email: string, password: string) => Promise<void>
|
|
34
|
+
logout: () => void
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const AuthContext = createContext<AuthContextType | undefined>(undefined)
|
|
38
|
+
|
|
39
|
+
export function AuthProvider({ children }: { children: ReactNode }) {
|
|
40
|
+
const [user, setUser] = useState<User | null>(null)
|
|
41
|
+
const [accessToken, setAccessToken] = useState<string | null>(null)
|
|
42
|
+
const [authStatus, setAuthStatus] = useState<AuthStatus>('loading')
|
|
43
|
+
const navigate = useNavigate()
|
|
44
|
+
|
|
45
|
+
const handleAuthSuccess = useCallback((result: AuthResult) => {
|
|
46
|
+
setAccessToken(result.access)
|
|
47
|
+
setUser(result.user)
|
|
48
|
+
setAuthStatus('authenticated')
|
|
49
|
+
}, [])
|
|
50
|
+
|
|
51
|
+
const tryRefreshToken = useCallback(async (): Promise<boolean> => {
|
|
52
|
+
try {
|
|
53
|
+
const result = await authEndpoints.refreshToken(authAxios)
|
|
54
|
+
setAccessToken(result.access)
|
|
55
|
+
setAuthStatus('authenticated')
|
|
56
|
+
return true
|
|
57
|
+
} catch {
|
|
58
|
+
setAuthStatus('unauthenticated')
|
|
59
|
+
setAccessToken(null)
|
|
60
|
+
setUser(null)
|
|
61
|
+
return false
|
|
62
|
+
}
|
|
63
|
+
}, [])
|
|
64
|
+
|
|
65
|
+
const handleLogout = useCallback(() => {
|
|
66
|
+
setAccessToken(null)
|
|
67
|
+
setUser(null)
|
|
68
|
+
setAuthStatus('unauthenticated')
|
|
69
|
+
navigate('/auth/login')
|
|
70
|
+
}, [navigate])
|
|
71
|
+
|
|
72
|
+
const login = useCallback(async (email: string, password: string) => {
|
|
73
|
+
const result = await authEndpoints.login(authAxios, email, password)
|
|
74
|
+
handleAuthSuccess(result)
|
|
75
|
+
}, [handleAuthSuccess])
|
|
76
|
+
|
|
77
|
+
const register = useCallback(async (name: string, email: string, password: string) => {
|
|
78
|
+
const result = await authEndpoints.register(authAxios, name, email, password)
|
|
79
|
+
handleAuthSuccess(result)
|
|
80
|
+
}, [handleAuthSuccess])
|
|
81
|
+
|
|
82
|
+
const value: AuthContextType = {
|
|
83
|
+
user,
|
|
84
|
+
accessToken,
|
|
85
|
+
authStatus,
|
|
86
|
+
isAuthenticated: authStatus === 'authenticated',
|
|
87
|
+
isLoading: authStatus === 'loading',
|
|
88
|
+
setAccessToken,
|
|
89
|
+
setUser,
|
|
90
|
+
handleAuthSuccess,
|
|
91
|
+
tryRefreshToken,
|
|
92
|
+
handleLogout,
|
|
93
|
+
login,
|
|
94
|
+
register,
|
|
95
|
+
logout: handleLogout,
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function useAuth() {
|
|
102
|
+
const context = useContext(AuthContext)
|
|
103
|
+
if (context === undefined) {
|
|
104
|
+
throw new Error('useAuth must be used within an AuthProvider')
|
|
105
|
+
}
|
|
106
|
+
return context
|
|
107
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { createContext, useContext, useState, useCallback } from 'react'
|
|
2
|
+
import type { ReactNode } from 'react'
|
|
3
|
+
|
|
4
|
+
type Theme = 'light' | 'dark'
|
|
5
|
+
|
|
6
|
+
interface ThemeContextType {
|
|
7
|
+
theme: Theme
|
|
8
|
+
toggleTheme: () => void
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const ThemeContext = createContext<ThemeContextType | undefined>(undefined)
|
|
12
|
+
|
|
13
|
+
export function ThemeProvider({ children }: { children: ReactNode }) {
|
|
14
|
+
const [theme, setTheme] = useState<Theme>('light')
|
|
15
|
+
|
|
16
|
+
const toggleTheme = useCallback(() => {
|
|
17
|
+
setTheme((prev) => (prev === 'light' ? 'dark' : 'light'))
|
|
18
|
+
}, [])
|
|
19
|
+
|
|
20
|
+
const value: ThemeContextType = {
|
|
21
|
+
theme,
|
|
22
|
+
toggleTheme,
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function useTheme() {
|
|
29
|
+
const context = useContext(ThemeContext)
|
|
30
|
+
if (context === undefined) {
|
|
31
|
+
throw new Error('useTheme must be used within a ThemeProvider')
|
|
32
|
+
}
|
|
33
|
+
return context
|
|
34
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { createContext, useContext, useState } from 'react'
|
|
2
|
+
import type { ReactNode } from 'react'
|
|
3
|
+
|
|
4
|
+
interface WorkspaceContextType {
|
|
5
|
+
sidebarCollapsed: boolean
|
|
6
|
+
setSidebarCollapsed: (collapsed: boolean) => void
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const WorkspaceContext = createContext<WorkspaceContextType | undefined>(undefined)
|
|
10
|
+
|
|
11
|
+
export function WorkspaceProvider({ children }: { children: ReactNode }) {
|
|
12
|
+
const [sidebarCollapsed, setSidebarCollapsed] = useState(false)
|
|
13
|
+
|
|
14
|
+
const value: WorkspaceContextType = {
|
|
15
|
+
sidebarCollapsed,
|
|
16
|
+
setSidebarCollapsed,
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return <WorkspaceContext.Provider value={value}>{children}</WorkspaceContext.Provider>
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function useWorkspace() {
|
|
23
|
+
const context = useContext(WorkspaceContext)
|
|
24
|
+
if (context === undefined) {
|
|
25
|
+
throw new Error('useWorkspace must be used within a WorkspaceProvider')
|
|
26
|
+
}
|
|
27
|
+
return context
|
|
28
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
|
|
2
|
+
import { baseAxios } from '@/services/base.service'
|
|
3
|
+
import { extractErrorMessage } from '@/services/base.service'
|
|
4
|
+
|
|
5
|
+
export function useApi<T>(
|
|
6
|
+
queryKey: string[],
|
|
7
|
+
url: string,
|
|
8
|
+
options?: { enabled?: boolean }
|
|
9
|
+
) {
|
|
10
|
+
return useQuery({
|
|
11
|
+
queryKey,
|
|
12
|
+
queryFn: async () => {
|
|
13
|
+
const response = await baseAxios.get(url)
|
|
14
|
+
return response.data.data as T
|
|
15
|
+
},
|
|
16
|
+
enabled: options?.enabled,
|
|
17
|
+
})
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function useApiMutation<T, V = unknown>(
|
|
21
|
+
url: string,
|
|
22
|
+
method: 'POST' | 'PUT' | 'PATCH' | 'DELETE' = 'POST'
|
|
23
|
+
) {
|
|
24
|
+
const queryClient = useQueryClient()
|
|
25
|
+
|
|
26
|
+
return useMutation({
|
|
27
|
+
mutationFn: async (data?: V) => {
|
|
28
|
+
const response = await baseAxios({
|
|
29
|
+
method,
|
|
30
|
+
url,
|
|
31
|
+
data,
|
|
32
|
+
})
|
|
33
|
+
return response.data.data as T
|
|
34
|
+
},
|
|
35
|
+
onError: (error) => {
|
|
36
|
+
console.error('API mutation error:', extractErrorMessage(error))
|
|
37
|
+
},
|
|
38
|
+
})
|
|
39
|
+
}
|