Compare commits
2
Commits
12ecbf991e
...
88c86ac810
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
88c86ac810
|
||
|
|
f59ff54256
|
@@ -11,7 +11,7 @@ backend:
|
|||||||
image:
|
image:
|
||||||
pullPolicy: IfNotPresent
|
pullPolicy: IfNotPresent
|
||||||
repository: pargar-backend
|
repository: pargar-backend
|
||||||
tag: 0.3.8
|
tag: 0.3.9
|
||||||
metrics:
|
metrics:
|
||||||
serviceMonitor:
|
serviceMonitor:
|
||||||
enabled: true
|
enabled: true
|
||||||
|
|||||||
@@ -3269,7 +3269,7 @@ ws.onmessage = (ev) => {
|
|||||||
setOpen(true);
|
setOpen(true);
|
||||||
setNotif(null);
|
setNotif(null);
|
||||||
}}
|
}}
|
||||||
className="fixed bottom-24 start-5 end-5 z-[64] flex max-w-full items-center gap-2.5 rounded-2xl border-2 border-white/50 bg-card/85 p-2.5 text-right shadow-offset backdrop-blur-2xl sm:end-auto sm:max-w-[300px]"
|
className="fixed bottom-[calc(5.5rem+env(safe-area-inset-bottom,0px))] start-5 end-5 z-[64] flex max-w-full items-center gap-2.5 rounded-2xl border-2 border-white/50 bg-card/85 p-2.5 text-right shadow-offset backdrop-blur-2xl sm:bottom-24 sm:end-auto sm:max-w-[300px]"
|
||||||
>
|
>
|
||||||
<span className="grid h-9 w-9 shrink-0 place-items-center rounded-xl bg-accent/15">
|
<span className="grid h-9 w-9 shrink-0 place-items-center rounded-xl bg-accent/15">
|
||||||
<MessageSquare className="h-4 w-4 text-accent" />
|
<MessageSquare className="h-4 w-4 text-accent" />
|
||||||
@@ -3286,7 +3286,9 @@ ws.onmessage = (ev) => {
|
|||||||
</AnimatePresence>
|
</AnimatePresence>
|
||||||
|
|
||||||
{me && !open && (
|
{me && !open && (
|
||||||
<div className="fixed bottom-5 start-5 z-[64]">
|
// On mobile the bottom nav is 56px; lift FAB above it + safe area + gap.
|
||||||
|
// md:bottom-5 restores original position when the bottom nav is hidden.
|
||||||
|
<div className="fixed bottom-[calc(4.5rem+env(safe-area-inset-bottom,0px))] start-5 z-[64] md:bottom-5">
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
{unreadTotal > 0 && (
|
{unreadTotal > 0 && (
|
||||||
<span className="pointer-events-none absolute -inset-1 -z-10 animate-ping rounded-2xl bg-accent/40" aria-hidden />
|
<span className="pointer-events-none absolute -inset-1 -z-10 animate-ping rounded-2xl bg-accent/40" aria-hidden />
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import {
|
|||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { useMemo, useState } from "react";
|
import { useMemo, useState } from "react";
|
||||||
import type { TreeChapter, TreeData, TreeLesson } from "@/lib/types";
|
import type { TreeChapter, TreeData, TreeLesson } from "@/lib/types";
|
||||||
|
import { useAuth } from "@/lib/auth-store";
|
||||||
import { cn, formatFaNumber } from "@/lib/utils";
|
import { cn, formatFaNumber } from "@/lib/utils";
|
||||||
|
|
||||||
const CHAPTER_ICONS: Record<string, typeof Terminal> = {
|
const CHAPTER_ICONS: Record<string, typeof Terminal> = {
|
||||||
@@ -38,6 +39,8 @@ const CHAPTER_ICONS: Record<string, typeof Terminal> = {
|
|||||||
|
|
||||||
export function SkillTree({ data, onSelect }: { data: TreeData; onSelect?: (id: number) => void }) {
|
export function SkillTree({ data, onSelect }: { data: TreeData; onSelect?: (id: number) => void }) {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const role = useAuth((s) => s.user?.role);
|
||||||
|
const isStaff = role === "admin" || role === "mentor";
|
||||||
|
|
||||||
const chapters = useMemo(() => [...data.chapters].sort((a, b) => a.sortOrder - b.sortOrder), [data]);
|
const chapters = useMemo(() => [...data.chapters].sort((a, b) => a.sortOrder - b.sortOrder), [data]);
|
||||||
const byId = useMemo(() => {
|
const byId = useMemo(() => {
|
||||||
@@ -97,6 +100,7 @@ export function SkillTree({ data, onSelect }: { data: TreeData; onSelect?: (id:
|
|||||||
open={expanded.has(ch.id)}
|
open={expanded.has(ch.id)}
|
||||||
onToggle={() => toggle(ch.id)}
|
onToggle={() => toggle(ch.id)}
|
||||||
onSelect={onSelect ?? ((id: number) => router.push(`/player/${id}`))}
|
onSelect={onSelect ?? ((id: number) => router.push(`/player/${id}`))}
|
||||||
|
isStaff={isStaff}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -110,12 +114,14 @@ function ChapterRow({
|
|||||||
open,
|
open,
|
||||||
onToggle,
|
onToggle,
|
||||||
onSelect,
|
onSelect,
|
||||||
|
isStaff,
|
||||||
}: {
|
}: {
|
||||||
ch: TreeChapter;
|
ch: TreeChapter;
|
||||||
byId: Map<number, TreeLesson>;
|
byId: Map<number, TreeLesson>;
|
||||||
open: boolean;
|
open: boolean;
|
||||||
onToggle: () => void;
|
onToggle: () => void;
|
||||||
onSelect: (id: number) => void;
|
onSelect: (id: number) => void;
|
||||||
|
isStaff: boolean;
|
||||||
}) {
|
}) {
|
||||||
const lessons = [...(ch.lessons ?? [])].sort((a, b) => a.y - b.y || a.x - b.x);
|
const lessons = [...(ch.lessons ?? [])].sort((a, b) => a.y - b.y || a.x - b.x);
|
||||||
const done = lessons.filter((l) => l.progress?.passedQuiz).length;
|
const done = lessons.filter((l) => l.progress?.passedQuiz).length;
|
||||||
@@ -147,8 +153,13 @@ function ChapterRow({
|
|||||||
<CheckCircle2 className="h-3.5 w-3.5" /> کامل شد
|
<CheckCircle2 className="h-3.5 w-3.5" /> کامل شد
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
{isStaff && !ch.isPublished && (
|
||||||
|
<span className="hidden shrink-0 rounded-full border border-dashed border-warning/50 bg-warning/10 px-2 py-0.5 text-[11px] font-bold text-warning sm:inline-block">
|
||||||
|
پیشنویس
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
<span className="hidden shrink-0 rounded-full bg-muted px-2 py-0.5 text-[11px] font-bold text-muted-foreground sm:inline-block">
|
<span className="hidden shrink-0 rounded-full bg-muted px-2 py-0.5 text-[11px] font-bold text-muted-foreground sm:inline-block">
|
||||||
هفتهٔ {ch.sortOrder}
|
فصل {ch.sortOrder}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<p className="truncate text-xs text-muted-foreground">{ch.description}</p>
|
<p className="truncate text-xs text-muted-foreground">{ch.description}</p>
|
||||||
@@ -173,7 +184,7 @@ function ChapterRow({
|
|||||||
<div className="overflow-hidden">
|
<div className="overflow-hidden">
|
||||||
<div className="grid gap-2.5 border-t-2 border-border/70 p-4 sm:grid-cols-2">
|
<div className="grid gap-2.5 border-t-2 border-border/70 p-4 sm:grid-cols-2">
|
||||||
{lessons.map((lesson) => (
|
{lessons.map((lesson) => (
|
||||||
<LessonRow key={lesson.id} lesson={lesson} byId={byId} onSelect={onSelect} />
|
<LessonRow key={lesson.id} lesson={lesson} byId={byId} onSelect={onSelect} isStaff={isStaff} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -186,10 +197,12 @@ function LessonRow({
|
|||||||
lesson,
|
lesson,
|
||||||
byId,
|
byId,
|
||||||
onSelect,
|
onSelect,
|
||||||
|
isStaff,
|
||||||
}: {
|
}: {
|
||||||
lesson: TreeLesson;
|
lesson: TreeLesson;
|
||||||
byId: Map<number, TreeLesson>;
|
byId: Map<number, TreeLesson>;
|
||||||
onSelect: (id: number) => void;
|
onSelect: (id: number) => void;
|
||||||
|
isStaff: boolean;
|
||||||
}) {
|
}) {
|
||||||
const passed = !!lesson.progress?.passedQuiz;
|
const passed = !!lesson.progress?.passedQuiz;
|
||||||
const locked = lesson.locked;
|
const locked = lesson.locked;
|
||||||
@@ -254,6 +267,11 @@ function LessonRow({
|
|||||||
</div>
|
</div>
|
||||||
<div className="mt-1.5 flex flex-wrap items-center gap-2">
|
<div className="mt-1.5 flex flex-wrap items-center gap-2">
|
||||||
<span className={cn("rounded-full px-2 py-0.5 text-[11px] font-bold", s.pill)}>{s.pillText}</span>
|
<span className={cn("rounded-full px-2 py-0.5 text-[11px] font-bold", s.pill)}>{s.pillText}</span>
|
||||||
|
{isStaff && lesson.isPublished === false && (
|
||||||
|
<span className="rounded-full border border-dashed border-warning/50 bg-warning/10 px-2 py-0.5 text-[11px] font-bold text-warning">
|
||||||
|
پیشنویس
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
{inProgress && (
|
{inProgress && (
|
||||||
<div className="h-1.5 w-24 overflow-hidden rounded-full bg-muted">
|
<div className="h-1.5 w-24 overflow-hidden rounded-full bg-muted">
|
||||||
<div className="h-full rounded-full bg-primary" style={{ width: `${pct}%` }} />
|
<div className="h-full rounded-full bg-primary" style={{ width: `${pct}%` }} />
|
||||||
|
|||||||
@@ -117,6 +117,7 @@ export interface TreeLesson {
|
|||||||
xpReward: number;
|
xpReward: number;
|
||||||
durationSeconds: number;
|
durationSeconds: number;
|
||||||
chapterId: number;
|
chapterId: number;
|
||||||
|
isPublished?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TreeChapter extends Chapter {
|
export interface TreeChapter extends Chapter {
|
||||||
|
|||||||
Reference in New Issue
Block a user