mirror of
https://github.com/rgrgogu/new_starr.git
synced 2026-09-27 00:12:54 +08:00
@@ -21,6 +21,7 @@ import { useClientTiers } from "@/contexts/ClientTiersProvider";
|
||||
import { useDateFormat } from "@/hooks/useDateFormat";
|
||||
import api from "@/utils/api.util";
|
||||
import { toast } from "sonner";
|
||||
import CourseBadge from "@/modules/admin/components/courses/CourseBadge";
|
||||
|
||||
// ─── Tier badge fallbacks (used when no policy is configured in DB) ───────────
|
||||
|
||||
@@ -113,22 +114,7 @@ const getFallbackIcon = (type) => type === "badge" ? BadgeCheck : Trophy;
|
||||
|
||||
// ─── Certificate landscape card (profile preview) ────────────────────────────
|
||||
|
||||
const CertBadgeIcon = ({ className }) => (
|
||||
<svg className={className} width="120" height="120" viewBox="0 0 120 120" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Prism logo">
|
||||
<defs>
|
||||
<linearGradient id="prism-p" x1="0" y1="0" x2="1" y2="1">
|
||||
<stop offset="0" stopColor="#8EA2F6"/>
|
||||
<stop offset="1" stopColor="#5061E6"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<g transform="rotate(45 60 60)">
|
||||
<rect x="24" y="24" width="72" height="72" rx="16" fill="url(#prism-p)"/>
|
||||
<rect x="46" y="46" width="28" height="28" rx="6" fill="#FFFFFF"/>
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
|
||||
const LandscapeCertCard = ({ courseTitle, issuedAt, courseUuid }) => {
|
||||
const LandscapeCertCard = ({ courseTitle, issuedAt, courseUuid, badgeColor, badgeImageUrl }) => {
|
||||
const [downloading, setDownloading] = useState(false);
|
||||
const { fmtDate } = useDateFormat();
|
||||
|
||||
@@ -156,7 +142,7 @@ const LandscapeCertCard = ({ courseTitle, issuedAt, courseUuid }) => {
|
||||
|
||||
return (
|
||||
<div className="rounded-xl border bg-muted/40 p-3.5 flex items-center gap-3.5">
|
||||
<CertBadgeIcon className="w-12 h-12 shrink-0" />
|
||||
<CourseBadge mini color={badgeColor ?? "purple"} imageUrl={badgeImageUrl ?? null} />
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-[10px] uppercase tracking-widest text-muted-foreground font-semibold">Certificate of Completion</p>
|
||||
<p className="text-sm font-semibold truncate mt-0.5">{courseTitle}</p>
|
||||
@@ -195,6 +181,20 @@ const ProfilePage = () => {
|
||||
|
||||
const [badgeOpen, setBadgeOpen] = useState(false);
|
||||
const [selectedBadge, setSelectedBadge] = useState(null);
|
||||
const [badgeSrc, setBadgeSrc] = useState(null);
|
||||
const [firstCertBadge, setFirstCertBadge] = useState({ color: "purple", imageUrl: null });
|
||||
|
||||
useEffect(() => {
|
||||
const asset = myTier?.plan?.category?.badgeAsset ?? myTier?.category?.badgeAsset
|
||||
if (!asset) { setBadgeSrc(null); return }
|
||||
if (asset.storage_provider !== 's3') { setBadgeSrc(asset.file_url ?? null); return }
|
||||
api.post('/client/media/token', { asset_id: asset.asset_id })
|
||||
.then(({ data }) => {
|
||||
const base = (import.meta.env.VITE_API_URL ?? '').replace(/\/$/, '')
|
||||
setBadgeSrc(`${base}/client/media/stream/${data?.data?.token}`)
|
||||
})
|
||||
.catch(() => setBadgeSrc(null))
|
||||
}, [myTier])
|
||||
|
||||
const [inProgressCourses, setInProgressCourses] = useState([]);
|
||||
const [inProgressCoursesLoading, setInProgressCoursesLoading] = useState(false);
|
||||
@@ -222,7 +222,7 @@ const ProfilePage = () => {
|
||||
// ── Derived ────────────────────────────────────────────────────────────────
|
||||
|
||||
const tier = myTier?.tier ?? user?.tier ?? "free";
|
||||
const tierBadge = resolveTierBadge(myTier);
|
||||
const tierBadge = { ...resolveTierBadge(myTier), ...(badgeSrc ? { src: badgeSrc } : {}) };
|
||||
const earlyAccessBadge = resolveEarlyAccessBadge(systemBadges);
|
||||
const displayName = fullName || user?.personal_info?.name?.full_name || user?.email?.split("@")[0] || "—";
|
||||
const initials = displayName.split(" ").map((w) => w[0]).join("").slice(0, 2).toUpperCase();
|
||||
@@ -232,6 +232,34 @@ const ProfilePage = () => {
|
||||
|
||||
const certAchievements = achievements.filter((a) => a.key.startsWith("course_completed_"));
|
||||
|
||||
// Fetch badge color/image for the first certificate shown in the profile preview
|
||||
useEffect(() => {
|
||||
if (!certAchievements.length) return;
|
||||
const first = certAchievements[0];
|
||||
const uuid = first.metadata?.courseUuid ?? first.key.replace("course_completed_", "");
|
||||
if (!uuid) return;
|
||||
const STREAM_BASE = `${import.meta.env.VITE_API_URL}/client/media/stream`;
|
||||
api.get(`/client/courses/uuid/${uuid}`)
|
||||
.then(({ data }) => {
|
||||
const c = data.data ?? data;
|
||||
if (!c) return;
|
||||
if (c.badge_asset_id) {
|
||||
api.post("/client/media/token", { asset_id: c.badge_asset_id })
|
||||
.then(({ data: td }) => {
|
||||
const token = td.data?.token;
|
||||
setFirstCertBadge({
|
||||
color: c.badge_color ?? "purple",
|
||||
imageUrl: token ? `${STREAM_BASE}/${token}` : null,
|
||||
});
|
||||
})
|
||||
.catch(() => setFirstCertBadge({ color: c.badge_color ?? "purple", imageUrl: null }));
|
||||
} else {
|
||||
setFirstCertBadge({ color: c.badge_color ?? "purple", imageUrl: c.badge_image_url ?? null });
|
||||
}
|
||||
})
|
||||
.catch(() => {});
|
||||
}, [certAchievements.length]);
|
||||
|
||||
const hasEarlyAccess = achievements.some((a) => a.key === "early_access");
|
||||
// Premium badge only shows when currently exclusive (went through premium to get here)
|
||||
const hasPremiumBadge = achievements.some((a) => a.key === "premium_first_time") && userRank >= 2;
|
||||
@@ -612,6 +640,8 @@ const ProfilePage = () => {
|
||||
courseTitle={certAchievements[0].description}
|
||||
issuedAt={certAchievements[0].granted_at}
|
||||
courseUuid={certAchievements[0].metadata?.courseUuid ?? certAchievements[0].key.replace("course_completed_", "")}
|
||||
badgeColor={firstCertBadge.color}
|
||||
badgeImageUrl={firstCertBadge.imageUrl}
|
||||
/>
|
||||
</CardContent>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user