forked from Tadrisino/MathApp_frontend
Colors keep their role: headers/banners darken to readable contrast (same hue), lessons uses the teal token, packages use indigo; +XP pop, streak milestone card, lazy confetti
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -8,7 +8,7 @@ import { SeyarMascot } from '@/mascots/SeyarMascot';
|
||||
import { IslandPath } from '@/components/IslandPath';
|
||||
import { IslandSheet } from '@/components/IslandSheet';
|
||||
import { EmptyState, ErrorState, LoadingState, Screen, Sep } from '@/components/ui';
|
||||
import { shade } from '@/lib/color';
|
||||
import { onWhite, shade } from '@/lib/color';
|
||||
import { toFa } from '@/lib/format';
|
||||
import { MathText } from '@/lib/MathText';
|
||||
|
||||
@@ -106,7 +106,7 @@ export function HomeScreen() {
|
||||
type="button"
|
||||
onClick={() => nav.push({ name: 'unit', unitId: unit.id })}
|
||||
className="flex w-full items-center gap-3 rounded-2xl px-4 py-3 text-right text-white active:translate-y-0.5"
|
||||
style={{ backgroundColor: color, boxShadow: `0 4px 0 ${shade(color, -0.25)}` }}
|
||||
style={{ backgroundColor: onWhite(color), boxShadow: `0 4px 0 ${shade(onWhite(color), -0.25)}` }}
|
||||
>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="text-[11px] font-bold text-white/80">
|
||||
|
||||
@@ -4,6 +4,7 @@ import { Brain, Check, ChevronDown, Lock, Play, Sparkles } from 'lucide-react';
|
||||
import { api, type HomeNode, type HomeUnit } from '@/api';
|
||||
import { useAppData } from '@/state/AppData';
|
||||
import { findContinue, unitColor, unitProgress } from '@/state/curriculum';
|
||||
import { onWhite } from '@/lib/color';
|
||||
import { useNav } from '@/nav/Navigator';
|
||||
import { useAsync } from '@/lib/useAsync';
|
||||
import { EmptyState, ErrorState, LoadingState, ProgressBar, Screen, ScreenHeader, Sep } from '@/components/ui';
|
||||
@@ -36,7 +37,7 @@ export function LessonsScreen() {
|
||||
<ScreenHeader
|
||||
title="درسها"
|
||||
subtitle={<>{gradeTitle && <>{gradeTitle}<Sep /></>}اول بخون، بعد آزمونِ جزیره رو بده</>}
|
||||
color="#2FB36B"
|
||||
color="#97C0C0"
|
||||
mascot={{ char: 'sigma', mood: 'happy' }}
|
||||
>
|
||||
{home && totalNodes > 0 && (
|
||||
@@ -62,7 +63,7 @@ export function LessonsScreen() {
|
||||
type="button"
|
||||
onClick={() => nav.push({ name: 'lesson', lessonId: current.node.nextLessonId as number, nodeId: current.node.id })}
|
||||
className="flex items-center gap-3 rounded-2xl px-4 py-3.5 text-right text-white"
|
||||
style={{ backgroundColor: unitColor(current.unitIndex), boxShadow: `0 4px 0 ${unitColor(current.unitIndex)}99` }}
|
||||
style={{ backgroundColor: onWhite(unitColor(current.unitIndex)), boxShadow: `0 4px 0 ${onWhite(unitColor(current.unitIndex))}99` }}
|
||||
>
|
||||
<span className="flex h-11 w-11 flex-shrink-0 items-center justify-center rounded-full bg-white/25">
|
||||
<Play size={20} fill="white" />
|
||||
@@ -181,7 +182,7 @@ function NodeRow({
|
||||
>
|
||||
<span
|
||||
className="flex h-7 w-7 flex-shrink-0 items-center justify-center rounded-full text-xs font-black"
|
||||
style={done ? { backgroundColor: '#5FBF8F', color: '#fff' } : { backgroundColor: locked ? '#f0eeff' : `${color}22`, color: locked ? '#727299' : color }}
|
||||
style={done ? { backgroundColor: '#3E9E73', color: '#fff' } : { backgroundColor: locked ? '#f0eeff' : `${color}22`, color: locked ? '#727299' : color }}
|
||||
>
|
||||
{done ? <Check size={15} strokeWidth={3.5} /> : locked ? <Lock size={13} /> : toFa(num)}
|
||||
</span>
|
||||
@@ -219,7 +220,7 @@ function NodeRow({
|
||||
>
|
||||
<span
|
||||
className="flex h-6 w-6 flex-shrink-0 items-center justify-center rounded-full text-[11px] font-black"
|
||||
style={l.done ? { backgroundColor: '#5FBF8F', color: '#fff' } : { backgroundColor: '#fff', color: '#727299', border: '1px solid #e9e6f7' }}
|
||||
style={l.done ? { backgroundColor: '#3E9E73', color: '#fff' } : { backgroundColor: '#fff', color: '#727299', border: '1px solid #e9e6f7' }}
|
||||
>
|
||||
{l.done ? <Check size={13} strokeWidth={3.5} /> : toFa(li + 1)}
|
||||
</span>
|
||||
@@ -232,7 +233,7 @@ function NodeRow({
|
||||
type="button"
|
||||
onClick={() => nav.push({ name: 'quiz', nodeId: node.id })}
|
||||
className="mt-2.5 flex w-full items-center justify-center gap-2 rounded-xl py-3 text-sm font-black text-white active:translate-y-[2px]"
|
||||
style={{ backgroundColor: color, boxShadow: `0 3px 0 ${color}99` }}
|
||||
style={{ backgroundColor: onWhite(color), boxShadow: `0 3px 0 ${onWhite(color)}99` }}
|
||||
>
|
||||
{done ? <Sparkles size={15} /> : <Play size={15} fill="white" />}
|
||||
{done ? 'دوباره تمرین کن' : 'درسها رو خوندی؟ برو آزمون'}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { AnimatePresence, motion } from 'motion/react';
|
||||
import confetti from 'canvas-confetti';
|
||||
import { Brain, Check, ChevronLeft, Coins, Flag, Heart, Infinity as InfinityIcon, Star, X } from 'lucide-react';
|
||||
import { api, ApiError, type AnswerResult, type Hearts, type Question, type ReportReason } from '@/api';
|
||||
import { useAppData } from '@/state/AppData';
|
||||
@@ -165,7 +164,7 @@ export function QuizScreen({ lessonId, nodeId }: { lessonId?: number; nodeId?: s
|
||||
: await api.lesson.complete(lessonId, payload, completeKey.current);
|
||||
setPhase({ kind: 'result', result });
|
||||
sounds.finish();
|
||||
if (result.stars >= 2) void confetti({ particleCount: 90, spread: 70, origin: { y: 0.35 } });
|
||||
if (result.stars >= 2) void import('canvas-confetti').then(({ default: confetti }) => confetti({ particleCount: 90, spread: 70, origin: { y: 0.35 } }));
|
||||
void refreshHome();
|
||||
void refreshSummary();
|
||||
} catch (error) {
|
||||
@@ -283,6 +282,17 @@ export function QuizScreen({ lessonId, nodeId }: { lessonId?: number; nodeId?: s
|
||||
!answer ? 'border-line bg-white text-indigo' : answer.correct ? 'border-green-200 bg-green-50 text-green-700' : 'border-purple-200 bg-purple-50 text-[#5B3FA0]'
|
||||
}`}>
|
||||
{bubble}
|
||||
{answer?.correct && answer.xpEarned > 0 && (
|
||||
<motion.span
|
||||
key={question.id}
|
||||
initial={{ opacity: 0, y: 8, scale: 0.8 }}
|
||||
animate={{ opacity: 1, y: 0, scale: 1 }}
|
||||
transition={{ type: 'spring', stiffness: 400, damping: 14 }}
|
||||
className="mr-2 inline-block rounded-full bg-[#FFF3C4] px-2 py-0.5 text-[11px] font-black text-[#8A5A00]"
|
||||
>
|
||||
+{toFa(answer.xpEarned)} ⭐
|
||||
</motion.span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -57,6 +57,8 @@ export function StreakScreen() {
|
||||
))}
|
||||
</div>
|
||||
|
||||
<StreakGoal current={calendar.data.currentStreak} />
|
||||
|
||||
<div className="rounded-3xl border border-line bg-white p-4">
|
||||
<div className="mb-2 grid grid-cols-7 gap-1.5 text-center text-[10px] font-bold text-muted">
|
||||
{WEEKDAYS.map((d) => <span key={d}>{d}</span>)}
|
||||
@@ -90,3 +92,24 @@ export function StreakScreen() {
|
||||
</Screen>
|
||||
);
|
||||
}
|
||||
|
||||
/** نقطهعطفهای زنجیره؛ فقط پیام و نوار، هیچ پاداشِ واقعیای پشتش نیست (پاداش تصمیمِ محصولی است). */
|
||||
const MILESTONES = [3, 7, 14, 30, 60, 100];
|
||||
|
||||
function StreakGoal({ current }: { current: number }) {
|
||||
const next = MILESTONES.find((m) => m > current);
|
||||
const reached = MILESTONES.filter((m) => m <= current).pop();
|
||||
return (
|
||||
<div className="rounded-3xl border border-line bg-[#FFF3E8] px-4 py-3">
|
||||
<p className="text-sm font-black leading-7 text-ink">
|
||||
{reached ? `🔥 ${toFa(current)} روزهای! ` : ''}
|
||||
{next ? `${toFa(next - current)} روز دیگه میشه زنجیرهی ${toFa(next)} روزه.` : 'از همهی نقطهعطفها رد شدی، افسانهای!'}
|
||||
</p>
|
||||
{next && (
|
||||
<div className="mt-2 h-2 overflow-hidden rounded-full bg-white">
|
||||
<div className="h-full rounded-full bg-orange transition-[width]" style={{ width: `${Math.round((current / next) * 100)}%` }} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -37,7 +37,7 @@ export function SubscriptionScreen() {
|
||||
return (
|
||||
<Screen>
|
||||
<div className="flex-1 overflow-y-auto">
|
||||
<ScreenHeader title="بستهی سوال" subtitle="هر وقت لازم داشتی، سوال بخر" color="#ED6956" mascot={{ char: 'pi', mood: 'excited', size: 60 }} />
|
||||
<ScreenHeader title="بستهی سوال" subtitle="هر وقت لازم داشتی، سوال بخر" color="#4C5FD5" mascot={{ char: 'pi', mood: 'excited', size: 60 }} />
|
||||
|
||||
<div className="flex flex-col gap-4 px-4 py-5">
|
||||
<div className="rounded-3xl border border-line bg-white p-4">
|
||||
|
||||
Reference in New Issue
Block a user