first commit
This commit is contained in:
14
.env.example
Normal file
14
.env.example
Normal file
@@ -0,0 +1,14 @@
|
||||
# GEMINI_API_KEY: Required for Gemini AI API calls.
|
||||
# AI Studio automatically injects this at runtime from user secrets.
|
||||
# Users configure this via the Secrets panel in the AI Studio UI.
|
||||
GEMINI_API_KEY="MY_GEMINI_API_KEY"
|
||||
|
||||
# APP_URL: The URL where this applet is hosted.
|
||||
# AI Studio automatically injects this at runtime with the Cloud Run service URL.
|
||||
# Used for self-referential links, OAuth callbacks, and API endpoints.
|
||||
APP_URL="MY_APP_URL"
|
||||
|
||||
# Backend API base URL for auth and dashboard requests.
|
||||
# Example:
|
||||
# VITE_API_BASE_URL="https://api.example.com"
|
||||
VITE_API_BASE_URL="http://qkg4sggc0ocoo04cwokk0g80.65.109.214.67.sslip.io"
|
||||
8
.gitignore
vendored
Normal file
8
.gitignore
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
node_modules/
|
||||
build/
|
||||
dist/
|
||||
coverage/
|
||||
.DS_Store
|
||||
*.log
|
||||
.env*
|
||||
!.env.example
|
||||
21
README.md
Normal file
21
README.md
Normal file
@@ -0,0 +1,21 @@
|
||||
<div align="center">
|
||||
<img width="1200" height="475" alt="GHBanner" src="https://github.com/user-attachments/assets/0aa67016-6eaf-458a-adb2-6e31a0763ed6" />
|
||||
</div>
|
||||
|
||||
# Run and deploy your AI Studio app
|
||||
|
||||
This contains everything you need to run your app locally.
|
||||
|
||||
View your app in AI Studio: https://ai.studio/apps/8a661eb5-dd66-42cd-a58c-87ef78c4d19c
|
||||
|
||||
## Run Locally
|
||||
|
||||
**Prerequisites:** Node.js
|
||||
|
||||
|
||||
1. Install dependencies:
|
||||
`npm install`
|
||||
2. Set the `GEMINI_API_KEY` in `.env.local`
|
||||
3. Set `VITE_API_BASE_URL` in `.env.local` to your backend address
|
||||
4. Run the app:
|
||||
`npm run dev`
|
||||
13
index.html
Normal file
13
index.html
Normal file
@@ -0,0 +1,13 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>My Google AI Studio App</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
6
metadata.json
Normal file
6
metadata.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"name": "GymPro Admin Panel",
|
||||
"description": "یک پنل مدیریت پیشرفته برای باشگاههای ورزشی با قابلیت مدیریت اعضا، کلاسها، و پرداختها به همراه تم تاریک و روشن.",
|
||||
"requestFramePermissions": [],
|
||||
"majorCapabilities": []
|
||||
}
|
||||
4852
package-lock.json
generated
Normal file
4852
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
38
package.json
Normal file
38
package.json
Normal file
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"name": "react-example",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite --port=3000 --host=0.0.0.0",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"clean": "rm -rf dist",
|
||||
"lint": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@google/genai": "^1.29.0",
|
||||
"@tailwindcss/vite": "^4.1.14",
|
||||
"@vitejs/plugin-react": "^5.0.4",
|
||||
"axios": "^1.15.2",
|
||||
"clsx": "^2.1.1",
|
||||
"dotenv": "^17.2.3",
|
||||
"express": "^4.21.2",
|
||||
"lucide-react": "^0.546.0",
|
||||
"motion": "^12.23.24",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"recharts": "^3.8.1",
|
||||
"tailwind-merge": "^3.5.0",
|
||||
"vite": "^6.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/express": "^4.17.21",
|
||||
"@types/node": "^22.14.0",
|
||||
"autoprefixer": "^10.4.21",
|
||||
"tailwindcss": "^4.1.14",
|
||||
"tsx": "^4.21.0",
|
||||
"typescript": "~5.8.2",
|
||||
"vite": "^6.2.0"
|
||||
}
|
||||
}
|
||||
1387
src/App.tsx
Normal file
1387
src/App.tsx
Normal file
File diff suppressed because it is too large
Load Diff
26
src/index.css
Normal file
26
src/index.css
Normal file
@@ -0,0 +1,26 @@
|
||||
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Vazirmatn:wght@400;500;700&display=swap');
|
||||
@import "tailwindcss";
|
||||
|
||||
@theme {
|
||||
--font-sans: "Inter", "Vazirmatn", ui-sans-serif, system-ui, sans-serif;
|
||||
}
|
||||
|
||||
@layer base {
|
||||
body {
|
||||
@apply bg-slate-50 text-slate-900 transition-colors duration-300;
|
||||
}
|
||||
|
||||
.dark body {
|
||||
@apply bg-[#0A0A0B] text-zinc-100;
|
||||
}
|
||||
}
|
||||
|
||||
@theme {
|
||||
--color-accent: #A3E635;
|
||||
--color-card: #161618;
|
||||
--color-border: #27272a;
|
||||
}
|
||||
|
||||
.rtl {
|
||||
direction: rtl;
|
||||
}
|
||||
50
src/lib/utils.ts
Normal file
50
src/lib/utils.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import { clsx, type ClassValue } from 'clsx';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs));
|
||||
}
|
||||
|
||||
const PERSIAN_DIGITS = '۰۱۲۳۴۵۶۷۸۹';
|
||||
const ARABIC_DIGITS = '٠١٢٣٤٥٦٧٨٩';
|
||||
|
||||
export function normalizeDigits(value: string) {
|
||||
return value
|
||||
.split('')
|
||||
.map((char) => {
|
||||
const persianIndex = PERSIAN_DIGITS.indexOf(char);
|
||||
if (persianIndex >= 0) {
|
||||
return String(persianIndex);
|
||||
}
|
||||
|
||||
const arabicIndex = ARABIC_DIGITS.indexOf(char);
|
||||
if (arabicIndex >= 0) {
|
||||
return String(arabicIndex);
|
||||
}
|
||||
|
||||
return char;
|
||||
})
|
||||
.join('');
|
||||
}
|
||||
|
||||
export function normalizeMobileNumber(value: string) {
|
||||
const digitsOnly = normalizeDigits(value).replace(/\D/g, '');
|
||||
|
||||
if (digitsOnly.startsWith('0098')) {
|
||||
return digitsOnly.slice(4);
|
||||
}
|
||||
|
||||
if (digitsOnly.startsWith('98')) {
|
||||
return digitsOnly.slice(2);
|
||||
}
|
||||
|
||||
if (digitsOnly.startsWith('0')) {
|
||||
return digitsOnly.slice(1);
|
||||
}
|
||||
|
||||
return digitsOnly;
|
||||
}
|
||||
|
||||
export function normalizeVerificationCode(value: string) {
|
||||
return normalizeDigits(value).replace(/\D/g, '');
|
||||
}
|
||||
10
src/main.tsx
Normal file
10
src/main.tsx
Normal file
@@ -0,0 +1,10 @@
|
||||
import {StrictMode} from 'react';
|
||||
import {createRoot} from 'react-dom/client';
|
||||
import App from './App.tsx';
|
||||
import './index.css';
|
||||
|
||||
createRoot(document.getElementById('root')!).render(
|
||||
<StrictMode>
|
||||
<App />
|
||||
</StrictMode>,
|
||||
);
|
||||
84
src/services/api.ts
Normal file
84
src/services/api.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
import axios from 'axios';
|
||||
|
||||
const DEFAULT_API_HOST = 'qkg4sggc0ocoo04cwokk0g80.65.109.214.67.sslip.io';
|
||||
|
||||
const resolveApiBaseUrl = () => {
|
||||
const configuredBaseUrl = import.meta.env.VITE_API_BASE_URL?.trim();
|
||||
if (configuredBaseUrl) {
|
||||
return configuredBaseUrl;
|
||||
}
|
||||
|
||||
if (typeof window !== 'undefined' && window.location.protocol === 'https:') {
|
||||
return `https://${DEFAULT_API_HOST}`;
|
||||
}
|
||||
|
||||
return `http://${DEFAULT_API_HOST}`;
|
||||
};
|
||||
|
||||
export const API_BASE_URL = resolveApiBaseUrl();
|
||||
|
||||
export const getApiErrorMessage = (error: unknown, fallbackMessage: string) => {
|
||||
if (axios.isAxiosError(error)) {
|
||||
if (!error.response) {
|
||||
return 'اتصال به سرور برقرار نشد. آدرس بکاند یا در دسترس بودن سرور را بررسی کنید.';
|
||||
}
|
||||
|
||||
const responseMessage =
|
||||
error.response.data?.errors?.[0]?.message ||
|
||||
error.response.data?.message ||
|
||||
error.response.data?.title;
|
||||
|
||||
if (typeof responseMessage === 'string' && responseMessage.trim()) {
|
||||
return responseMessage;
|
||||
}
|
||||
}
|
||||
|
||||
if (error instanceof Error && error.message.trim()) {
|
||||
return error.message;
|
||||
}
|
||||
|
||||
return fallbackMessage;
|
||||
};
|
||||
|
||||
export const api = axios.create({
|
||||
baseURL: API_BASE_URL,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
api.interceptors.request.use((config) => {
|
||||
const token = localStorage.getItem('accessToken');
|
||||
if (token) {
|
||||
config.headers.Authorization = `Bearer ${token}`;
|
||||
}
|
||||
return config;
|
||||
});
|
||||
|
||||
api.interceptors.response.use(
|
||||
(response) => response,
|
||||
async (error) => {
|
||||
const originalRequest = error.config;
|
||||
if (error.response?.status === 401 && !originalRequest._retry) {
|
||||
originalRequest._retry = true;
|
||||
const refreshToken = localStorage.getItem('refreshToken');
|
||||
if (refreshToken) {
|
||||
try {
|
||||
const response = await axios.post(`${API_BASE_URL}/api/v1/accounts/refresh-token`, { refreshToken });
|
||||
if (response.data.isSuccess) {
|
||||
const { accessToken, refreshToken: newRefreshToken } = response.data.value;
|
||||
localStorage.setItem('accessToken', accessToken);
|
||||
localStorage.setItem('refreshToken', newRefreshToken);
|
||||
originalRequest.headers.Authorization = `Bearer ${accessToken}`;
|
||||
return api(originalRequest);
|
||||
}
|
||||
} catch (refreshError) {
|
||||
localStorage.removeItem('accessToken');
|
||||
localStorage.removeItem('refreshToken');
|
||||
window.location.reload();
|
||||
}
|
||||
}
|
||||
}
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
49
src/services/authService.ts
Normal file
49
src/services/authService.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import { api } from './api';
|
||||
import { normalizeMobileNumber, normalizeVerificationCode } from '../lib/utils';
|
||||
|
||||
export const authService = {
|
||||
sendOtp: async (mobileNumber: string) => {
|
||||
const normalizedMobile = normalizeMobileNumber(mobileNumber);
|
||||
const response = await api.post('/api/v1/accounts/send-otp-sms', {
|
||||
mobile: {
|
||||
number: normalizedMobile,
|
||||
countryCode: '98'
|
||||
}
|
||||
});
|
||||
return response.data;
|
||||
},
|
||||
|
||||
login: async (mobileNumber: string, code: string) => {
|
||||
const normalizedMobile = normalizeMobileNumber(mobileNumber);
|
||||
const normalizedCode = normalizeVerificationCode(code);
|
||||
const response = await api.post('/api/v1/accounts/register-and-login-by-mobile', {
|
||||
mobile: {
|
||||
number: normalizedMobile,
|
||||
countryCode: '98'
|
||||
},
|
||||
verificationCode: normalizedCode
|
||||
});
|
||||
|
||||
if (response.data.isSuccess) {
|
||||
const { accessToken, refreshToken } = response.data.value;
|
||||
localStorage.setItem('accessToken', accessToken);
|
||||
localStorage.setItem('refreshToken', refreshToken);
|
||||
}
|
||||
|
||||
return response.data;
|
||||
},
|
||||
|
||||
logout: async () => {
|
||||
const refreshToken = localStorage.getItem('refreshToken');
|
||||
if (refreshToken) {
|
||||
await api.post('/api/v1/accounts/logout', { refreshToken });
|
||||
}
|
||||
localStorage.removeItem('accessToken');
|
||||
localStorage.removeItem('refreshToken');
|
||||
},
|
||||
|
||||
getMe: async () => {
|
||||
const response = await api.get('/api/v1/users/me');
|
||||
return response.data;
|
||||
}
|
||||
};
|
||||
40
src/services/dashboardService.ts
Normal file
40
src/services/dashboardService.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { api } from './api';
|
||||
|
||||
export const dashboardService = {
|
||||
getUsers: async (offset = 0, limit = 10, search = '') => {
|
||||
const response = await api.get('/api/v1/users', {
|
||||
params: { 'Pagination.Offset': offset, 'Pagination.Limit': limit, Search: search }
|
||||
});
|
||||
return response.data;
|
||||
},
|
||||
|
||||
updateUserBlockStatus: async (userId: string, isBlocked: boolean) => {
|
||||
const response = await api.put(`/api/v1/users/${userId}/block-status`, { isBlocked });
|
||||
return response.data;
|
||||
},
|
||||
|
||||
promoteUserToAdmin: async (userId: string) => {
|
||||
const response = await api.post(`/api/v1/users/${userId}/promote-to-admin`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
getInvoices: async (offset = 0, limit = 10) => {
|
||||
const response = await api.get('/api/v1/subscriptions/invoices', {
|
||||
params: { 'Pagination.Offset': offset, 'Pagination.Limit': limit }
|
||||
});
|
||||
return response.data;
|
||||
},
|
||||
|
||||
getSubscriptions: async (offset = 0, limit = 10, isActive: boolean | null = null) => {
|
||||
const params: any = { 'Pagination.Offset': offset, 'Pagination.Limit': limit };
|
||||
if (isActive !== null) params.IsActive = isActive;
|
||||
|
||||
const response = await api.get('/api/v1/subscriptions/user-subscriptions', { params });
|
||||
return response.data;
|
||||
},
|
||||
|
||||
getPackages: async () => {
|
||||
const response = await api.get('/api/v1/subscriptions/packages');
|
||||
return response.data;
|
||||
}
|
||||
};
|
||||
149
src/services/exerciseService.ts
Normal file
149
src/services/exerciseService.ts
Normal file
@@ -0,0 +1,149 @@
|
||||
import { api } from './api';
|
||||
|
||||
type MasterEntityKey =
|
||||
| 'tags'
|
||||
| 'safety-levels'
|
||||
| 'muscles'
|
||||
| 'muscle-groups'
|
||||
| 'metric-types'
|
||||
| 'locations'
|
||||
| 'exercise-types'
|
||||
| 'exercise-synonyms'
|
||||
| 'equipments'
|
||||
| 'difficulty-levels';
|
||||
|
||||
const masterPaths: Record<MasterEntityKey, string> = {
|
||||
'tags': '/api/v1/tags',
|
||||
'safety-levels': '/api/v1/safety-levels',
|
||||
'muscles': '/api/v1/muscles',
|
||||
'muscle-groups': '/api/v1/muscle-groups',
|
||||
'metric-types': '/api/v1/metric-types',
|
||||
'locations': '/api/v1/locations',
|
||||
'exercise-types': '/api/v1/exercise-types',
|
||||
'exercise-synonyms': '/api/v1/exercise-synonyms',
|
||||
'equipments': '/api/v1/equipments',
|
||||
'difficulty-levels': '/api/v1/difficulty-levels',
|
||||
};
|
||||
|
||||
export const exerciseService = {
|
||||
getExercises: async (offset = 0, limit = 10, search = '') => {
|
||||
const response = await api.get('/api/v1/exercises', {
|
||||
params: {
|
||||
'Pagination.Offset': offset,
|
||||
'Pagination.Limit': limit,
|
||||
Search: search,
|
||||
},
|
||||
});
|
||||
return response.data;
|
||||
},
|
||||
|
||||
getExerciseByPublicId: async (publicId: string) => {
|
||||
const response = await api.get(`/api/v1/exercises/${publicId}`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
createExercise: async (payload: {
|
||||
title: string;
|
||||
description: string;
|
||||
instructions: string;
|
||||
difficultyLevelId: number | string;
|
||||
exerciseTypeId: number | string;
|
||||
safetyLevelId: number | string;
|
||||
}) => {
|
||||
const response = await api.post('/api/v1/exercises', payload);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
getMasterList: async (entity: MasterEntityKey, offset = 0, limit = 50, search = '') => {
|
||||
const response = await api.get(masterPaths[entity], {
|
||||
params: {
|
||||
'Pagination.Offset': offset,
|
||||
'Pagination.Limit': limit,
|
||||
Search: search,
|
||||
},
|
||||
});
|
||||
return response.data;
|
||||
},
|
||||
|
||||
createMasterItem: async (entity: MasterEntityKey, payload: Record<string, unknown>) => {
|
||||
const response = await api.post(masterPaths[entity], payload);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
updateMasterItem: async (entity: MasterEntityKey, payload: Record<string, unknown>) => {
|
||||
const response = await api.put(masterPaths[entity], payload);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
deleteMasterItem: async (entity: MasterEntityKey, id: number | string) => {
|
||||
const response = await api.delete(`${masterPaths[entity]}/${id}`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
addExerciseMuscle: async (exerciseId: number | string, muscleId: number | string, isPrimary: boolean) => {
|
||||
const response = await api.post(`/api/v1/exercises/${exerciseId}/muscles`, { exerciseId, muscleId, isPrimary });
|
||||
return response.data;
|
||||
},
|
||||
|
||||
deleteExerciseMuscle: async (exerciseId: number | string, muscleId: number | string) => {
|
||||
const response = await api.delete(`/api/v1/exercises/${exerciseId}/muscles/${muscleId}`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
addExerciseMetric: async (exerciseId: number | string, metricTypeId: number | string, isPrimary: boolean) => {
|
||||
const response = await api.post(`/api/v1/exercises/${exerciseId}/metrics`, { exerciseId, metricTypeId, isPrimary });
|
||||
return response.data;
|
||||
},
|
||||
|
||||
deleteExerciseMetric: async (exerciseId: number | string, metricTypeId: number | string) => {
|
||||
const response = await api.delete(`/api/v1/exercises/${exerciseId}/metrics/${metricTypeId}`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
addExerciseTag: async (exerciseId: number | string, tagId: number | string) => {
|
||||
const response = await api.post(`/api/v1/exercises/${exerciseId}/tags`, { exerciseId, tagId });
|
||||
return response.data;
|
||||
},
|
||||
|
||||
deleteExerciseTag: async (exerciseId: number | string, tagId: number | string) => {
|
||||
const response = await api.delete(`/api/v1/exercises/${exerciseId}/tags/${tagId}`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
addExerciseLocation: async (exerciseId: number | string, locationId: number | string) => {
|
||||
const response = await api.post(`/api/v1/exercises/${exerciseId}/locations`, { exerciseId, locationId });
|
||||
return response.data;
|
||||
},
|
||||
|
||||
deleteExerciseLocation: async (exerciseId: number | string, locationId: number | string) => {
|
||||
const response = await api.delete(`/api/v1/exercises/${exerciseId}/locations/${locationId}`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
addExerciseEquipment: async (exerciseId: number | string, equipmentId: number | string) => {
|
||||
const response = await api.post(`/api/v1/exercises/${exerciseId}/equipments`, { exerciseId, equipmentId });
|
||||
return response.data;
|
||||
},
|
||||
|
||||
deleteExerciseEquipment: async (exerciseId: number | string, equipmentId: number | string) => {
|
||||
const response = await api.delete(`/api/v1/exercises/${exerciseId}/equipments/${equipmentId}`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
addExerciseSynonym: async (exerciseId: number | string, synonym: string, languageCode: string) => {
|
||||
const response = await api.post('/api/v1/exercise-synonyms', { exerciseId, synonym, languageCode });
|
||||
return response.data;
|
||||
},
|
||||
|
||||
updateExerciseSynonym: async (id: number | string, synonym: string, languageCode: string) => {
|
||||
const response = await api.put('/api/v1/exercise-synonyms', { id, synonym, languageCode });
|
||||
return response.data;
|
||||
},
|
||||
|
||||
deleteExerciseSynonym: async (id: number | string) => {
|
||||
const response = await api.delete(`/api/v1/exercise-synonyms/${id}`);
|
||||
return response.data;
|
||||
},
|
||||
};
|
||||
|
||||
export type { MasterEntityKey };
|
||||
1
src/vite-env.d.ts
vendored
Normal file
1
src/vite-env.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/// <reference types="vite/client" />
|
||||
26
tsconfig.json
Normal file
26
tsconfig.json
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"experimentalDecorators": true,
|
||||
"useDefineForClassFields": false,
|
||||
"module": "ESNext",
|
||||
"lib": [
|
||||
"ES2022",
|
||||
"DOM",
|
||||
"DOM.Iterable"
|
||||
],
|
||||
"skipLibCheck": true,
|
||||
"moduleResolution": "bundler",
|
||||
"isolatedModules": true,
|
||||
"moduleDetection": "force",
|
||||
"allowJs": true,
|
||||
"jsx": "react-jsx",
|
||||
"paths": {
|
||||
"@/*": [
|
||||
"./*"
|
||||
]
|
||||
},
|
||||
"allowImportingTsExtensions": true,
|
||||
"noEmit": true
|
||||
}
|
||||
}
|
||||
24
vite.config.ts
Normal file
24
vite.config.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import tailwindcss from '@tailwindcss/vite';
|
||||
import react from '@vitejs/plugin-react';
|
||||
import path from 'path';
|
||||
import {defineConfig, loadEnv} from 'vite';
|
||||
|
||||
export default defineConfig(({mode}) => {
|
||||
const env = loadEnv(mode, '.', '');
|
||||
return {
|
||||
plugins: [react(), tailwindcss()],
|
||||
define: {
|
||||
'process.env.GEMINI_API_KEY': JSON.stringify(env.GEMINI_API_KEY),
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': path.resolve(__dirname, '.'),
|
||||
},
|
||||
},
|
||||
server: {
|
||||
// HMR is disabled in AI Studio via DISABLE_HMR env var.
|
||||
// Do not modifyâfile watching is disabled to prevent flickering during agent edits.
|
||||
hmr: process.env.DISABLE_HMR !== 'true',
|
||||
},
|
||||
};
|
||||
});
|
||||
Reference in New Issue
Block a user