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.
Files changed (148) hide show
  1. ntc/__init__.py +5 -0
  2. ntc/app_creator.py +296 -0
  3. ntc/cli.py +4 -0
  4. ntc/cli_enhanced.py +820 -0
  5. ntc/composer/__init__.py +0 -0
  6. ntc/composer/main.py +443 -0
  7. ntc/deployment/__init__.py +0 -0
  8. ntc/templates/__init__.py +2 -0
  9. ntc/templates/auth_app/__init__.py +0 -0
  10. ntc/templates/auth_app/admin.py +24 -0
  11. ntc/templates/auth_app/apps.py +6 -0
  12. ntc/templates/auth_app/models.py +30 -0
  13. ntc/templates/auth_app/serializers.py +101 -0
  14. ntc/templates/auth_app/urls.py +19 -0
  15. ntc/templates/auth_app/views.py +175 -0
  16. ntc/templates/drf_structure/__init__.py +1 -0
  17. ntc/templates/drf_structure/drf_structure.md +83 -0
  18. ntc/templates/drf_structure/main.py +389 -0
  19. ntc/templates/drf_structure/structure.json +126 -0
  20. ntc/templates/drf_structure/templates/.entry/start_core_backend.sh +37 -0
  21. ntc/templates/drf_structure/templates/.entry/start_tasks_backend.sh +30 -0
  22. ntc/templates/drf_structure/templates/CLAUDE.md +194 -0
  23. ntc/templates/drf_structure/templates/README.md +117 -0
  24. ntc/templates/drf_structure/templates/apps/app_admin.py +3 -0
  25. ntc/templates/drf_structure/templates/apps/app_apps.py +6 -0
  26. ntc/templates/drf_structure/templates/apps/app_models.py +4 -0
  27. ntc/templates/drf_structure/templates/apps/app_urls.py +8 -0
  28. ntc/templates/drf_structure/templates/apps/app_views.py +6 -0
  29. ntc/templates/drf_structure/templates/apps/common_apps.py +6 -0
  30. ntc/templates/drf_structure/templates/apps/common_models.py +8 -0
  31. ntc/templates/drf_structure/templates/config/gunicorn_config.py +9 -0
  32. ntc/templates/drf_structure/templates/config/http.conf +33 -0
  33. ntc/templates/drf_structure/templates/core/base_model.py +9 -0
  34. ntc/templates/drf_structure/templates/core/formatter.py +5 -0
  35. ntc/templates/drf_structure/templates/core/logger.py +8 -0
  36. ntc/templates/drf_structure/templates/core/pagination.py +6 -0
  37. ntc/templates/drf_structure/templates/core/response_wrapper.py +7 -0
  38. ntc/templates/drf_structure/templates/core/services/azure/email/backend.py +108 -0
  39. ntc/templates/drf_structure/templates/core/services/azure/email/client.py +296 -0
  40. ntc/templates/drf_structure/templates/core/services/azure/email/notifier.py +102 -0
  41. ntc/templates/drf_structure/templates/core/services/object_storage.py +285 -0
  42. ntc/templates/drf_structure/templates/django_project/asgi.py +5 -0
  43. ntc/templates/drf_structure/templates/django_project/celery.py +8 -0
  44. ntc/templates/drf_structure/templates/django_project/dev_settings.py +2 -0
  45. ntc/templates/drf_structure/templates/django_project/django_settings.txt +322 -0
  46. ntc/templates/drf_structure/templates/django_project/management_command.py +114 -0
  47. ntc/templates/drf_structure/templates/django_project/prod_settings.py +3 -0
  48. ntc/templates/drf_structure/templates/django_project/schema.py +7 -0
  49. ntc/templates/drf_structure/templates/django_project/urls.py +6 -0
  50. ntc/templates/drf_structure/templates/django_project/wsgi.py +5 -0
  51. ntc/templates/drf_structure/templates/docker/docker-compose.yml +11 -0
  52. ntc/templates/drf_structure/templates/knowledge/knowledge_description.md +201 -0
  53. ntc/templates/drf_structure/templates/knowledge/knowledge_design_pattern.md +722 -0
  54. ntc/templates/drf_structure/templates/knowledge/knowledge_structure.md +151 -0
  55. ntc/templates/drf_structure/templates/root/README.md +307 -0
  56. ntc/templates/drf_structure/templates/root/django_env.txt +98 -0
  57. ntc/templates/drf_structure/templates/root/django_requirements.txt +24 -0
  58. ntc/templates/drf_structure/templates/root/gitignore.txt +6 -0
  59. ntc/templates/drf_structure/templates/root/manage.py +11 -0
  60. ntc/templates/flutter/.gitignore +44 -0
  61. ntc/templates/flutter/.knowledge/knowledge_description.md +215 -0
  62. ntc/templates/flutter/.knowledge/knowledge_design_pattern.md +310 -0
  63. ntc/templates/flutter/.knowledge/knowledge_structure.md +83 -0
  64. ntc/templates/flutter/CLAUDE.md +190 -0
  65. ntc/templates/flutter/__init__.py +0 -0
  66. ntc/templates/flutter/analysis_options.yaml +9 -0
  67. ntc/templates/flutter/main.py +187 -0
  68. ntc/templates/flutter/pubspec.yaml +65 -0
  69. ntc/templates/flutter/structure.json +51 -0
  70. ntc/templates/nextjs/.env.example +10 -0
  71. ntc/templates/nextjs/.gitignore +36 -0
  72. ntc/templates/nextjs/CLAUDE.md +181 -0
  73. ntc/templates/nextjs/README.md +94 -0
  74. ntc/templates/nextjs/__init__.py +0 -0
  75. ntc/templates/nextjs/knowledge/knowledge_description.md +208 -0
  76. ntc/templates/nextjs/knowledge/knowledge_design_pattern.md +460 -0
  77. ntc/templates/nextjs/knowledge/knowledge_structure.md +75 -0
  78. ntc/templates/nextjs/main.py +224 -0
  79. ntc/templates/nextjs/package.json +25 -0
  80. ntc/templates/nextjs/src/app/globals.css +26 -0
  81. ntc/templates/nextjs/src/app/layout.tsx +59 -0
  82. ntc/templates/nextjs/src/app/page.tsx +21 -0
  83. ntc/templates/nextjs/src/components/Header.tsx +16 -0
  84. ntc/templates/nextjs/src/services/api.ts +81 -0
  85. ntc/templates/nextjs/src/utils/constants.ts +1 -0
  86. ntc/templates/nextjs/structure.json +32 -0
  87. ntc/templates/nextjs/tsconfig.json +27 -0
  88. ntc/templates/react_admin/.env.example +1 -0
  89. ntc/templates/react_admin/.gitignore +10 -0
  90. ntc/templates/react_admin/CLAUDE.md +210 -0
  91. ntc/templates/react_admin/README.md +72 -0
  92. ntc/templates/react_admin/__init__.py +0 -0
  93. ntc/templates/react_admin/index.html +13 -0
  94. ntc/templates/react_admin/knowledge/knowledge_description.md +202 -0
  95. ntc/templates/react_admin/knowledge/knowledge_design_pattern.md +448 -0
  96. ntc/templates/react_admin/knowledge/knowledge_structure.md +97 -0
  97. ntc/templates/react_admin/main.py +208 -0
  98. ntc/templates/react_admin/package.json +35 -0
  99. ntc/templates/react_admin/src/App.tsx +60 -0
  100. ntc/templates/react_admin/src/components/ProtectedRoute.tsx +16 -0
  101. ntc/templates/react_admin/src/components/Sidebar.tsx +90 -0
  102. ntc/templates/react_admin/src/components/TopNav.tsx +44 -0
  103. ntc/templates/react_admin/src/context/AuthContext.tsx +107 -0
  104. ntc/templates/react_admin/src/context/ThemeContext.tsx +34 -0
  105. ntc/templates/react_admin/src/context/WorkspaceContext.tsx +28 -0
  106. ntc/templates/react_admin/src/hooks/useApi.ts +39 -0
  107. ntc/templates/react_admin/src/index.css +34 -0
  108. ntc/templates/react_admin/src/layouts/AuthLayout.tsx +23 -0
  109. ntc/templates/react_admin/src/layouts/DashboardLayout.tsx +24 -0
  110. ntc/templates/react_admin/src/main.tsx +5 -0
  111. ntc/templates/react_admin/src/pages/DashboardPage.tsx +10 -0
  112. ntc/templates/react_admin/src/pages/auth/LoginPage.tsx +79 -0
  113. ntc/templates/react_admin/src/pages/auth/RegisterPage.tsx +93 -0
  114. ntc/templates/react_admin/src/services/api.service.ts +44 -0
  115. ntc/templates/react_admin/src/services/base.service.ts +193 -0
  116. ntc/templates/react_admin/structure.json +52 -0
  117. ntc/templates/react_admin/tsconfig.app.json +25 -0
  118. ntc/templates/react_admin/tsconfig.json +7 -0
  119. ntc/templates/react_admin/tsconfig.node.json +19 -0
  120. ntc/templates/react_admin/vite.config.ts +13 -0
  121. ntc/templates/react_structure/CLAUDE.md +176 -0
  122. ntc/templates/react_structure/__init__.py +1 -0
  123. ntc/templates/react_structure/index.css +21 -0
  124. ntc/templates/react_structure/index.html +13 -0
  125. ntc/templates/react_structure/knowledge/knowledge_description.md +170 -0
  126. ntc/templates/react_structure/knowledge/knowledge_design_pattern.md +411 -0
  127. ntc/templates/react_structure/knowledge/knowledge_structure.md +74 -0
  128. ntc/templates/react_structure/main.py +217 -0
  129. ntc/templates/react_structure/package.json +32 -0
  130. ntc/templates/react_structure/src/App.tsx +63 -0
  131. ntc/templates/react_structure/src/components/Header.tsx +51 -0
  132. ntc/templates/react_structure/src/main.tsx +5 -0
  133. ntc/templates/react_structure/src/services/api.ts +90 -0
  134. ntc/templates/react_structure/src/utils/constants.ts +1 -0
  135. ntc/templates/react_structure/structure.json +65 -0
  136. ntc/templates/react_structure/tsconfig.app.json +25 -0
  137. ntc/templates/react_structure/tsconfig.json +7 -0
  138. ntc/templates/react_structure/tsconfig.node.json +19 -0
  139. ntc/templates/react_structure/vite.config.ts +12 -0
  140. ntc/utils/__init__.py +1 -0
  141. ntc/utils/environment.py +67 -0
  142. ntc/utils/output.py +41 -0
  143. ntc_dev_kit-1.0.0.dist-info/METADATA +188 -0
  144. ntc_dev_kit-1.0.0.dist-info/RECORD +148 -0
  145. ntc_dev_kit-1.0.0.dist-info/WHEEL +5 -0
  146. ntc_dev_kit-1.0.0.dist-info/entry_points.txt +2 -0
  147. ntc_dev_kit-1.0.0.dist-info/licenses/LICENSE +21 -0
  148. 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,5 @@
1
+ import { createRoot } from 'react-dom/client'
2
+ import './index.css'
3
+ import App from './App.tsx'
4
+
5
+ createRoot(document.getElementById('root')!).render(<App />)
@@ -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,7 @@
1
+ {
2
+ "files": [],
3
+ "references": [
4
+ { "path": "./tsconfig.app.json" },
5
+ { "path": "./tsconfig.node.json" }
6
+ ]
7
+ }
@@ -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
+ })