feat: implement 8 features across full stack

- Leaderboard: rank-based percentile tiers (champion/gold/silver/bronze),
    deterministic tie-breaking (xp DESC, user_id ASC)
  - Mentor chat: md: breakpoints, safe-area-inset-bottom padding
  - Learning progress: server-side progressPct, curriculum expansion safety
  - Physical orders: hard-disabled with 410 Gone
  - Certificate approval: pending/issued flow, admin issue/revoke endpoints,
    certificate_requires_approval setting (migration 0031)
  - Draft/publish: is_published on lessons and chapters, admin publish UI
    (migration 0030)
  - Video panel: static thumbnails (no preload), client-side pagination
  - Rich editor: TipTap + tiptap-markdown for lesson descriptions,
    marked + DOMPurify for SafeMarkdown rendering
This commit is contained in:
ahp
2026-10-18 10:14:22 +03:30
parent 75e2892e36
commit 03610bc3d3
23 changed files with 1641 additions and 474 deletions
+12 -2
View File
@@ -33,10 +33,11 @@ export interface User {
export interface Certificate {
id: number;
userId: number;
publicId: string;
publicId?: string;
fullName: string;
issuedAt: string;
revokedAt?: string;
status: "pending" | "issued";
}
export type PhysicalOrderStatus = "requested" | "paid" | "shipped" | "cancelled";
@@ -91,6 +92,7 @@ export interface Lesson {
sortOrder: number;
xpReward: number;
isActive: boolean;
isPublished: boolean;
}
export interface Chapter {
@@ -99,6 +101,7 @@ export interface Chapter {
description: string;
icon: string;
sortOrder: number;
isPublished: boolean;
}
export interface TreeLesson {
@@ -122,6 +125,9 @@ export interface TreeChapter extends Chapter {
export interface TreeData {
chapters: TreeChapter[];
totalLessons: number;
completedLessons: number;
progressPct: number;
me: { xp: number; streak: number; hearts: number; isLocked: boolean; role: Role };
}
@@ -246,12 +252,15 @@ export interface Conversation {
archived: boolean;
}
export type LeaderboardTier = "champion" | "gold" | "silver" | "bronze";
export interface LeaderboardEntry {
userId: number;
name: string;
email: string;
xp: number;
rank: number;
tier: LeaderboardTier;
avatarVariant?: "marble" | "beam" | "pixel" | "sunset" | "ring" | "bauhaus";
avatarPalette?: string;
avatarPhoto?: string;
@@ -260,7 +269,8 @@ export interface LeaderboardEntry {
export interface LeaderboardData {
week: string;
entries: LeaderboardEntry[];
me: { rank: number; xp: number; streak: number };
totalParticipants: number;
me: { rank: number; xp: number; streak: number; tier: LeaderboardTier };
}
export interface NotificationPrefs {