import { useEffect, useState } from "react";
import { useNavigate } from "react-router-dom";
import {
Edit, BookOpen, Award, Trophy, Shield, Star, Zap, Target, BadgeCheck, Medal, Flame, LockIcon, Camera, ChevronRight, Download, RefreshCcw
} from "lucide-react";
import * as LucideIcons from "lucide-react";
import { getTierColor } from "@/utils/tierColors";
import AvatarUploadDialog from "@/components/generic/AvatarUploadDialog";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Progress } from "@/components/ui/progress";
import { Separator } from "@/components/ui/separator";
import { Skeleton } from "@/components/ui/skeleton";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
import ResponsiveModal from "@/components/generic/ResponsiveModal";
import { useProfile } from "@/contexts/ProfileProvider";
import { useAuth } from "@/contexts/AuthContext";
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) ───────────
const TIER_BADGE_FALLBACKS = {
free: {
src: "/badges/free-access-badge-leaf-flaticon.svg",
label: "Free",
description: "Active member using the Free Plan.",
information: "Free users have access to the platform's core features without a subscription.",
},
premium: {
src: "/badges/premium-badge-sheriff-flaticon.svg",
label: "Premium",
description: "Subscriber with access to premium learning content.",
information: "Premium members can access all premium courses available on the platform.",
},
exclusive: {
src: "/badges/exclusive-award-flaticon.svg",
label: "Exclusive",
description: "Premium member with access to exclusive course content.",
information: "This badge represents a member with access to exclusive courses.",
},
};
const EARLY_ACCESS_FALLBACK = {
src: "/badges/early-access-badge-percent-flaticon.svg",
label: "Early Access",
description: "Registered during the Philproperties beta period.",
information: "Exclusive to members who registered before Dec 31, 2026.",
};
// Renders either a Lucide icon badge or an image badge.
function TierBadgeDisplay({ badge, className }) {
if (badge?.src) return ;
if (badge?.icon) {
const Icon = LucideIcons[badge.icon];
if (!Icon) return null;
const swatch = getTierColor(badge.colorKey ?? "green").swatch;
return ;
}
return null;
}
function resolveTierBadge(myTier) {
const tier = myTier?.tier ?? "free";
const category = myTier?.plan?.category ?? myTier?.category ?? null;
if (category) {
const hasBadge = category.badgeAsset || category.badge_icon || category.badge_label;
if (hasBadge) {
return {
src: category.badgeAsset?.file_url ?? null,
icon: !category.badgeAsset ? (category.badge_icon ?? null) : null,
colorKey: category.color ?? "green",
label: category.badge_label ?? category.name ?? tier,
description: "",
information: category.description ?? "",
};
}
}
return TIER_BADGE_FALLBACKS[tier] ?? TIER_BADGE_FALLBACKS.free;
}
function resolveEarlyAccessBadge(systemBadges) {
const found = systemBadges?.find((b) => b.key === "early_access");
if (!found) return EARLY_ACCESS_FALLBACK;
return {
src: found.asset?.file_url ?? EARLY_ACCESS_FALLBACK.src,
label: found.label ?? EARLY_ACCESS_FALLBACK.label,
description: found.description ?? EARLY_ACCESS_FALLBACK.description,
information: found.information ?? EARLY_ACCESS_FALLBACK.information,
};
}
// ─── Achievement icon map (by key) ───────────────────────────────────────────
const ACHIEVEMENT_ICONS = {
early_access: Star,
premium_first_time: BadgeCheck,
exclusive_first_time: Medal,
first_course_completed: BookOpen,
courses_completed_5: Flame,
courses_completed_10: Zap,
perfect_quiz_score: Target,
profile_completed: Shield,
first_referral: Award,
};
const getFallbackIcon = (type) => type === "badge" ? BadgeCheck : Trophy;
// ─── Certificate landscape card (profile preview) ────────────────────────────
const LandscapeCertCard = ({ courseTitle, issuedAt, courseUuid, badgeColor, badgeImageUrl }) => {
const [downloading, setDownloading] = useState(false);
const { fmtDate } = useDateFormat();
const issuedLabel = issuedAt ? fmtDate(issuedAt) : "—";
const handleDownload = async () => {
setDownloading(true);
try {
const res = await api.get(`/client/certificates/${courseUuid}`, { responseType: "blob" });
const disposition = res.headers["content-disposition"] ?? "";
const match = disposition.match(/filename="([^"]+)"/);
const filename = match ? match[1] : `certificate-${courseUuid}.pdf`;
const url = URL.createObjectURL(new Blob([res.data], { type: "application/pdf" }));
const a = document.createElement("a");
a.href = url;
a.download = filename;
a.click();
URL.revokeObjectURL(url);
} catch {
toast("Could not download certificate.");
} finally {
setDownloading(false);
}
};
return (
Certificate of Completion
{courseTitle}
Issued {issuedLabel}
Verified
{downloading ? "…" : "PDF"}
);
};
// ─── Component ───────────────────────────────────────────────────────────────
const ProfilePage = () => {
const navigate = useNavigate();
const { user } = useAuth();
const { fmtDate } = useDateFormat();
const {
profile, profileLoading, getProfile,
achievements, achievementsLoading, getAchievements,
fullName, avatarUrl, occupation,
uploadAvatar, deleteAvatar, avatarLoading,
} = useProfile();
const [avatarDialogOpen, setAvatarDialogOpen] = useState(false);
const { myTier, tierLoading, getMyTier, systemBadges, getSystemBadges } = useClientTiers();
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);
const [pendingModalOpen, setPendingModalOpen] = useState(false);
const [pendingModalCourse, setPendingModalCourse] = useState(null);
const [completedTotal, setCompletedTotal] = useState(0);
const [completedLoading, setCompletedLoading] = useState(false);
useEffect(() => {
getProfile();
getAchievements();
getMyTier();
getSystemBadges();
(async () => {
setInProgressCoursesLoading(true);
try {
const { data } = await api.get("/client/courses/in-progress");
setInProgressCourses(data.data ?? []);
} catch {
// silent — empty state handles it
} finally {
setInProgressCoursesLoading(false);
}
})();
(async () => {
setCompletedLoading(true);
try {
const { data } = await api.get("/client/courses/completed");
const counts = data.data?.counts ?? {};
setCompletedTotal((counts.courses ?? 0) + (counts.units ?? 0) + (counts.lessons ?? 0));
} catch {
// silent — empty state handles it
} finally {
setCompletedLoading(false);
}
})();
}, []);
// ── Derived ────────────────────────────────────────────────────────────────
const tier = myTier?.tier ?? user?.tier ?? "free";
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();
const tierRank = { free: 0, premium: 1, exclusive: 2 };
const userRank = tierRank[tier] ?? 0;
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;
// Exclusive badge only shows when currently exclusive
const hasExclusiveBadge = achievements.some((a) => a.key === "exclusive_first_time") && userRank >= 2;
const badgesReady = !tierLoading && !achievementsLoading;
// Build badge object with earnedAt from achievements for modal
const getBadgeWithDate = (achievementKey, tierKey) => {
const achievement = achievements.find((a) => a.key === achievementKey);
const earnedAt = achievement?.granted_at ? fmtDate(achievement.granted_at) : null;
return { ...TIER_BADGE_FALLBACKS[tierKey], earnedAt };
};
const getEarlyAccessBadge = () => {
const achievement = achievements.find((a) => a.key === "early_access");
const earnedAt = achievement?.granted_at ? fmtDate(achievement.granted_at) : null;
return { ...earlyAccessBadge, earnedAt };
};
const getActiveTierBadgeWithDate = () => {
const achievementKey = tier === "premium"
? "premium_first_time"
: tier === "exclusive"
? "exclusive_first_time"
: null;
return achievementKey ? getBadgeWithDate(achievementKey, tier) : tierBadge;
};
return (
{/* ── Header card ──────────────────────────────────────────────────── */}
{initials}
setAvatarDialogOpen(true)}
>
{profileLoading ? (
) : (
{displayName}
)}
{/* ── Badges ─────────────────────────────────────────────── */}
{badgesReady ? (
{/* Early access — always show if earned */}
{hasEarlyAccess && (
{ setSelectedBadge(getEarlyAccessBadge()); setBadgeOpen(true); }}
/>
{earlyAccessBadge.label}
)}
{/* Premium badge — only when currently exclusive */}
{hasPremiumBadge && (
{ setSelectedBadge(getBadgeWithDate("premium_first_time", "premium")); setBadgeOpen(true); }}
/>
Premium
)}
{/* Exclusive badge — only when currently exclusive */}
{hasExclusiveBadge && (
{ setSelectedBadge(getBadgeWithDate("exclusive_first_time", "exclusive")); setBadgeOpen(true); }}
/>
Exclusive
)}
{/* Current active tier badge — always shown */}
{/* For free tier, show free badge. For premium/exclusive already shown above via hasPremiumBadge/hasExclusiveBadge */}
{userRank === 0 && (
{ setSelectedBadge(tierBadge); setBadgeOpen(true); }}>
{tierBadge.label}
)}
{/* For premium tier without achievement yet (edge case) */}
{userRank === 1 && !hasPremiumBadge && (
{ setSelectedBadge(getActiveTierBadgeWithDate()); setBadgeOpen(true); }}>
{tierBadge.label}
)}
) : (
)}
{user?.email ?? "—"}
{occupation && {occupation} }
navigate("/profile/edit")}>
Edit profile
{/* ── Badge modal ───────────────────────────────────────────────────── */}
setBadgeOpen(false)}>Close
}
>
{(selectedBadge?.src || selectedBadge?.icon) && (
)}
{selectedBadge?.label}
{selectedBadge?.earnedAt && (
{selectedBadge.earnedAt}
)}
{selectedBadge?.information}
{/* ── Two columns ──────────────────────────────────────────────────── */}
{/* Left column */}
{/* Learning progress */}
Learning progress
Only you
{inProgressCoursesLoading ? (
{[...Array(2)].map((_, i) => (
))}
) : inProgressCourses.length === 0 ? (
No courses in progress.
) : (
inProgressCourses.slice(0, 2).map((course, i) => {
const pct = course.lessons_total > 0
? Math.round((course.lessons_completed / course.lessons_total) * 100)
: 0;
const quizPending = course.reading_status === 'completed';
return (
{quizPending ? (
{ setPendingModalCourse(course); setPendingModalOpen(true); }}
>
Quiz / Assessment Pending
) : (
{course.lessons_completed} / {course.lessons_total} lessons
)}
{i < Math.min(inProgressCourses.length, 2) - 1 &&
}
);
})
)}
{/* Quiz / Assessment Pending detail modal */}
{ setPendingModalOpen(v); if (!v) setPendingModalCourse(null); }}
title={pendingModalCourse?.title ?? 'Pending Items'}
description="You've finished reading all lessons. Complete the items below to earn your certificate."
hideDrawerClose={false}
footer={
{
setPendingModalOpen(false);
navigate(`/course/${pendingModalCourse?.course_id}/unit`);
}}
>
Go to Course
}
>
{pendingModalCourse && (() => {
const { pending_quizzes = [], pending_assessment, assessment_configured = true } = pendingModalCourse;
const hasQuizzes = pending_quizzes.length > 0;
const hasAssessment = !!pending_assessment;
if (!hasQuizzes && !hasAssessment && assessment_configured) {
return (
No pending items found.
);
}
return (
{!assessment_configured && (
You've finished all lessons, but this course's final assessment hasn't been set up yet.
Check back once it's available — the course can't be marked complete until then.
)}
{hasQuizzes && (
Unit Quizzes
{pending_quizzes.map((q) => (
{q.title || 'Unit Quiz'}
{q.unit_title}
Pass: {q.passing_score}%
{q.attempt_count === 0 ? 'Not attempted' : `${q.attempt_count} attempt${q.attempt_count === 1 ? '' : 's'}`}
{q.is_required && (
Required
)}
))}
)}
{hasAssessment && (
Final Assessment
{pending_assessment.title || 'Course Assessment'}
Pass: {pending_assessment.passing_score}%
{pending_assessment.attempt_count === 0 ? 'Not attempted' : `${pending_assessment.attempt_count} attempt${pending_assessment.attempt_count === 1 ? '' : 's'}`}
{pending_assessment.is_required && (
Required
)}
)}
);
})()}
{/* Account info */}
Account info
Member since
{profile?.createdAt ? fmtDate(profile.createdAt) : "—"}
Plan
{tierLoading ? (
) : (
{tier}{myTier?.expires_at && ` · until ${fmtDate(myTier.expires_at)}`}
)}
{/* Right column */}
{/* Certificates — preview + full list at /certificates */}
navigate("/certificates")}
>
{achievementsLoading ? (
) : certAchievements.length > 0 ? (
{certAchievements.length}
) : null}
{!achievementsLoading && certAchievements.length > 0 && (
)}
{/* Achievements — latest 2, full list at /achievements */}
navigate("/achievements")}
>
Achievements
{achievementsLoading ? (
) : achievements.length > 0 ? (
{achievements.length}
) : null}
{achievementsLoading ? (
{[...Array(2)].map((_, i) => (
))}
) : achievements.length > 0 ? (() => {
const latest = [...achievements]
.sort((a, b) => new Date(b.granted_at) - new Date(a.granted_at))
.slice(0, 2);
return (
{latest.map((item, i) => {
const Icon = ACHIEVEMENT_ICONS[item.key] ?? getFallbackIcon(item.type);
return (
{item.label}
{item.description}
{item.granted_at && (
{fmtDate(item.granted_at)}
)}
{i < latest.length - 1 &&
}
);
})}
);
})() : (
Nothing to show yet
Complete more courses to unlock achievements.
)}
{/* Completed — live count, full list at /completed */}
navigate("/completed")}
>
Completed
Only you
{completedLoading ? (
) : completedTotal > 0 ? (
{completedTotal}
) : null}
setAvatarDialogOpen(false)}
currentAvatarUrl={avatarUrl ?? ''}
initials={initials}
onUpload={uploadAvatar}
onDelete={deleteAvatar}
loading={avatarLoading}
/>
);
};
export default ProfilePage;