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,34 @@
|
|
|
1
|
+
@import "tailwindcss";
|
|
2
|
+
|
|
3
|
+
:root {
|
|
4
|
+
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
|
|
5
|
+
line-height: 1.5;
|
|
6
|
+
font-weight: 400;
|
|
7
|
+
|
|
8
|
+
color-scheme: light dark;
|
|
9
|
+
color: rgba(255, 255, 255, 0.87);
|
|
10
|
+
background-color: #242424;
|
|
11
|
+
|
|
12
|
+
font-synthesis: none;
|
|
13
|
+
text-rendering: optimizeLegibility;
|
|
14
|
+
-webkit-font-smoothing: antialiased;
|
|
15
|
+
-moz-osx-font-smoothing: grayscale;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
body {
|
|
19
|
+
margin: 0;
|
|
20
|
+
display: flex;
|
|
21
|
+
place-items: center;
|
|
22
|
+
min-width: 320px;
|
|
23
|
+
min-height: 100vh;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
#root {
|
|
27
|
+
max-width: 1280px;
|
|
28
|
+
margin: 0 auto;
|
|
29
|
+
text-align: center;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
* {
|
|
33
|
+
box-sizing: border-box;
|
|
34
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { Outlet, Navigate, useLocation } from 'react-router-dom'
|
|
2
|
+
import { useAuth } from '@/context/AuthContext'
|
|
3
|
+
|
|
4
|
+
export function AuthLayout() {
|
|
5
|
+
const { authStatus } = useAuth()
|
|
6
|
+
const location = useLocation()
|
|
7
|
+
|
|
8
|
+
if (authStatus === 'loading') return null
|
|
9
|
+
|
|
10
|
+
if (authStatus === 'authenticated') {
|
|
11
|
+
const from = (location.state as { from?: { pathname?: string } } | null)?.from
|
|
12
|
+
const redirectTo = from?.pathname || '/'
|
|
13
|
+
return <Navigate to={redirectTo} replace />
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
return (
|
|
17
|
+
<div className="min-h-screen flex items-center justify-center bg-gray-100">
|
|
18
|
+
<div className="w-full max-w-md">
|
|
19
|
+
<Outlet />
|
|
20
|
+
</div>
|
|
21
|
+
</div>
|
|
22
|
+
)
|
|
23
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { Outlet } from 'react-router-dom'
|
|
2
|
+
import { Sidebar } from '@/components/Sidebar'
|
|
3
|
+
import { TopNav } from '@/components/TopNav'
|
|
4
|
+
import { useWorkspace } from '@/context/WorkspaceContext'
|
|
5
|
+
|
|
6
|
+
export function DashboardLayout() {
|
|
7
|
+
const { sidebarCollapsed } = useWorkspace()
|
|
8
|
+
|
|
9
|
+
return (
|
|
10
|
+
<div className="flex h-screen overflow-hidden bg-gray-100">
|
|
11
|
+
<Sidebar />
|
|
12
|
+
<div className="flex flex-1 flex-col overflow-hidden">
|
|
13
|
+
<TopNav />
|
|
14
|
+
<main
|
|
15
|
+
className={`relative flex-1 overflow-y-auto p-6 transition-all duration-300 ${
|
|
16
|
+
sidebarCollapsed ? 'ml-[72px]' : 'ml-[260px]'
|
|
17
|
+
}`}
|
|
18
|
+
>
|
|
19
|
+
<Outlet />
|
|
20
|
+
</main>
|
|
21
|
+
</div>
|
|
22
|
+
</div>
|
|
23
|
+
)
|
|
24
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export function DashboardPage() {
|
|
2
|
+
return (
|
|
3
|
+
<div>
|
|
4
|
+
<h1 className="text-2xl font-bold text-gray-900 mb-6">Dashboard</h1>
|
|
5
|
+
<div className="bg-white rounded-lg shadow p-6">
|
|
6
|
+
<p className="text-gray-600">Welcome to the admin dashboard!</p>
|
|
7
|
+
</div>
|
|
8
|
+
</div>
|
|
9
|
+
)
|
|
10
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { useState } from 'react'
|
|
2
|
+
import { useNavigate, Link } from 'react-router-dom'
|
|
3
|
+
import { useAuth } from '@/context/AuthContext'
|
|
4
|
+
|
|
5
|
+
export function LoginPage() {
|
|
6
|
+
const [email, setEmail] = useState('')
|
|
7
|
+
const [password, setPassword] = useState('')
|
|
8
|
+
const [error, setError] = useState('')
|
|
9
|
+
const [loading, setLoading] = useState(false)
|
|
10
|
+
const { login } = useAuth()
|
|
11
|
+
const navigate = useNavigate()
|
|
12
|
+
|
|
13
|
+
const handleSubmit = async (e: React.FormEvent) => {
|
|
14
|
+
e.preventDefault()
|
|
15
|
+
setError('')
|
|
16
|
+
setLoading(true)
|
|
17
|
+
|
|
18
|
+
try {
|
|
19
|
+
await login(email, password)
|
|
20
|
+
navigate('/')
|
|
21
|
+
} catch (err: any) {
|
|
22
|
+
setError(err.message || 'Login failed')
|
|
23
|
+
} finally {
|
|
24
|
+
setLoading(false)
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<div className="bg-white rounded-lg shadow-lg p-8">
|
|
30
|
+
<h1 className="text-2xl font-bold text-gray-900 mb-6">Login</h1>
|
|
31
|
+
{error && (
|
|
32
|
+
<div className="mb-4 p-3 bg-red-50 border border-red-200 text-red-600 rounded">
|
|
33
|
+
{error}
|
|
34
|
+
</div>
|
|
35
|
+
)}
|
|
36
|
+
<form onSubmit={handleSubmit} className="space-y-4">
|
|
37
|
+
<div>
|
|
38
|
+
<label htmlFor="email" className="block text-sm font-medium text-gray-700 mb-1">
|
|
39
|
+
Email
|
|
40
|
+
</label>
|
|
41
|
+
<input
|
|
42
|
+
type="email"
|
|
43
|
+
id="email"
|
|
44
|
+
value={email}
|
|
45
|
+
onChange={(e) => setEmail(e.target.value)}
|
|
46
|
+
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
47
|
+
required
|
|
48
|
+
/>
|
|
49
|
+
</div>
|
|
50
|
+
<div>
|
|
51
|
+
<label htmlFor="password" className="block text-sm font-medium text-gray-700 mb-1">
|
|
52
|
+
Password
|
|
53
|
+
</label>
|
|
54
|
+
<input
|
|
55
|
+
type="password"
|
|
56
|
+
id="password"
|
|
57
|
+
value={password}
|
|
58
|
+
onChange={(e) => setPassword(e.target.value)}
|
|
59
|
+
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
60
|
+
required
|
|
61
|
+
/>
|
|
62
|
+
</div>
|
|
63
|
+
<button
|
|
64
|
+
type="submit"
|
|
65
|
+
disabled={loading}
|
|
66
|
+
className="w-full bg-blue-600 text-white py-2 px-4 rounded-lg hover:bg-blue-700 disabled:opacity-50 transition-colors"
|
|
67
|
+
>
|
|
68
|
+
{loading ? 'Loading...' : 'Login'}
|
|
69
|
+
</button>
|
|
70
|
+
</form>
|
|
71
|
+
<p className="mt-4 text-center text-sm text-gray-600">
|
|
72
|
+
Don't have an account?{' '}
|
|
73
|
+
<Link to="/auth/register" className="text-blue-600 hover:underline">
|
|
74
|
+
Register
|
|
75
|
+
</Link>
|
|
76
|
+
</p>
|
|
77
|
+
</div>
|
|
78
|
+
)
|
|
79
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { useState } from 'react'
|
|
2
|
+
import { useNavigate, Link } from 'react-router-dom'
|
|
3
|
+
import { useAuth } from '@/context/AuthContext'
|
|
4
|
+
|
|
5
|
+
export function RegisterPage() {
|
|
6
|
+
const [name, setName] = useState('')
|
|
7
|
+
const [email, setEmail] = useState('')
|
|
8
|
+
const [password, setPassword] = useState('')
|
|
9
|
+
const [error, setError] = useState('')
|
|
10
|
+
const [loading, setLoading] = useState(false)
|
|
11
|
+
const { register } = useAuth()
|
|
12
|
+
const navigate = useNavigate()
|
|
13
|
+
|
|
14
|
+
const handleSubmit = async (e: React.FormEvent) => {
|
|
15
|
+
e.preventDefault()
|
|
16
|
+
setError('')
|
|
17
|
+
setLoading(true)
|
|
18
|
+
|
|
19
|
+
try {
|
|
20
|
+
await register(name, email, password)
|
|
21
|
+
navigate('/')
|
|
22
|
+
} catch (err: any) {
|
|
23
|
+
setError(err.message || 'Registration failed')
|
|
24
|
+
} finally {
|
|
25
|
+
setLoading(false)
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<div className="bg-white rounded-lg shadow-lg p-8">
|
|
31
|
+
<h1 className="text-2xl font-bold text-gray-900 mb-6">Register</h1>
|
|
32
|
+
{error && (
|
|
33
|
+
<div className="mb-4 p-3 bg-red-50 border border-red-200 text-red-600 rounded">
|
|
34
|
+
{error}
|
|
35
|
+
</div>
|
|
36
|
+
)}
|
|
37
|
+
<form onSubmit={handleSubmit} className="space-y-4">
|
|
38
|
+
<div>
|
|
39
|
+
<label htmlFor="name" className="block text-sm font-medium text-gray-700 mb-1">
|
|
40
|
+
Name
|
|
41
|
+
</label>
|
|
42
|
+
<input
|
|
43
|
+
type="text"
|
|
44
|
+
id="name"
|
|
45
|
+
value={name}
|
|
46
|
+
onChange={(e) => setName(e.target.value)}
|
|
47
|
+
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
48
|
+
required
|
|
49
|
+
/>
|
|
50
|
+
</div>
|
|
51
|
+
<div>
|
|
52
|
+
<label htmlFor="email" className="block text-sm font-medium text-gray-700 mb-1">
|
|
53
|
+
Email
|
|
54
|
+
</label>
|
|
55
|
+
<input
|
|
56
|
+
type="email"
|
|
57
|
+
id="email"
|
|
58
|
+
value={email}
|
|
59
|
+
onChange={(e) => setEmail(e.target.value)}
|
|
60
|
+
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
61
|
+
required
|
|
62
|
+
/>
|
|
63
|
+
</div>
|
|
64
|
+
<div>
|
|
65
|
+
<label htmlFor="password" className="block text-sm font-medium text-gray-700 mb-1">
|
|
66
|
+
Password
|
|
67
|
+
</label>
|
|
68
|
+
<input
|
|
69
|
+
type="password"
|
|
70
|
+
id="password"
|
|
71
|
+
value={password}
|
|
72
|
+
onChange={(e) => setPassword(e.target.value)}
|
|
73
|
+
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
74
|
+
required
|
|
75
|
+
/>
|
|
76
|
+
</div>
|
|
77
|
+
<button
|
|
78
|
+
type="submit"
|
|
79
|
+
disabled={loading}
|
|
80
|
+
className="w-full bg-blue-600 text-white py-2 px-4 rounded-lg hover:bg-blue-700 disabled:opacity-50 transition-colors"
|
|
81
|
+
>
|
|
82
|
+
{loading ? 'Loading...' : 'Register'}
|
|
83
|
+
</button>
|
|
84
|
+
</form>
|
|
85
|
+
<p className="mt-4 text-center text-sm text-gray-600">
|
|
86
|
+
Already have an account?{' '}
|
|
87
|
+
<Link to="/auth/login" className="text-blue-600 hover:underline">
|
|
88
|
+
Login
|
|
89
|
+
</Link>
|
|
90
|
+
</p>
|
|
91
|
+
</div>
|
|
92
|
+
)
|
|
93
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { AxiosInstance } from 'axios'
|
|
2
|
+
import { get, post, put, patch, del, upload } from './base.service'
|
|
3
|
+
|
|
4
|
+
export interface User {
|
|
5
|
+
id: string
|
|
6
|
+
name: string
|
|
7
|
+
email: string
|
|
8
|
+
avatar?: string | null
|
|
9
|
+
role?: string
|
|
10
|
+
created_at: string
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface AuthResult {
|
|
14
|
+
access: string
|
|
15
|
+
refresh: string
|
|
16
|
+
user: User
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const authEndpoints = {
|
|
20
|
+
login: async (instance: AxiosInstance, email: string, password: string): Promise<AuthResult> => {
|
|
21
|
+
return post(instance, '/auth/login/', { email, password })
|
|
22
|
+
},
|
|
23
|
+
register: async (instance: AxiosInstance, name: string, email: string, password: string): Promise<AuthResult> => {
|
|
24
|
+
return post(instance, '/auth/register/', { name, email, password })
|
|
25
|
+
},
|
|
26
|
+
logout: async (instance: AxiosInstance): Promise<void> => {
|
|
27
|
+
return post(instance, '/auth/logout/')
|
|
28
|
+
},
|
|
29
|
+
refreshToken: async (instance: AxiosInstance): Promise<{ access: string }> => {
|
|
30
|
+
return post(instance, '/auth/refresh/')
|
|
31
|
+
},
|
|
32
|
+
verifyOTP: async (instance: AxiosInstance, email: string, code: string): Promise<AuthResult> => {
|
|
33
|
+
return post(instance, '/auth/verify-otp/', { email, code })
|
|
34
|
+
},
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export const userEndpoints = {
|
|
38
|
+
getMe: async (instance: AxiosInstance): Promise<User> => {
|
|
39
|
+
return get(instance, '/users/me/')
|
|
40
|
+
},
|
|
41
|
+
updateProfile: async (instance: AxiosInstance, data: Partial<User>): Promise<User> => {
|
|
42
|
+
return patch(instance, '/users/me/', data)
|
|
43
|
+
},
|
|
44
|
+
}
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import axios from 'axios'
|
|
2
|
+
import type { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Determines the API base URL based on the current environment.
|
|
6
|
+
*/
|
|
7
|
+
function resolveBaseUrl(): string {
|
|
8
|
+
const envOverride = import.meta.env.VITE_API_BASE_URL
|
|
9
|
+
if (envOverride) return envOverride
|
|
10
|
+
|
|
11
|
+
const hostname = window.location.hostname
|
|
12
|
+
|
|
13
|
+
if (hostname === 'localhost' || hostname === '127.0.0.1') {
|
|
14
|
+
return 'http://127.0.0.1:8000/v1'
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
return 'https://api.{project_name}.com/v1'
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const BASE_URL = resolveBaseUrl()
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Base Axios instance shared across all service layers.
|
|
24
|
+
*/
|
|
25
|
+
export const baseAxios: AxiosInstance = axios.create({
|
|
26
|
+
baseURL: BASE_URL,
|
|
27
|
+
withCredentials: true,
|
|
28
|
+
headers: {
|
|
29
|
+
'Content-Type': 'application/json',
|
|
30
|
+
Accept: 'application/json',
|
|
31
|
+
},
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Axios instance for authentication endpoints.
|
|
36
|
+
*/
|
|
37
|
+
export const authAxios: AxiosInstance = axios.create({
|
|
38
|
+
baseURL: BASE_URL,
|
|
39
|
+
withCredentials: true,
|
|
40
|
+
headers: {
|
|
41
|
+
'Content-Type': 'application/json',
|
|
42
|
+
Accept: 'application/json',
|
|
43
|
+
},
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
export type ApiResponse<T> = {
|
|
47
|
+
data: T
|
|
48
|
+
message?: string
|
|
49
|
+
status: number
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export type ApiError = {
|
|
53
|
+
message: string
|
|
54
|
+
status: number
|
|
55
|
+
errors?: Record<string, string[]>
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function humanizeFieldName(field: string): string {
|
|
59
|
+
return field
|
|
60
|
+
.split('.')
|
|
61
|
+
.at(-1)
|
|
62
|
+
?.replace(/_/g, ' ')
|
|
63
|
+
.replace(/\b\w/g, (char) => char.toUpperCase()) ?? field
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function getPrimaryValidationMessage(
|
|
67
|
+
errors?: Record<string, string[]>,
|
|
68
|
+
): string | null {
|
|
69
|
+
if (!errors) return null
|
|
70
|
+
|
|
71
|
+
for (const [field, messages] of Object.entries(errors)) {
|
|
72
|
+
const firstMessage = messages.find(Boolean)
|
|
73
|
+
if (firstMessage) {
|
|
74
|
+
return `${humanizeFieldName(field)}: ${firstMessage}`
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return null
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Extracts a normalized ApiError from an Axios error response.
|
|
83
|
+
*/
|
|
84
|
+
export function extractApiError(error: unknown): ApiError {
|
|
85
|
+
if (axios.isAxiosError(error)) {
|
|
86
|
+
const data = error.response?.data
|
|
87
|
+
const validationMessage = getPrimaryValidationMessage(data?.errors)
|
|
88
|
+
const rawMessage = data?.message || error.message || 'An unexpected error occurred'
|
|
89
|
+
const message =
|
|
90
|
+
rawMessage === 'Validation error' && validationMessage
|
|
91
|
+
? validationMessage
|
|
92
|
+
: rawMessage
|
|
93
|
+
|
|
94
|
+
return {
|
|
95
|
+
message,
|
|
96
|
+
status: error.response?.status || 500,
|
|
97
|
+
errors: data?.errors,
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return {
|
|
101
|
+
message: error instanceof Error ? error.message : 'An unexpected error occurred',
|
|
102
|
+
status: 500,
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Extracts a human-readable error message string from an unknown error.
|
|
108
|
+
*/
|
|
109
|
+
export function extractErrorMessage(error: unknown, fallback = 'An unexpected error occurred'): string {
|
|
110
|
+
return extractApiError(error).message || fallback
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Generic GET helper.
|
|
115
|
+
*/
|
|
116
|
+
export async function get<T>(
|
|
117
|
+
instance: AxiosInstance,
|
|
118
|
+
url: string,
|
|
119
|
+
config?: AxiosRequestConfig,
|
|
120
|
+
): Promise<T> {
|
|
121
|
+
const response: AxiosResponse<ApiResponse<T>> = await instance.get(url, config)
|
|
122
|
+
return response.data.data
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Generic POST helper.
|
|
127
|
+
*/
|
|
128
|
+
export async function post<T>(
|
|
129
|
+
instance: AxiosInstance,
|
|
130
|
+
url: string,
|
|
131
|
+
data?: unknown,
|
|
132
|
+
config?: AxiosRequestConfig,
|
|
133
|
+
): Promise<T> {
|
|
134
|
+
const response: AxiosResponse<ApiResponse<T>> = await instance.post(url, data, config)
|
|
135
|
+
return response.data.data
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Generic PUT helper.
|
|
140
|
+
*/
|
|
141
|
+
export async function put<T>(
|
|
142
|
+
instance: AxiosInstance,
|
|
143
|
+
url: string,
|
|
144
|
+
data?: unknown,
|
|
145
|
+
config?: AxiosRequestConfig,
|
|
146
|
+
): Promise<T> {
|
|
147
|
+
const response: AxiosResponse<ApiResponse<T>> = await instance.put(url, data, config)
|
|
148
|
+
return response.data.data
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Generic PATCH helper.
|
|
153
|
+
*/
|
|
154
|
+
export async function patch<T>(
|
|
155
|
+
instance: AxiosInstance,
|
|
156
|
+
url: string,
|
|
157
|
+
data?: unknown,
|
|
158
|
+
config?: AxiosRequestConfig,
|
|
159
|
+
): Promise<T> {
|
|
160
|
+
const response: AxiosResponse<ApiResponse<T>> = await instance.patch(url, data, config)
|
|
161
|
+
return response.data.data
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Generic DELETE helper.
|
|
166
|
+
*/
|
|
167
|
+
export async function del<T>(
|
|
168
|
+
instance: AxiosInstance,
|
|
169
|
+
url: string,
|
|
170
|
+
config?: AxiosRequestConfig,
|
|
171
|
+
): Promise<T> {
|
|
172
|
+
const response: AxiosResponse<ApiResponse<T>> = await instance.delete(url, config)
|
|
173
|
+
return response.data.data
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Multipart form-data POST (for file uploads).
|
|
178
|
+
*/
|
|
179
|
+
export async function upload<T>(
|
|
180
|
+
instance: AxiosInstance,
|
|
181
|
+
url: string,
|
|
182
|
+
formData: FormData,
|
|
183
|
+
config?: AxiosRequestConfig,
|
|
184
|
+
): Promise<T> {
|
|
185
|
+
const response: AxiosResponse<ApiResponse<T>> = await instance.post(url, formData, {
|
|
186
|
+
...config,
|
|
187
|
+
headers: {
|
|
188
|
+
...config?.headers,
|
|
189
|
+
'Content-Type': 'multipart/form-data',
|
|
190
|
+
},
|
|
191
|
+
})
|
|
192
|
+
return response.data.data
|
|
193
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"{project_name}": {
|
|
3
|
+
"package.json": { "template": "react_admin/package.json" },
|
|
4
|
+
".gitignore": "node_modules/\ndist/\n.env.local\n.env.development.local\n.env.test.local\n.env.production.local\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\n.DS_Store\n",
|
|
5
|
+
"README.md": "# {project_name} Admin Dashboard\n\nA React + TypeScript + Vite admin dashboard initialized by `ntc react init`.\n\n## Tech Stack\n\n- React 19\n- TypeScript\n- Vite 8\n- Tailwind CSS 4\n- React Router 7\n- TanStack Query\n- Axios\n- Lucide React\n\n## Getting Started\n\n```bash\nnpm install\nnpm run dev\n```\n\n## Available Scripts\n\n- `npm run dev` - Runs the app in development mode\n- `npm run build` - Builds the app for production\n- `npm run lint` - Runs ESLint\n- `npm run preview` - Previews the production build\n",
|
|
6
|
+
"index.html": { "template": "react_admin/index.html" },
|
|
7
|
+
"vite.config.ts": { "template": "react_admin/vite.config.ts" },
|
|
8
|
+
"tsconfig.json": { "template": "react_admin/tsconfig.json" },
|
|
9
|
+
"tsconfig.app.json": { "template": "react_admin/tsconfig.app.json" },
|
|
10
|
+
"tsconfig.node.json": { "template": "react_admin/tsconfig.node.json" },
|
|
11
|
+
".env.example": "VITE_API_BASE_URL=http://localhost:8000/v1\n",
|
|
12
|
+
"CLAUDE.md": { "template": "react_admin/CLAUDE.md" },
|
|
13
|
+
".knowledge": {
|
|
14
|
+
"knowledge_structure.md": { "template": "react_admin/knowledge/knowledge_structure.md" },
|
|
15
|
+
"knowledge_description.md": { "template": "react_admin/knowledge/knowledge_description.md" },
|
|
16
|
+
"knowledge_design_pattern.md": { "template": "react_admin/knowledge/knowledge_design_pattern.md" }
|
|
17
|
+
},
|
|
18
|
+
"src": {
|
|
19
|
+
"main.tsx": { "template": "react_admin/src/main.tsx" },
|
|
20
|
+
"index.css": { "template": "react_admin/src/index.css" },
|
|
21
|
+
"App.tsx": { "template": "react_admin/src/App.tsx" },
|
|
22
|
+
"services": {
|
|
23
|
+
"base.service.ts": { "template": "react_admin/src/services/base.service.ts" },
|
|
24
|
+
"api.service.ts": { "template": "react_admin/src/services/api.service.ts" }
|
|
25
|
+
},
|
|
26
|
+
"context": {
|
|
27
|
+
"AuthContext.tsx": { "template": "react_admin/src/context/AuthContext.tsx" },
|
|
28
|
+
"ThemeContext.tsx": { "template": "react_admin/src/context/ThemeContext.tsx" },
|
|
29
|
+
"WorkspaceContext.tsx": { "template": "react_admin/src/context/WorkspaceContext.tsx" }
|
|
30
|
+
},
|
|
31
|
+
"layouts": {
|
|
32
|
+
"DashboardLayout.tsx": { "template": "react_admin/src/layouts/DashboardLayout.tsx" },
|
|
33
|
+
"AuthLayout.tsx": { "template": "react_admin/src/layouts/AuthLayout.tsx" }
|
|
34
|
+
},
|
|
35
|
+
"components": {
|
|
36
|
+
"ProtectedRoute.tsx": { "template": "react_admin/src/components/ProtectedRoute.tsx" },
|
|
37
|
+
"Sidebar.tsx": { "template": "react_admin/src/components/Sidebar.tsx" },
|
|
38
|
+
"TopNav.tsx": { "template": "react_admin/src/components/TopNav.tsx" }
|
|
39
|
+
},
|
|
40
|
+
"pages": {
|
|
41
|
+
"DashboardPage.tsx": { "template": "react_admin/src/pages/DashboardPage.tsx" },
|
|
42
|
+
"auth": {
|
|
43
|
+
"LoginPage.tsx": { "template": "react_admin/src/pages/auth/LoginPage.tsx" },
|
|
44
|
+
"RegisterPage.tsx": { "template": "react_admin/src/pages/auth/RegisterPage.tsx" }
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
"hooks": {
|
|
48
|
+
"useApi.ts": { "template": "react_admin/src/hooks/useApi.ts" }
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2020",
|
|
4
|
+
"useDefineForClassFields": true,
|
|
5
|
+
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
|
6
|
+
"module": "ESNext",
|
|
7
|
+
"skipLibCheck": true,
|
|
8
|
+
"moduleResolution": "bundler",
|
|
9
|
+
"allowImportingTsExtensions": true,
|
|
10
|
+
"isolatedModules": true,
|
|
11
|
+
"moduleDetection": "force",
|
|
12
|
+
"noEmit": true,
|
|
13
|
+
"jsx": "react-jsx",
|
|
14
|
+
"strict": true,
|
|
15
|
+
"noUnusedLocals": true,
|
|
16
|
+
"noUnusedParameters": true,
|
|
17
|
+
"noFallthroughCasesInSwitch": true,
|
|
18
|
+
"noUncheckedSideEffectImports": true,
|
|
19
|
+
"baseUrl": ".",
|
|
20
|
+
"paths": {
|
|
21
|
+
"@/*": ["./src/*"]
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
"include": ["src"]
|
|
25
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"lib": ["ES2023"],
|
|
5
|
+
"module": "ESNext",
|
|
6
|
+
"skipLibCheck": true,
|
|
7
|
+
"moduleResolution": "bundler",
|
|
8
|
+
"allowImportingTsExtensions": true,
|
|
9
|
+
"isolatedModules": true,
|
|
10
|
+
"moduleDetection": "force",
|
|
11
|
+
"noEmit": true,
|
|
12
|
+
"strict": true,
|
|
13
|
+
"noUnusedLocals": true,
|
|
14
|
+
"noUnusedParameters": true,
|
|
15
|
+
"noFallthroughCasesInSwitch": true,
|
|
16
|
+
"noUncheckedSideEffectImports": true
|
|
17
|
+
},
|
|
18
|
+
"include": ["vite.config.ts"]
|
|
19
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { defineConfig } from 'vite'
|
|
2
|
+
import react from '@vitejs/plugin-react'
|
|
3
|
+
import tailwindcss from '@tailwindcss/vite'
|
|
4
|
+
import path from 'path'
|
|
5
|
+
|
|
6
|
+
export default defineConfig({
|
|
7
|
+
plugins: [react(), tailwindcss()],
|
|
8
|
+
resolve: {
|
|
9
|
+
alias: {
|
|
10
|
+
'@': path.resolve(__dirname, './src'),
|
|
11
|
+
},
|
|
12
|
+
},
|
|
13
|
+
})
|