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
+31
View File
@@ -0,0 +1,31 @@
package api
import "testing"
func TestTierFromRank(t *testing.T) {
cases := []struct {
rank, total int
want string
}{
{1, 1, "champion"},
{1, 100, "champion"},
{1, 0, "bronze"}, // zero total → bronze (guard)
{0, 100, "bronze"}, // zero rank → bronze (guard)
{10, 100, "gold"}, // exactly 10%
{11, 100, "silver"}, // 11%
{30, 100, "silver"}, // exactly 30%
{31, 100, "bronze"}, // 31%
{5, 50, "gold"}, // 10%
{15, 50, "silver"}, // exactly 30%
{16, 50, "bronze"}, // 32% → bronze
{50, 100, "bronze"},
{2, 10, "silver"}, // 20% → silver
{1, 10, "champion"},
}
for _, c := range cases {
got := tierFromRank(c.rank, c.total)
if got != c.want {
t.Errorf("tierFromRank(%d, %d) = %q; want %q", c.rank, c.total, got, c.want)
}
}
}