- Store opens with two choices (subscription / question packs); 3- and 6-month cards show per-month price and saving. - Units that need a subscription show a crown badge and open the store; a 402 from the quiz goes there too. «ادامه بده» skips locked units. - Profile: subscription card with an animated days-left bar (planDurationDays). - BrandMark (SVG) on splash, home header and footer; copyright footer; floating math glyphs on splash. CSS transform/opacity only; honours reduced motion. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
59 lines
2.4 KiB
TypeScript
59 lines
2.4 KiB
TypeScript
import type { HomeNode, HomeResponse, HomeUnit, NodeStatus } from '@/api';
|
|
|
|
/** رنگِ فصلها؛ سرور برای فصل رنگ نمیدهد، پس چرخشی انتخاب میشود. */
|
|
const UNIT_PALETTE = ['#ED6956', '#91ADC6', '#97C0C0', '#9B7BFF', '#FF7A3D', '#F3A9B0', '#4C5FD5', '#5FBF8F', '#E06BAE'];
|
|
|
|
export const unitColor = (index: number) => UNIT_PALETTE[((index % UNIT_PALETTE.length) + UNIT_PALETTE.length) % UNIT_PALETTE.length];
|
|
|
|
export function unitProgress(unit: HomeUnit) {
|
|
const total = unit.nodes.length;
|
|
const done = unit.nodes.filter((n) => n.status === 'completed').length;
|
|
return { done, total, percent: total ? Math.round((done / total) * 100) : 0 };
|
|
}
|
|
|
|
export const isPlayable = (status: NodeStatus) => status !== 'locked';
|
|
|
|
export interface NodeRef {
|
|
unit: HomeUnit;
|
|
unitIndex: number;
|
|
node: HomeNode;
|
|
}
|
|
|
|
/** اولین جزیرهی بازِ ناتمام در کلِ نقشه؛ برای «ادامه بده». */
|
|
export function findContinue(home: HomeResponse | null): NodeRef | null {
|
|
if (!home) return null;
|
|
for (let i = 0; i < home.units.length; i++) {
|
|
if (home.units[i].requiresSubscription) continue;
|
|
const node = home.units[i].nodes.find((n) => n.status !== 'completed' && n.status !== 'locked');
|
|
if (node) return { unit: home.units[i], unitIndex: i, node };
|
|
}
|
|
return null;
|
|
}
|
|
|
|
export function findNode(home: HomeResponse | null, nodeId: string | undefined): NodeRef | null {
|
|
if (!home || !nodeId) return null;
|
|
for (let i = 0; i < home.units.length; i++) {
|
|
const node = home.units[i].nodes.find((n) => n.id === nodeId);
|
|
if (node) return { unit: home.units[i], unitIndex: i, node };
|
|
}
|
|
return null;
|
|
}
|
|
|
|
export function findUnit(home: HomeResponse | null, unitId: string) {
|
|
const index = home?.units.findIndex((u) => u.id === unitId) ?? -1;
|
|
return index >= 0 ? { unit: home!.units[index], unitIndex: index } : null;
|
|
}
|
|
|
|
/** جزیرهای که این درس در آن است. */
|
|
export function findNodeOfLesson(home: HomeResponse | null, lessonId: number): NodeRef | null {
|
|
if (!home) return null;
|
|
for (let i = 0; i < home.units.length; i++) {
|
|
const node = home.units[i].nodes.find((n) => n.lessonIds.includes(lessonId));
|
|
if (node) return { unit: home.units[i], unitIndex: i, node };
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/** درسی که با زدنِ جزیره باز میشود. */
|
|
export const nextLessonOf = (node: HomeNode): number | null => node.nextLessonId ?? node.lessonIds[0] ?? null;
|