feat(frontend): draft/publish, certificate approval UI, contributors, leaderboard tiers
- admin-studio: chapter/lesson publish toggle with Draft/Published badges; per-chapter accordion - cap/page: remove auto-issue effect; student requests, admin approves - learning-panel: admin issue-certificate action per student - site-settings: contributors editor with avatar upload (ContributorAvatarPicker, TagInput) - community-panel: render contributors section below social links - leaderboard-panel: TierBadge, totalParticipants, tier on entries and self - page.tsx: fix "شروع مسیر" routing to /register instead of /login - navbar: correct icons (Settings, Palette) in user dropdown - types: Contributor, LeaderboardTier, Certificate status field, updated LeaderboardData/Entry - layout: PWA icon metadata (192, 512, apple-touch-icon)
This commit is contained in:
@@ -1,17 +1,50 @@
|
||||
"use client";
|
||||
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { ExternalLink, Share2 } from "lucide-react";
|
||||
import { ExternalLink, Share2, Tag, Users } from "lucide-react";
|
||||
import { http, toUserError } from "@/lib/api";
|
||||
import { Panel } from "@/components/panel";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import type { Contributor } from "@/lib/types";
|
||||
|
||||
export type SocialLink = { name: string; url: string };
|
||||
|
||||
export type CommunityPayload = {
|
||||
socials: SocialLink[];
|
||||
contributors: Contributor[];
|
||||
};
|
||||
|
||||
function ContributorAvatar({ contributor }: { contributor: Contributor }) {
|
||||
if (contributor.avatar) {
|
||||
return (
|
||||
<img
|
||||
src={contributor.avatar}
|
||||
alt={contributor.name}
|
||||
className="h-12 w-12 shrink-0 rounded-full border-2 border-border object-cover"
|
||||
onError={(e) => {
|
||||
(e.currentTarget as HTMLImageElement).style.display = "none";
|
||||
(e.currentTarget.nextElementSibling as HTMLElement | null)?.removeAttribute("hidden");
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
const initials = contributor.name
|
||||
.split(" ")
|
||||
.map((w) => w[0])
|
||||
.slice(0, 2)
|
||||
.join("")
|
||||
.toUpperCase();
|
||||
return (
|
||||
<span
|
||||
aria-hidden
|
||||
className="flex h-12 w-12 shrink-0 items-center justify-center rounded-full border-2 border-border bg-primary/10 text-sm font-bold text-primary"
|
||||
>
|
||||
{initials}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
export function CommunityPanel() {
|
||||
const { data, isLoading, isError, error, refetch } = useQuery({
|
||||
queryKey: ["community"],
|
||||
@@ -19,12 +52,17 @@ export function CommunityPanel() {
|
||||
});
|
||||
|
||||
if (isLoading) {
|
||||
return <div className="h-72 animate-pulse rounded-2xl border-2 border-border bg-muted/40" />;
|
||||
return (
|
||||
<div className="mx-auto max-w-2xl space-y-4">
|
||||
<div className="h-40 animate-pulse rounded-2xl border-2 border-border bg-muted/40" />
|
||||
<div className="h-64 animate-pulse rounded-2xl border-2 border-border bg-muted/40" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (isError) {
|
||||
return (
|
||||
<div className="mx-auto max-w-xl rounded-2xl border-2 border-destructive/30 bg-destructive/5 p-8 text-center">
|
||||
<div className="mx-auto max-w-2xl rounded-2xl border-2 border-destructive/30 bg-destructive/5 p-8 text-center">
|
||||
<p className="font-medium text-destructive">{toUserError(error)}</p>
|
||||
<Button className="mt-4" variant="outline" onClick={() => refetch()}>
|
||||
تلاش دوباره
|
||||
@@ -34,9 +72,11 @@ export function CommunityPanel() {
|
||||
}
|
||||
|
||||
const socials = data?.socials ?? [];
|
||||
const contributors = data?.contributors ?? [];
|
||||
|
||||
return (
|
||||
<div className="mx-auto max-w-xl">
|
||||
<div className="mx-auto max-w-2xl space-y-4">
|
||||
{/* Social links */}
|
||||
<Panel
|
||||
title="شبکههای پرگار"
|
||||
description="راههای رسمی ارتباط با برنامه. لینک شخصی هر کاربر روی پروفایل خودش است."
|
||||
@@ -63,6 +103,61 @@ export function CommunityPanel() {
|
||||
</ul>
|
||||
)}
|
||||
</Panel>
|
||||
|
||||
{/* Contributors */}
|
||||
<Panel
|
||||
title="مشارکتکنندگان"
|
||||
description="افرادی که در ساخت پرگار سهیم بودهاند. ممنون از هر کمکی."
|
||||
icon={<Users className="h-5 w-5" />}
|
||||
tint="bg-emerald-500/15 text-emerald-600 dark:text-emerald-400"
|
||||
>
|
||||
{contributors.length === 0 ? (
|
||||
<p className="py-8 text-center text-sm text-muted-foreground">اطلاعات مشارکتکنندگان هنوز ثبت نشده است.</p>
|
||||
) : (
|
||||
<ul className="divide-y divide-border/60">
|
||||
{contributors.map((c, i) => (
|
||||
<li key={i} className="flex flex-col gap-3 py-4 first:pt-0 last:pb-0 sm:flex-row sm:items-start">
|
||||
<ContributorAvatar contributor={c} />
|
||||
<div className="min-w-0 flex-1 space-y-1.5">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
{c.url ? (
|
||||
<a
|
||||
href={c.url}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="flex items-center gap-1 font-bold hover:text-primary hover:underline"
|
||||
>
|
||||
{c.name}
|
||||
<ExternalLink className="h-3 w-3 shrink-0 text-muted-foreground" />
|
||||
</a>
|
||||
) : (
|
||||
<span className="font-bold">{c.name}</span>
|
||||
)}
|
||||
{c.role && (
|
||||
<span className="text-xs text-muted-foreground">{c.role}</span>
|
||||
)}
|
||||
</div>
|
||||
{c.bio && <p className="text-sm leading-relaxed text-muted-foreground">{c.bio}</p>}
|
||||
{c.contributions.length > 0 && (
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{c.contributions.map((cat) => (
|
||||
<Badge
|
||||
key={cat}
|
||||
variant="secondary"
|
||||
className="gap-1 px-2 py-0.5 text-xs"
|
||||
>
|
||||
<Tag className="h-3 w-3" />
|
||||
{cat}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</Panel>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user