forked from Tadrisino/MathApp_frontend
Compare commits
6 Commits
zareian/ap
...
zareian/ap
| Author | SHA1 | Date | |
|---|---|---|---|
| 21cce5ea2b | |||
| 007c450c0a | |||
| 7dd457d2fc | |||
| 4705925022 | |||
| 82c9e870b3 | |||
| 8ac87e40de |
@@ -35,12 +35,29 @@ import { ConnectionCheck } from '@/api/ConnectionCheck';
|
|||||||
```tsx
|
```tsx
|
||||||
import { api, ApiError } from '@/api';
|
import { api, ApiError } from '@/api';
|
||||||
|
|
||||||
const grades = await api.grades();
|
const grades = await api.grades(); // lessonCount=0 یعنی پایه خالی است
|
||||||
const home = await api.home(); // پایهی خودِ کاربر
|
const home = await api.home(); // بدونِ آرگومان: پایهی خودِ کاربر
|
||||||
const intro = await api.lessons.intro(lessonId);
|
const steps = await api.nodes.lessons(nodeId); // کارتهای نقشهی مهارت
|
||||||
|
const intro = await api.lessons.intro(lessonId); // توضیح + مثال + گامبهگام
|
||||||
const answer = await api.ai.ask('این رو نفهمیدم', { lessonId });
|
const answer = await api.ai.ask('این رو نفهمیدم', { lessonId });
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### چند نکته که وقت میگیرند اگر ندانید
|
||||||
|
|
||||||
|
- `grades()` حالا `lessonCount` میدهد. صفر یعنی آن پایه هنوز محتوایی ندارد —
|
||||||
|
گزینهاش را غیرفعال کنید، وگرنه کاربر انتخابش میکند و صفحهی خالی میبیند.
|
||||||
|
- `home()` در هر مبحث `icon` و `color` و در هر فصل `blurb` میدهد؛ لازم نیست
|
||||||
|
رنگها را هاردکد کنید.
|
||||||
|
- `home()` فقط شناسهی درسها را میدهد. برای کارتهای نقشهی مهارت (عنوان،
|
||||||
|
آزمون یا ویدیو، «۵ سوال»/«۱۲ دقیقه»، انجامشده) از `nodes.lessons(nodeId)`
|
||||||
|
استفاده کنید — یک درخواست برای کلِ مبحث، نه یکی بهازای هر کارت.
|
||||||
|
- `lessons.intro()` متن را سهتکه میدهد: `introText` (توضیح)، `example`، و
|
||||||
|
`steps` (حلِ گامبهگام). `kind` میگوید آزمون است یا ویدیو.
|
||||||
|
- `plans()` حالا `features` و `badge` میدهد — بندهای کارتِ اشتراک و برچسبِ
|
||||||
|
بالایش دیگر لازم نیست در کد بمانند؛ عوض کردنشان به دیپلویِ تازه نیاز ندارد.
|
||||||
|
- `profile.update({ age })` هم کار میکند؛ سرور خودش به سالِ تولدِ شمسی تبدیل
|
||||||
|
میکند. `age` و `birthYear` را با هم نفرستید.
|
||||||
|
|
||||||
### ورود
|
### ورود
|
||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
|
|||||||
@@ -14,14 +14,29 @@
|
|||||||
import { ApiError, createHttpClient, localTokenStorage } from './client';
|
import { ApiError, createHttpClient, localTokenStorage } from './client';
|
||||||
import type { ClientOptions, HttpClient, RequestOptions, Tokens } from './client';
|
import type { ClientOptions, HttpClient, RequestOptions, Tokens } from './client';
|
||||||
import type {
|
import type {
|
||||||
AnswerResponse, AskResponse, Grade, HomeResponse, LessonIntro, Plan, Question,
|
AnswerResponse, AskResponse, Grade, HomeResponse, LessonIntro, NodeLessons, Plan,
|
||||||
ReportReason, ReportResponse, UserProfile,
|
Question, ReportReason, ReportResponse, UserProfile,
|
||||||
} from './types';
|
} from './types';
|
||||||
|
|
||||||
export { ApiError, localTokenStorage, createHttpClient };
|
export { ApiError, localTokenStorage, createHttpClient };
|
||||||
export type { Tokens, HttpClient, ClientOptions, RequestOptions };
|
export type { Tokens, HttpClient, ClientOptions, RequestOptions };
|
||||||
export * from './types';
|
export * from './types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* آدرسِ کاملِ یک مسیرِ API.
|
||||||
|
*
|
||||||
|
* fetch(apiUrl('/auth/login'), …)
|
||||||
|
*
|
||||||
|
* برای کدی که هنوز مستقیم fetch میزند. مسیرِ نسبی (`/api/auth/login`) در
|
||||||
|
* بیلدِ نهایی به دامنهی خودِ فرانت میرود، nginx چون مسیر را نمیشناسد
|
||||||
|
* index.html برمیگرداند، و نتیجه پاسخِ ۲۰۰ با بدنهی HTML است که موقعِ
|
||||||
|
* res.json() با «Unexpected token '<'» میشکند.
|
||||||
|
*/
|
||||||
|
export function apiUrl(path: string): string {
|
||||||
|
const base = createHttpClient().baseUrl;
|
||||||
|
return `${base}${path.startsWith('/') ? path : `/${path}`}`;
|
||||||
|
}
|
||||||
|
|
||||||
/** پرسش از دستیار کند است (تولیدِ متن)؛ ۱۵ ثانیهی پیشفرض کم است. */
|
/** پرسش از دستیار کند است (تولیدِ متن)؛ ۱۵ ثانیهی پیشفرض کم است. */
|
||||||
const ASK_TIMEOUT_MS = 45_000;
|
const ASK_TIMEOUT_MS = 45_000;
|
||||||
|
|
||||||
@@ -66,8 +81,13 @@ export function createApi(options: ClientOptions = {}) {
|
|||||||
/* ── کاربر ────────────────────────────────────────────────────────── */
|
/* ── کاربر ────────────────────────────────────────────────────────── */
|
||||||
profile: {
|
profile: {
|
||||||
get: (opts?: RequestOptions) => http.get<UserProfile>('/user/profile', opts),
|
get: (opts?: RequestOptions) => http.get<UserProfile>('/user/profile', opts),
|
||||||
|
/**
|
||||||
|
* فقط فیلدهایی که میفرستید عوض میشوند. صفحهی ثبتنام سن میپرسد، پس
|
||||||
|
* `age` هم پذیرفته میشود و سرور خودش به سالِ تولدِ شمسی تبدیلش میکند —
|
||||||
|
* `age` و `birthYear` را با هم نفرستید.
|
||||||
|
*/
|
||||||
update: (patch: Partial<Pick<UserProfile,
|
update: (patch: Partial<Pick<UserProfile,
|
||||||
'name' | 'avatar' | 'birthYear' | 'parentPhone' | 'bio'>>) =>
|
'name' | 'avatar' | 'birthYear' | 'parentPhone' | 'bio'>> & { age?: number }) =>
|
||||||
http.patch<UserProfile>('/user/profile', patch),
|
http.patch<UserProfile>('/user/profile', patch),
|
||||||
setGrade: (gradeId: number) =>
|
setGrade: (gradeId: number) =>
|
||||||
http.post<{ success: boolean; gradeId: number }>('/user/grade', { gradeId }),
|
http.post<{ success: boolean; gradeId: number }>('/user/grade', { gradeId }),
|
||||||
@@ -96,6 +116,16 @@ export function createApi(options: ClientOptions = {}) {
|
|||||||
http.create<Record<string, unknown>>(`/lessons/${lessonId}/complete`, {}),
|
http.create<Record<string, unknown>>(`/lessons/${lessonId}/complete`, {}),
|
||||||
},
|
},
|
||||||
|
|
||||||
|
nodes: {
|
||||||
|
/**
|
||||||
|
* قدمهای یک مبحث برای نقشهی مهارت — عنوان، نوع، زیرنویس و «انجام شد».
|
||||||
|
* `home()` فقط شناسهی درسها را میدهد؛ بدونِ این، برای هر کارت یک
|
||||||
|
* درخواستِ intro جدا لازم بود.
|
||||||
|
*/
|
||||||
|
lessons: (nodeId: number | string, opts?: RequestOptions) =>
|
||||||
|
http.get<NodeLessons>(`/nodes/${nodeId}/lessons`, opts),
|
||||||
|
},
|
||||||
|
|
||||||
questions: {
|
questions: {
|
||||||
/**
|
/**
|
||||||
* ثبتِ پاسخ. مسیر زیرِ /questions است نه /lessons، و نامِ فیلد
|
* ثبتِ پاسخ. مسیر زیرِ /questions است نه /lessons، و نامِ فیلد
|
||||||
|
|||||||
@@ -12,6 +12,9 @@ export type NodeStatusValue = 'locked' | 'unlocked' | 'in_progress' | 'completed
|
|||||||
export interface HomeNode {
|
export interface HomeNode {
|
||||||
id: string;
|
id: string;
|
||||||
title: string;
|
title: string;
|
||||||
|
/** آیکون و رنگِ کارتِ مبحث — از سرور میآید، هاردکد نکنید. */
|
||||||
|
icon: string;
|
||||||
|
color: string;
|
||||||
status: NodeStatusValue;
|
status: NodeStatusValue;
|
||||||
lessonIds: number[];
|
lessonIds: number[];
|
||||||
}
|
}
|
||||||
@@ -19,6 +22,8 @@ export interface HomeNode {
|
|||||||
export interface HomeUnit {
|
export interface HomeUnit {
|
||||||
id: string;
|
id: string;
|
||||||
title: string;
|
title: string;
|
||||||
|
/** یک جملهی کوتاه زیرِ عنوانِ فصل. */
|
||||||
|
blurb: string;
|
||||||
nodes: HomeNode[];
|
nodes: HomeNode[];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -38,6 +43,8 @@ export interface Grade {
|
|||||||
title: string;
|
title: string;
|
||||||
icon: string;
|
icon: string;
|
||||||
color: string;
|
color: string;
|
||||||
|
/** صفر یعنی این پایه هنوز محتوایی ندارد — گزینهاش را غیرفعال کنید. */
|
||||||
|
lessonCount: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** آمارِ بازی (xp/سکه/استریک) اینجا نیست — از /user/summary میآید. */
|
/** آمارِ بازی (xp/سکه/استریک) اینجا نیست — از /user/summary میآید. */
|
||||||
@@ -53,14 +60,40 @@ export interface UserProfile {
|
|||||||
bio: string;
|
bio: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** یک قدم در مسیر: آزمون یا ویدیو. */
|
||||||
|
export type LessonKind = 'quiz' | 'video';
|
||||||
|
|
||||||
export interface LessonIntro {
|
export interface LessonIntro {
|
||||||
lessonId: string;
|
lessonId: string;
|
||||||
title: string;
|
title: string;
|
||||||
|
kind: LessonKind;
|
||||||
|
/** متنِ درس سه تکه است: توضیح، مثال، و حلِ گامبهگام. */
|
||||||
introText: string;
|
introText: string;
|
||||||
|
example: string;
|
||||||
|
steps: string[];
|
||||||
|
/** فقط وقتی kind === 'video' پر است. */
|
||||||
|
videoUrl: string;
|
||||||
|
durationMinutes: number | null;
|
||||||
mascotAnimation: string;
|
mascotAnimation: string;
|
||||||
questionCount: number;
|
questionCount: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** یک کارت در نقشهی مهارت. */
|
||||||
|
export interface NodeLesson {
|
||||||
|
id: string;
|
||||||
|
kind: LessonKind;
|
||||||
|
title: string;
|
||||||
|
/** زیرنویسِ کارت: «۵ سوال» یا «۱۲ دقیقه». */
|
||||||
|
meta: string;
|
||||||
|
done: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface NodeLessons {
|
||||||
|
nodeId: string;
|
||||||
|
title: string;
|
||||||
|
lessons: NodeLesson[];
|
||||||
|
}
|
||||||
|
|
||||||
export interface ChoiceOption {
|
export interface ChoiceOption {
|
||||||
id: string;
|
id: string;
|
||||||
value: string;
|
value: string;
|
||||||
@@ -109,6 +142,10 @@ export interface Plan {
|
|||||||
popular: boolean;
|
popular: boolean;
|
||||||
discountPercent: number;
|
discountPercent: number;
|
||||||
durationDays: number;
|
durationDays: number;
|
||||||
|
/** بندهای زیرِ کارتِ پلن — از سرور میآید، دیگر لازم نیست در کد بماند. */
|
||||||
|
features: string[];
|
||||||
|
/** برچسبِ بالای کارت: «محبوبترین»، «بهترین ارزش». خالی یعنی بدونِ برچسب. */
|
||||||
|
badge: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ReportReason =
|
export type ReportReason =
|
||||||
|
|||||||
2195
src/app/App.tsx
2195
src/app/App.tsx
File diff suppressed because it is too large
Load Diff
@@ -31,6 +31,24 @@ export default defineConfig({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// در حالتِ توسعه، هر درخواستِ /api به بکاند فرستاده میشود.
|
||||||
|
//
|
||||||
|
// چرا لازم است: کدِ اپ مسیرهای نسبی صدا میزند (مثلاً /api/auth/login). بدونِ
|
||||||
|
// این پروکسی، آن درخواست به خودِ سرورِ vite میرود و چون مسیرِ ناشناخته است
|
||||||
|
// index.html برمیگردد — یعنی پاسخِ ۲۰۰ ولی HTML، و کد موقعِ res.json() با
|
||||||
|
// «Unexpected token '<'» میشکند. خطایی که علتش از پیامش معلوم نیست.
|
||||||
|
//
|
||||||
|
// مقصد را با VITE_API_PROXY_TARGET عوض کنید (پیشفرض: بکاندِ مشترک).
|
||||||
|
server: {
|
||||||
|
proxy: {
|
||||||
|
'/api': {
|
||||||
|
target: process.env.VITE_API_PROXY_TARGET || 'https://api.math.tadrisino2.ir',
|
||||||
|
changeOrigin: true,
|
||||||
|
secure: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
// File types to support raw imports. Never add .css, .tsx, or .ts files to this.
|
// File types to support raw imports. Never add .css, .tsx, or .ts files to this.
|
||||||
assetsInclude: ['**/*.svg', '**/*.csv'],
|
assetsInclude: ['**/*.svg', '**/*.csv'],
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user