Compare commits
2 Commits
a341ffd314
...
d223336638
| Author | SHA1 | Date | |
|---|---|---|---|
| d223336638 | |||
| 15d110d824 |
290
src/app/App.tsx
290
src/app/App.tsx
@@ -6,7 +6,7 @@ import {
|
|||||||
Lock, Sparkles, MessageCircle, Check, X, Mic, Send,
|
Lock, Sparkles, MessageCircle, Check, X, Mic, Send,
|
||||||
ChevronLeft, ChevronRight, Zap, ChevronDown, AlertCircle,
|
ChevronLeft, ChevronRight, Zap, ChevronDown, AlertCircle,
|
||||||
Grid3x3, TrendingUp, LineChart, Sigma, Compass, Repeat,
|
Grid3x3, TrendingUp, LineChart, Sigma, Compass, Repeat,
|
||||||
Flame, Star, Trophy, Heart,
|
Flame, Star, Trophy, Heart, Users, FileText,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { createClient, ApiError } from "../api";
|
import { createClient, ApiError } from "../api";
|
||||||
import type {
|
import type {
|
||||||
@@ -300,7 +300,7 @@ function SeyarMascot({ char, mood = "idle", size = 100, bob = false }: { char: S
|
|||||||
// ─── Types ────────────────────────────────────────────────────────────────────
|
// ─── Types ────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
type AppFlow = "splash" | "onboarding" | "login" | "profile" | "main";
|
type AppFlow = "splash" | "onboarding" | "login" | "profile" | "main";
|
||||||
type MainScreen = "home" | "topics" | "skillmap" | "lessons" | "lesson" | "question" | "askai" | "subscription";
|
type MainScreen = "home" | "topics" | "skillmap" | "lessons" | "lesson" | "question" | "askai" | "subscription" | "notifications" | "leaderboard";
|
||||||
interface Profile {
|
interface Profile {
|
||||||
name: string;
|
name: string;
|
||||||
age: number;
|
age: number;
|
||||||
@@ -349,6 +349,19 @@ function mapMascotMood(server: string | undefined | null): SeyarMood {
|
|||||||
return "idle";
|
return "idle";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ─── اعلانها ─────────────────────────────────────────────────────────────────
|
||||||
|
interface NotificationItem { id: number; type: string; title: string; isRead: boolean }
|
||||||
|
|
||||||
|
const NOTIF_STYLE: Record<string, { icon: React.ElementType; color: string; bg: string }> = {
|
||||||
|
xp_earned: { icon: Star, color: "#E8A400", bg: "#FFF8E1" },
|
||||||
|
lesson_completed: { icon: Check, color: "#5FBF8F", bg: "#EAF8F0" },
|
||||||
|
order_paid: { icon: Crown, color: "#9B7BFF", bg: "#F5F1FF" },
|
||||||
|
ticket_replied: { icon: MessageCircle, color: "#4C5FD5", bg: "#EEF1FF" },
|
||||||
|
family_link_accepted: { icon: Users, color: "#FF7A3D", bg: "#FFF1E6" },
|
||||||
|
weekly_report_ready: { icon: FileText, color: "#91ADC6", bg: "#EAF2F6" },
|
||||||
|
};
|
||||||
|
const notifStyle = (type: string) => NOTIF_STYLE[type] ?? { icon: Bell, color: "#9e9ec0", bg: "#f0eeff" };
|
||||||
|
|
||||||
// ─── Bottom Nav ───────────────────────────────────────────────────────────────
|
// ─── Bottom Nav ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
function BottomNav({ active, onChange }: { active: MainScreen; onChange: (s: MainScreen) => void }) {
|
function BottomNav({ active, onChange }: { active: MainScreen; onChange: (s: MainScreen) => void }) {
|
||||||
@@ -1115,12 +1128,14 @@ function HomeScreen({
|
|||||||
profile,
|
profile,
|
||||||
home,
|
home,
|
||||||
summary,
|
summary,
|
||||||
|
unreadCount,
|
||||||
onNavigate,
|
onNavigate,
|
||||||
onContinue,
|
onContinue,
|
||||||
}: {
|
}: {
|
||||||
profile: Profile;
|
profile: Profile;
|
||||||
home: HomeResponse | null;
|
home: HomeResponse | null;
|
||||||
summary: { xp: number; coins: number; streak: { current: number; longest: number } } | null;
|
summary: { xp: number; coins: number; streak: { current: number; longest: number } } | null;
|
||||||
|
unreadCount: number;
|
||||||
onNavigate: (s: MainScreen) => void;
|
onNavigate: (s: MainScreen) => void;
|
||||||
onContinue: (unitId: string) => void;
|
onContinue: (unitId: string) => void;
|
||||||
}) {
|
}) {
|
||||||
@@ -1153,9 +1168,21 @@ function HomeScreen({
|
|||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<button className="w-10 h-10 rounded-full bg-white/15 flex items-center justify-center flex-shrink-0">
|
<div className="flex items-center gap-2 flex-shrink-0">
|
||||||
<Bell size={17}/>
|
<button onClick={() => onNavigate("leaderboard")}
|
||||||
</button>
|
className="w-10 h-10 rounded-full bg-white/15 flex items-center justify-center">
|
||||||
|
<Trophy size={17}/>
|
||||||
|
</button>
|
||||||
|
<button onClick={() => onNavigate("notifications")}
|
||||||
|
className="relative w-10 h-10 rounded-full bg-white/15 flex items-center justify-center">
|
||||||
|
<Bell size={17}/>
|
||||||
|
{unreadCount > 0 && (
|
||||||
|
<span className="absolute -top-0.5 -left-0.5 min-w-[16px] h-4 px-1 rounded-full bg-[#FFD23F] text-[#1a1a2e] text-[9px] font-black flex items-center justify-center">
|
||||||
|
{unreadCount > 9 ? "۹+" : String(unreadCount).replace(/\d/g, (d) => "۰۱۲۳۴۵۶۷۸۹"[Number(d)])}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -2409,6 +2436,212 @@ function AskAIScreen({ profile }: { profile: Profile }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ─── Notifications Screen ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
function NotificationsScreen({
|
||||||
|
items,
|
||||||
|
loading,
|
||||||
|
loadErr,
|
||||||
|
onBack,
|
||||||
|
onMarkRead,
|
||||||
|
onMarkAllRead,
|
||||||
|
}: {
|
||||||
|
items: NotificationItem[] | null;
|
||||||
|
loading: boolean;
|
||||||
|
loadErr: boolean;
|
||||||
|
onBack: () => void;
|
||||||
|
onMarkRead: (id: number) => void;
|
||||||
|
onMarkAllRead: () => void;
|
||||||
|
}) {
|
||||||
|
const unreadCount = items?.filter((n) => !n.isRead).length ?? 0;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col h-full bg-[#f0f4ff]" dir="rtl">
|
||||||
|
<div className="flex-shrink-0 bg-white px-5 pt-12 pb-4 border-b border-[#ede9ff]">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<button onClick={onBack}
|
||||||
|
className="w-8 h-8 rounded-full bg-[#f0eeff] flex items-center justify-center flex-shrink-0">
|
||||||
|
<ChevronRight size={17} className="text-[#ED6956]" strokeWidth={2.5}/>
|
||||||
|
</button>
|
||||||
|
<h1 className="flex-1 font-black text-[#1a1a2e] text-lg">اعلانها</h1>
|
||||||
|
{unreadCount > 0 && (
|
||||||
|
<button onClick={onMarkAllRead} className="text-xs text-[#4C5FD5] font-bold">
|
||||||
|
همه خوانده شد
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex-1 overflow-y-auto px-4 py-4">
|
||||||
|
{loading ? (
|
||||||
|
<div className="flex flex-col items-center justify-center gap-3 py-20">
|
||||||
|
<SeyarMascot char="sigma" mood="idle" size={64}/>
|
||||||
|
<p className="text-sm text-[#9e9ec0]">در حال آوردنِ اعلانها...</p>
|
||||||
|
</div>
|
||||||
|
) : loadErr ? (
|
||||||
|
<div className="flex flex-col items-center justify-center gap-3 py-20">
|
||||||
|
<SeyarMascot char="pi" mood="sad" size={64}/>
|
||||||
|
<p className="text-sm text-[#9e9ec0]">اعلانها الان نیومدن، بعداً دوباره امتحان کن.</p>
|
||||||
|
</div>
|
||||||
|
) : !items || items.length === 0 ? (
|
||||||
|
<div className="flex flex-col items-center justify-center gap-3 py-20">
|
||||||
|
<SeyarMascot char="pi" mood="idle" size={64}/>
|
||||||
|
<p className="text-sm text-[#9e9ec0]">فعلاً خبری نیست</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
{items.map((n) => {
|
||||||
|
const style = notifStyle(n.type);
|
||||||
|
const Icon = style.icon;
|
||||||
|
return (
|
||||||
|
<button key={n.id} onClick={() => !n.isRead && onMarkRead(n.id)}
|
||||||
|
className={`flex items-center gap-3 p-3.5 rounded-2xl text-right transition-colors ${
|
||||||
|
n.isRead ? "bg-white border border-[#ede9ff]" : "bg-[#EEF1FF] border border-[#4C5FD5]/20"
|
||||||
|
}`}>
|
||||||
|
<div className="w-10 h-10 rounded-full flex items-center justify-center flex-shrink-0" style={{ backgroundColor: style.bg }}>
|
||||||
|
<Icon size={18} style={{ color: style.color }}/>
|
||||||
|
</div>
|
||||||
|
<p className={`flex-1 text-sm ${n.isRead ? "text-[#9e9ec0]" : "text-[#1a1a2e] font-bold"}`}>{n.title}</p>
|
||||||
|
{!n.isRead && <span className="w-2 h-2 rounded-full bg-[#ED6956] flex-shrink-0"/>}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Leaderboard Screen ─────────────────────────────────────────────────────────
|
||||||
|
// توجه: کلاینتِ api.ts پارامترِ period رو "weekly" | "monthly" تایپ کرده، ولی
|
||||||
|
// بکاندِ واقعی فقط "weekly" | "all_time" رو قبول میکنه (سریالایزرِ خودشون رو
|
||||||
|
// دیدم). اینجا از مقادیرِ واقعیِ بکاند استفاده میکنیم.
|
||||||
|
|
||||||
|
interface LeaderboardEntry { rank: number; userId: string; name: string; xpInPeriod: number }
|
||||||
|
type LeaderboardPeriod = "weekly" | "all_time";
|
||||||
|
|
||||||
|
const RANK_MEDAL: Record<number, { bg: string; fg: string }> = {
|
||||||
|
1: { bg: "#FFD23F", fg: "#8a6d00" },
|
||||||
|
2: { bg: "#D9DEE8", fg: "#5a6270" },
|
||||||
|
3: { bg: "#E8B27A", fg: "#7a4a1a" },
|
||||||
|
};
|
||||||
|
|
||||||
|
function LeaderboardScreen({
|
||||||
|
entries,
|
||||||
|
period,
|
||||||
|
loading,
|
||||||
|
loadErr,
|
||||||
|
myUserId,
|
||||||
|
onChangePeriod,
|
||||||
|
onBack,
|
||||||
|
}: {
|
||||||
|
entries: LeaderboardEntry[] | null;
|
||||||
|
period: LeaderboardPeriod;
|
||||||
|
loading: boolean;
|
||||||
|
loadErr: boolean;
|
||||||
|
myUserId?: string;
|
||||||
|
onChangePeriod: (p: LeaderboardPeriod) => void;
|
||||||
|
onBack: () => void;
|
||||||
|
}) {
|
||||||
|
const toFa = (n: number) => String(n).replace(/\d/g, (d) => "۰۱۲۳۴۵۶۷۸۹"[Number(d)]);
|
||||||
|
const top3 = entries?.slice(0, 3) ?? [];
|
||||||
|
const rest = entries?.slice(3) ?? [];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col h-full bg-[#f0f4ff]" dir="rtl">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="flex-shrink-0 bg-[#FF7A3D] px-5 pt-12 pb-6 text-white rounded-b-[32px] relative overflow-hidden">
|
||||||
|
<div className="pointer-events-none absolute -left-10 -top-10 w-40 h-40 rounded-full bg-white/10"/>
|
||||||
|
<div className="relative z-10">
|
||||||
|
<div className="flex items-center gap-3 mb-3">
|
||||||
|
<button onClick={onBack} className="w-8 h-8 rounded-full bg-white/15 flex items-center justify-center flex-shrink-0">
|
||||||
|
<ChevronRight size={17} strokeWidth={2.5}/>
|
||||||
|
</button>
|
||||||
|
<h1 className="flex-1 font-black text-lg">جدولِ امتیازها</h1>
|
||||||
|
<SeyarMascot char="delta" mood={loading ? "idle" : "excited"} size={54}/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Period toggle */}
|
||||||
|
<div className="flex bg-white/15 rounded-2xl p-1 gap-1">
|
||||||
|
{([["weekly", "این هفته"], ["all_time", "کل زمان"]] as [LeaderboardPeriod, string][]).map(([p, label]) => (
|
||||||
|
<button key={p} onClick={() => onChangePeriod(p)}
|
||||||
|
className={`flex-1 py-2 rounded-xl text-xs font-bold transition-colors ${
|
||||||
|
period === p ? "bg-white text-[#FF7A3D]" : "text-white/80"
|
||||||
|
}`}>
|
||||||
|
{label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex-1 overflow-y-auto px-4 py-4">
|
||||||
|
{loading ? (
|
||||||
|
<div className="flex flex-col items-center justify-center gap-3 py-20">
|
||||||
|
<SeyarMascot char="delta" mood="idle" size={64}/>
|
||||||
|
<p className="text-sm text-[#9e9ec0]">در حال آوردنِ جدول...</p>
|
||||||
|
</div>
|
||||||
|
) : loadErr ? (
|
||||||
|
<div className="flex flex-col items-center justify-center gap-3 py-20">
|
||||||
|
<SeyarMascot char="pi" mood="sad" size={64}/>
|
||||||
|
<p className="text-sm text-[#9e9ec0]">جدول الان نیومد، بعداً دوباره امتحان کن.</p>
|
||||||
|
</div>
|
||||||
|
) : !entries || entries.length === 0 ? (
|
||||||
|
<div className="flex flex-col items-center justify-center gap-3 py-20">
|
||||||
|
<SeyarMascot char="pi" mood="idle" size={64}/>
|
||||||
|
<p className="text-sm text-[#9e9ec0]">هنوز کسی تو این جدول نیست</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{/* سکوی سه نفرِ برتر */}
|
||||||
|
{top3.length > 0 && (
|
||||||
|
<div className="flex items-end justify-center gap-2.5 mb-6 pt-4">
|
||||||
|
{[top3[1], top3[0], top3[2]].filter(Boolean).map((e) => {
|
||||||
|
const isMe = e.userId === myUserId;
|
||||||
|
const medal = RANK_MEDAL[e.rank];
|
||||||
|
const podiumH = e.rank === 1 ? 92 : e.rank === 2 ? 72 : 56;
|
||||||
|
return (
|
||||||
|
<div key={e.userId} className="flex flex-col items-center gap-1.5" style={{ width: 96 }}>
|
||||||
|
<div className={`w-14 h-14 rounded-full flex items-center justify-center font-black text-lg border-2 ${isMe ? "border-[#ED6956]" : "border-white"}`}
|
||||||
|
style={{ backgroundColor: medal.bg, color: medal.fg }}>
|
||||||
|
{toFa(e.rank)}
|
||||||
|
</div>
|
||||||
|
<p className={`text-xs font-bold text-center truncate w-full ${isMe ? "text-[#ED6956]" : "text-[#1a1a2e]"}`}>{isMe ? "تو" : e.name}</p>
|
||||||
|
<p className="text-[10px] text-[#9e9ec0]">{toFa(e.xpInPeriod)} XP</p>
|
||||||
|
<div className="w-full rounded-t-xl mt-1" style={{ height: podiumH, backgroundColor: medal.bg, opacity: 0.35 }}/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* بقیهی رتبهها */}
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
{rest.map((e) => {
|
||||||
|
const isMe = e.userId === myUserId;
|
||||||
|
return (
|
||||||
|
<div key={e.userId}
|
||||||
|
className={`flex items-center gap-3 p-3 rounded-2xl ${isMe ? "bg-[#FFF1E6] border-2 border-[#FF7A3D]/40" : "bg-white border border-[#ede9ff]"}`}>
|
||||||
|
<span className="w-7 text-center font-black text-[#9e9ec0] text-sm flex-shrink-0">{toFa(e.rank)}</span>
|
||||||
|
<div className="w-9 h-9 rounded-full bg-[#f0eeff] flex items-center justify-center flex-shrink-0 font-bold text-[#9e9ec0] text-xs">
|
||||||
|
{e.name ? e.name[0] : "؟"}
|
||||||
|
</div>
|
||||||
|
<p className={`flex-1 text-sm truncate ${isMe ? "text-[#B34E1C] font-bold" : "text-[#1a1a2e]"}`}>{isMe ? "تو" : e.name}</p>
|
||||||
|
<span className="text-xs text-[#9e9ec0] font-bold flex-shrink-0">{toFa(e.xpInPeriod)} XP</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// ─── Subscription ─────────────────────────────────────────────────────────────
|
// ─── Subscription ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
const PLAN_STYLES = [
|
const PLAN_STYLES = [
|
||||||
@@ -2523,6 +2756,10 @@ export default function App() {
|
|||||||
const [lessonId, setLessonId] = useState<number>(1);
|
const [lessonId, setLessonId] = useState<number>(1);
|
||||||
const [home, setHome] = useState<HomeResponse | null>(null);
|
const [home, setHome] = useState<HomeResponse | null>(null);
|
||||||
const [summary, setSummary] = useState<{ xp: number; coins: number; streak: { current: number; longest: number } } | null>(null);
|
const [summary, setSummary] = useState<{ xp: number; coins: number; streak: { current: number; longest: number } } | null>(null);
|
||||||
|
const [notifications, setNotifications] = useState<NotificationItem[] | null>(null);
|
||||||
|
const [notifLoading, setNotifLoading] = useState(false);
|
||||||
|
const [notifErr, setNotifErr] = useState(false);
|
||||||
|
const unreadCount = notifications?.filter((n) => !n.isRead).length ?? 0;
|
||||||
|
|
||||||
const refreshHome = () => {
|
const refreshHome = () => {
|
||||||
api.home().then((h) => {
|
api.home().then((h) => {
|
||||||
@@ -2532,10 +2769,47 @@ export default function App() {
|
|||||||
api.summary().then((s: any) => setSummary(s)).catch(() => {});
|
api.summary().then((s: any) => setSummary(s)).catch(() => {});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const refreshNotifications = () => {
|
||||||
|
setNotifLoading(true);
|
||||||
|
setNotifErr(false);
|
||||||
|
api.notifications.list()
|
||||||
|
.then((r) => setNotifications(r.items as unknown as NotificationItem[]))
|
||||||
|
.catch(() => setNotifErr(true))
|
||||||
|
.finally(() => setNotifLoading(false));
|
||||||
|
};
|
||||||
|
|
||||||
|
const markNotifRead = (id: number) => {
|
||||||
|
setNotifications((list) => list?.map((n) => (n.id === id ? { ...n, isRead: true } : n)) ?? list);
|
||||||
|
api.notifications.markRead(id).catch(() => {});
|
||||||
|
};
|
||||||
|
const markAllNotifRead = () => {
|
||||||
|
const unread = notifications?.filter((n) => !n.isRead) ?? [];
|
||||||
|
setNotifications((list) => list?.map((n) => ({ ...n, isRead: true })) ?? list);
|
||||||
|
unread.forEach((n) => api.notifications.markRead(n.id).catch(() => {}));
|
||||||
|
};
|
||||||
|
|
||||||
|
const [lbEntries, setLbEntries] = useState<LeaderboardEntry[] | null>(null);
|
||||||
|
const [lbPeriod, setLbPeriod] = useState<LeaderboardPeriod>("weekly");
|
||||||
|
const [lbLoading, setLbLoading] = useState(false);
|
||||||
|
const [lbErr, setLbErr] = useState(false);
|
||||||
|
|
||||||
|
const refreshLeaderboard = (period: LeaderboardPeriod) => {
|
||||||
|
setLbLoading(true);
|
||||||
|
setLbErr(false);
|
||||||
|
api.leaderboard(period as any)
|
||||||
|
.then((r) => setLbEntries(r.entries as unknown as LeaderboardEntry[]))
|
||||||
|
.catch(() => setLbErr(true))
|
||||||
|
.finally(() => setLbLoading(false));
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (flow === "main") refreshHome();
|
if (flow === "main") { refreshHome(); refreshNotifications(); }
|
||||||
}, [flow]);
|
}, [flow]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (flow === "main" && screen === "leaderboard" && !lbEntries) refreshLeaderboard(lbPeriod);
|
||||||
|
}, [flow, screen]);
|
||||||
|
|
||||||
// ─── تاریخچهی صفحات، برای دکمهی «بازگشت» ────────────────────────────────
|
// ─── تاریخچهی صفحات، برای دکمهی «بازگشت» ────────────────────────────────
|
||||||
// navigate: رفتن به جلو (صفحهی فعلی رو تو تاریخچه نگه میداره)
|
// navigate: رفتن به جلو (صفحهی فعلی رو تو تاریخچه نگه میداره)
|
||||||
// goBack: برگشت به صفحهی قبلی (یا خانه، اگه چیزی تو تاریخچه نبود)
|
// goBack: برگشت به صفحهی قبلی (یا خانه، اگه چیزی تو تاریخچه نبود)
|
||||||
@@ -2638,7 +2912,9 @@ export default function App() {
|
|||||||
{flow === "main" && screen === "askai" && <AskAIScreen profile={profile!}/>}
|
{flow === "main" && screen === "askai" && <AskAIScreen profile={profile!}/>}
|
||||||
{flow === "main" && screen === "subscription" && <SubscriptionScreen/>}
|
{flow === "main" && screen === "subscription" && <SubscriptionScreen/>}
|
||||||
{flow === "login" && <LoginScreen onDone={handleLoginDone}/>}
|
{flow === "login" && <LoginScreen onDone={handleLoginDone}/>}
|
||||||
{flow === "main" && screen === "home" && <HomeScreen profile={profile!} home={home} summary={summary} onNavigate={navigate} onContinue={(id) => { setUnitId(id); navigate("skillmap"); }}/>}
|
{flow === "main" && screen === "home" && <HomeScreen profile={profile!} home={home} summary={summary} unreadCount={unreadCount} onNavigate={navigate} onContinue={(id) => { setUnitId(id); navigate("skillmap"); }}/>}
|
||||||
|
{flow === "main" && screen === "notifications" && <NotificationsScreen items={notifications} loading={notifLoading} loadErr={notifErr} onBack={goBack} onMarkRead={markNotifRead} onMarkAllRead={markAllNotifRead}/>}
|
||||||
|
{flow === "main" && screen === "leaderboard" && <LeaderboardScreen entries={lbEntries} period={lbPeriod} loading={lbLoading} loadErr={lbErr} myUserId={auth?.userId} onChangePeriod={(p) => { setLbPeriod(p); refreshLeaderboard(p); }} onBack={goBack}/>}
|
||||||
{flow === "main" && screen === "topics" && <TopicSelectScreen home={home} onSelect={(id) => { setUnitId(id); navigate("skillmap"); }}/>}
|
{flow === "main" && screen === "topics" && <TopicSelectScreen home={home} onSelect={(id) => { setUnitId(id); navigate("skillmap"); }}/>}
|
||||||
{flow === "main" && screen === "lessons" && <LessonsHubScreen home={home} onOpenUnit={(id) => { setUnitId(id); navigate("skillmap"); }} onNavigate={navigate}/>}
|
{flow === "main" && screen === "lessons" && <LessonsHubScreen home={home} onOpenUnit={(id) => { setUnitId(id); navigate("skillmap"); }} onNavigate={navigate}/>}
|
||||||
{flow === "main" && screen === "skillmap" && <SkillMapScreen unitId={unitId} home={home} onOpenLesson={(id) => { setLessonId(id); navigate("lesson"); }} onBack={goBack}/>}
|
{flow === "main" && screen === "skillmap" && <SkillMapScreen unitId={unitId} home={home} onOpenLesson={(id) => { setLessonId(id); navigate("lesson"); }} onBack={goBack}/>}
|
||||||
|
|||||||
Reference in New Issue
Block a user