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