diff --git a/src/features/global-search/GlobalSearchTrigger.tsx b/src/features/global-search/GlobalSearchTrigger.tsx
index 26ee2f6..815cb2d 100644
--- a/src/features/global-search/GlobalSearchTrigger.tsx
+++ b/src/features/global-search/GlobalSearchTrigger.tsx
@@ -1,44 +1,18 @@
import { Ic } from '@/icons'
import { useGlobalSearchStore } from '@/stores/global-search'
-const isMac =
- typeof navigator !== 'undefined' &&
- /mac|iphone|ipad|ipod/i.test(navigator.platform || navigator.userAgent)
-
export default function GlobalSearchTrigger() {
const setOpen = useGlobalSearchStore((s) => s.setOpen)
return (
- <>
- {/* دسکتاپ: پیلِ شبیه اینپوت با میانبر صفحهکلید */}
-
-
-
-
- {/* موبایل: دکمهٔ آیکونی کوچک */}
-
-
-
- >
+
)
}
diff --git a/src/icons.tsx b/src/icons.tsx
index c34b78e..dc1b689 100644
--- a/src/icons.tsx
+++ b/src/icons.tsx
@@ -24,6 +24,7 @@ const PATHS: Record
= {
volume: '',
list: '',
grid: '',
+ layoutDashboard: '',
sparkles: '',
share: '',
users: '',
@@ -65,6 +66,10 @@ const PATHS: Record = {
chart: '',
shield: '',
folder: '',
+ sun: '',
+ moon: '',
+ server: '',
+ code: '',
}
export type IconName = keyof typeof PATHS
diff --git a/src/index.css b/src/index.css
index 8b9e32f..c7481df 100644
--- a/src/index.css
+++ b/src/index.css
@@ -9,7 +9,7 @@
--card-foreground: #ffffff;
--popover: #1e2128;
--popover-foreground: #ffffff;
- --primary: #6366f1; /* رنگ اصلی تم (قابل تغییر برای هر برند) */
+ --primary: #d09460; /* رنگ اصلی برند عقیله (برنز هویت بصری) */
--primary-foreground: #ffffff;
--secondary: #2c303b;
--secondary-foreground: #ebebeb;
@@ -20,18 +20,18 @@
--destructive: #ef4444;
--border: rgba(255, 255, 255, 0.12);
--input: rgba(255, 255, 255, 0.06);
- --ring: rgba(99, 102, 241, 0.55);
+ --ring: rgba(208, 148, 96, 0.55);
--radius: 14px;
/* پالت سایدبار و پنل کناری */
--sidebar: #131417;
--sidebar-foreground: #ebebeb;
- --sidebar-primary: #6366f1;
+ --sidebar-primary: #d09460;
--sidebar-primary-foreground: #ffffff;
--sidebar-accent: rgba(255, 255, 255, 0.06);
--sidebar-accent-foreground: #ffffff;
--sidebar-border: rgba(255, 255, 255, 0.07);
- --sidebar-ring: rgba(99, 102, 241, 0.55);
+ --sidebar-ring: rgba(208, 148, 96, 0.55);
--sidebar-width: 240px;
--panel-width: 384px;
@@ -43,7 +43,44 @@
--grey-3: #8b8b8b;
--grey-4: #4d4d4d;
--border-soft: rgba(255, 255, 255, 0.07);
- --g-primary: linear-gradient(135deg, #6366f1, #4338ca);
+ --g-primary: linear-gradient(135deg, #d09460, #a86e3d);
+}
+
+/* ─── Light Mode Tokens ─── */
+html.light {
+ --background: #f8fafc;
+ --foreground: #0f172a;
+ --card: #ffffff;
+ --card-foreground: #0f172a;
+ --popover: #ffffff;
+ --popover-foreground: #0f172a;
+ --primary: #d09460;
+ --primary-foreground: #ffffff;
+ --secondary: #f1f5f9;
+ --secondary-foreground: #1e293b;
+ --muted: #f1f5f9;
+ --muted-foreground: #64748b;
+ --accent: #f1f5f9;
+ --accent-foreground: #0f172a;
+ --destructive: #ef4444;
+ --border: rgba(15, 23, 42, 0.12);
+ --input: rgba(15, 23, 42, 0.06);
+ --ring: rgba(208, 148, 96, 0.55);
+
+ --sidebar: #ffffff;
+ --sidebar-foreground: #1e293b;
+ --sidebar-primary: #d09460;
+ --sidebar-primary-foreground: #ffffff;
+ --sidebar-accent: rgba(15, 23, 42, 0.05);
+ --sidebar-accent-foreground: #0f172a;
+ --sidebar-border: rgba(15, 23, 42, 0.08);
+ --sidebar-ring: rgba(208, 148, 96, 0.55);
+
+ --grey-1: #0f172a;
+ --grey-2: #334155;
+ --grey-3: #64748b;
+ --grey-4: #94a3b8;
+ --border-soft: rgba(15, 23, 42, 0.08);
}
@theme inline {
diff --git a/src/main.tsx b/src/main.tsx
index 8c40d7c..78e6349 100644
--- a/src/main.tsx
+++ b/src/main.tsx
@@ -2,8 +2,11 @@ import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import Providers from '@/app/providers'
import App from '@/app/App'
+import { useThemeStore, applyTheme } from '@/stores/theme'
import './index.css'
+applyTheme(useThemeStore.getState().isDark)
+
createRoot(document.getElementById('root')!).render(
diff --git a/src/services/auth.ts b/src/services/auth.ts
index 45a198d..d4f2cf0 100644
--- a/src/services/auth.ts
+++ b/src/services/auth.ts
@@ -10,43 +10,58 @@ import type { AuthUser, LoginInput } from '@/types'
* ────────────────────────────────────────────────────────────────────────── */
export async function login(input: LoginInput): Promise<{ token: string; user: AuthUser }> {
- // لاگین آزمایشی پیشفرض برای اجرای مستقیم تمپلیت
- if (
- (input.email === 'admin@gmail.com' || input.email === 'admin@example.com') &&
- input.password === 'admin'
- ) {
+ try {
+ const res = await apiFetch('account/login/', {
+ method: 'POST',
+ body: { email: input.email, password: input.password },
+ skipAuth: true,
+ })
+
+ const token = res.token ?? res.access_token ?? res.accessToken ?? res.data?.token
+ const userData = res.user ?? res.data?.user ?? res
+
return {
- token: 'demo-mock-jwt-token',
+ token,
user: {
- id: 1,
- email: input.email,
- name: 'مدیر کل سیستم',
- role: 'admin',
- isStaff: true,
- isSuperuser: true,
+ id: userData.id ?? userData.user_id ?? 1,
+ email: userData.email ?? input.email,
+ name: userData.fullname ?? userData.name ?? 'کاربر',
+ role: userData.role ?? (userData.is_superuser ? 'superadmin' : userData.is_staff ? 'admin' : 'user'),
+ isStaff: userData.is_staff ?? userData.isStaff ?? true,
+ isSuperuser: userData.is_superuser ?? userData.isSuperuser ?? false,
},
}
- }
-
- const res = await apiFetch('auth/login', {
- method: 'POST',
- body: { email: input.email, password: input.password },
- skipAuth: true,
- })
-
- return {
- token: res.token ?? res.access_token ?? res.accessToken ?? res.data?.token,
- user: {
- id: res.user?.id ?? res.user_id ?? res.data?.user?.id ?? 1,
- email: res.user?.email ?? res.email ?? res.data?.user?.email ?? input.email,
- name: res.user?.name ?? res.name ?? res.data?.user?.name ?? 'کاربر',
- role: res.user?.role ?? res.role ?? 'admin',
- isStaff: res.user?.isStaff ?? res.is_staff ?? true,
- isSuperuser: res.user?.isSuperuser ?? res.is_superuser ?? false,
- },
+ } catch (err: any) {
+ // در صورت عدم دسترسی به سرور (status === 0)، اجازه تست دمو داده میشود
+ if (
+ err?.status === 0 &&
+ (input.email === 'admin@gmail.com' || input.email === 'admin@example.com') &&
+ input.password === 'admin'
+ ) {
+ return {
+ token: 'demo-mock-jwt-token',
+ user: {
+ id: 1,
+ email: input.email,
+ name: 'مدیر کل سیستم (آزمایشی)',
+ role: 'admin',
+ isStaff: true,
+ isSuperuser: true,
+ },
+ }
+ }
+ throw err
}
}
export async function getProfile(): Promise {
- return apiFetch('auth/me')
+ const res = await apiFetch('account/profile/')
+ return {
+ id: res.id,
+ email: res.email,
+ name: res.fullname ?? res.name ?? 'کاربر',
+ role: res.is_superuser ? 'superadmin' : res.is_staff ? 'admin' : 'user',
+ isStaff: res.is_staff ?? false,
+ isSuperuser: res.is_superuser ?? false,
+ }
}
diff --git a/src/stores/server-health.ts b/src/stores/server-health.ts
new file mode 100644
index 0000000..d8a1f04
--- /dev/null
+++ b/src/stores/server-health.ts
@@ -0,0 +1,36 @@
+import { create } from 'zustand'
+import { apiFetch } from '@/services/http'
+
+export interface HealthState {
+ status: 'checking' | 'connected' | 'error'
+ message?: string
+ latencyMs?: number
+ lastChecked?: Date
+ checkHealth: (isSilent?: boolean) => Promise
+}
+
+export const useServerHealthStore = create((set) => ({
+ status: 'checking',
+ latencyMs: undefined,
+ message: 'در حال بررسی اتصال به سرور...',
+ checkHealth: async (isSilent = false) => {
+ if (!isSilent) set({ status: 'checking' })
+ const start = performance.now()
+ try {
+ await apiFetch('about-us/', { skipAuth: true })
+ const latency = Math.round(performance.now() - start)
+ set({
+ status: 'connected',
+ latencyMs: latency,
+ message: 'ارتباط مستقیم با بکاند جنگو (aqila_backend) برقرار است',
+ lastChecked: new Date(),
+ })
+ } catch {
+ set({
+ status: 'error',
+ message: 'عدم دریافت پاسخ از سرور (بررسی کنید که سرور جنگو فعال باشد)',
+ lastChecked: new Date(),
+ })
+ }
+ },
+}))
diff --git a/src/stores/theme.ts b/src/stores/theme.ts
new file mode 100644
index 0000000..873a9d2
--- /dev/null
+++ b/src/stores/theme.ts
@@ -0,0 +1,48 @@
+import { create } from 'zustand'
+import { persist } from 'zustand/middleware'
+
+interface ThemeState {
+ isDark: boolean
+ toggleTheme: () => void
+ setTheme: (isDark: boolean) => void
+}
+
+export const useThemeStore = create()(
+ persist(
+ (set, get) => ({
+ isDark: true,
+ toggleTheme: () => {
+ const next = !get().isDark
+ set({ isDark: next })
+ applyTheme(next)
+ },
+ setTheme: (isDark: boolean) => {
+ set({ isDark })
+ applyTheme(isDark)
+ },
+ }),
+ {
+ name: 'dashboard-theme',
+ onRehydrateStorage: () => (state) => {
+ if (state) {
+ applyTheme(state.isDark)
+ } else {
+ applyTheme(true)
+ }
+ },
+ },
+ ),
+)
+
+export function applyTheme(isDark: boolean) {
+ if (typeof document !== 'undefined') {
+ const root = document.documentElement
+ if (isDark) {
+ root.classList.add('dark')
+ root.classList.remove('light')
+ } else {
+ root.classList.add('light')
+ root.classList.remove('dark')
+ }
+ }
+}
diff --git a/vite.config.ts b/vite.config.ts
index 33b347a..1d4ff15 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -1,8 +1,112 @@
///
import path from 'node:path'
-import { defineConfig, loadEnv } from 'vite'
+import { exec } from 'node:child_process'
+import os from 'node:os'
+import { defineConfig, loadEnv, type Plugin } from 'vite'
import react from '@vitejs/plugin-react'
import tailwindcss from '@tailwindcss/vite'
+import babel from '@babel/core'
+import addDataLocator from './plugins/add-data-locator.cjs'
+
+function openInIdePlugin(): Plugin {
+ return {
+ name: 'open-in-ide-plugin',
+ apply: 'serve',
+ configureServer(server) {
+ server.middlewares.use((req, res, next) => {
+ const url = req.url?.split('?')[0]
+ if (url === '/api/open-in-ide' && req.method === 'POST') {
+ let body = ''
+ req.on('data', (chunk) => {
+ body += chunk
+ })
+ req.on('end', () => {
+ try {
+ const { locator } = JSON.parse(body)
+ if (!locator) {
+ res.statusCode = 400
+ res.setHeader('Content-Type', 'application/json')
+ res.end(JSON.stringify({ message: 'Locator is required' }))
+ return
+ }
+
+ const isWindows = os.platform() === 'win32'
+ const bin = isWindows ? 'antigravity-ide.cmd' : 'antigravity-ide'
+ const cmd = `${bin} -r -g "${locator}"`
+
+ exec(cmd, (error) => {
+ if (error) {
+ console.error(`[open-in-ide] Error: ${error.message}`)
+ res.statusCode = 500
+ res.setHeader('Content-Type', 'application/json')
+ res.end(
+ JSON.stringify({
+ message: 'Failed to open file in IDE',
+ error: error.message,
+ })
+ )
+ return
+ }
+ res.statusCode = 200
+ res.setHeader('Content-Type', 'application/json')
+ res.end(JSON.stringify({ success: true }))
+ })
+ } catch (err) {
+ console.error('[open-in-ide] Error parsing body:', err, 'raw body:', body)
+ res.statusCode = 500
+ res.setHeader('Content-Type', 'application/json')
+ res.end(JSON.stringify({ message: 'Internal server error', error: String(err), rawBody: body }))
+ }
+ })
+ return
+ }
+
+ if (
+ (url === '/api/remote-logs' ||
+ url === '/api/remote-network' ||
+ url === '/api/dev-storage') &&
+ req.method === 'POST'
+ ) {
+ res.statusCode = 200
+ res.setHeader('Content-Type', 'application/json')
+ res.end(JSON.stringify({ ok: true }))
+ return
+ }
+
+ next()
+ })
+ },
+ }
+}
+
+function addDataLocatorPlugin(): Plugin {
+ return {
+ name: 'add-data-locator-plugin',
+ enforce: 'pre',
+ apply: 'serve',
+ transform(code, id) {
+ const cleanId = id.split('?')[0]
+ if (cleanId.includes('node_modules') || !/\.[jt]sx$/.test(cleanId)) {
+ return null
+ }
+ try {
+ const result = babel.transformSync(code, {
+ filename: cleanId,
+ configFile: false,
+ babelrc: false,
+ parserOpts: { plugins: ['jsx', 'typescript'] },
+ plugins: [addDataLocator],
+ })
+ if (result?.code) {
+ return { code: result.code, map: result.map }
+ }
+ } catch (err) {
+ console.error('[add-data-locator-plugin] Transform error:', err)
+ }
+ return null
+ },
+ }
+}
export default defineConfig(({ mode }) => {
const env = loadEnv(mode, process.cwd(), '')
@@ -10,7 +114,12 @@ export default defineConfig(({ mode }) => {
const apiTarget = env.VITE_API_TARGET ?? 'http://localhost:8000'
return {
- plugins: [react(), tailwindcss()],
+ plugins: [
+ openInIdePlugin(),
+ addDataLocatorPlugin(),
+ react(),
+ tailwindcss(),
+ ],
resolve: {
alias: { '@': path.resolve(__dirname, './src') },
},
@@ -19,8 +128,6 @@ export default defineConfig(({ mode }) => {
'/api': {
target: apiTarget,
changeOrigin: true,
- /* بکاند پیشوند '/api' ندارد؛ آن را حذف میکنیم تا به ریشه برسد. */
- rewrite: (p) => p.replace(/^\/api/, ''),
},
},
},