Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
129 changes: 129 additions & 0 deletions brain-trails/components/dashboard/CharacterCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
"use client";

import { motion } from "framer-motion";
import { useRouter } from "next/navigation";
import { Flame, Star, Scroll, Timer, GraduationCap, Layers, PenTool, Coins } from "lucide-react";
import { useAuth } from "@/context/AuthContext";
import { useTheme } from "@/context/ThemeContext";
import { useQuests } from "@/hooks/useQuests";
import OwlCompanion from "@/components/ui/OwlCompanion";

/**
* Habitica-style "character sheet" — the dashboard hero. Avatar + three vitality
* bars (streak / XP / today's quests) + the gold purse + the do-a-thing actions.
*/
function levelTitle(level: number): string {
if (level >= 40) return "Archmage of Study";
if (level >= 25) return "Grand Scholar";
if (level >= 15) return "Adept";
if (level >= 7) return "Scholar";
if (level >= 3) return "Apprentice";
return "Novice";
}

export default function CharacterCard() {
const { profile } = useAuth();
const { theme } = useTheme();
const router = useRouter();
const { quests } = useQuests();
const isSun = theme === "sun";

const name = profile?.display_name || profile?.username || "Traveler";
const level = profile?.level ?? 1;
const xp = profile?.xp ?? 0;
const streak = profile?.streak_days ?? 0;
const gold = profile?.gold ?? 0;

const xpIntoLevel = Math.max(0, xp - (level - 1) * 1000);
const xpPct = Math.min(xpIntoLevel / 1000, 1) * 100;

// Streak "vitality" — fills toward the next milestone (7 / 30 / 100).
const tiers = [7, 30, 100];
const nextTier = tiers.find((t) => streak < t) ?? 100;
const prevTier = [0, ...tiers].filter((t) => t <= streak).pop() ?? 0;
const streakPct = nextTier > prevTier ? ((streak - prevTier) / (nextTier - prevTier)) * 100 : 100;

const doneToday = quests.filter((q) => q.period === "daily" && q.is_completed).length;
const totalToday = quests.filter((q) => q.period === "daily").length;
const questPct = totalToday > 0 ? (doneToday / totalToday) * 100 : 0;

const card = isSun ? "bg-white border-slate-200" : "bg-slate-900/80 border-white/10";
const ink = isSun ? "text-slate-900" : "text-white";
const sub = isSun ? "text-slate-500" : "text-slate-400";
const track = isSun ? "bg-slate-100" : "bg-white/10";

const Bar = ({ icon, label, value, pct, from, to }: {
icon: React.ReactNode; label: string; value: string; pct: number; from: string; to: string;
}) => (
<div>
<div className="flex items-center justify-between mb-1">
<span className={`flex items-center gap-1.5 text-[11px] font-semibold uppercase tracking-wide ${sub}`}>{icon}{label}</span>
<span className={`text-[11px] font-bold ${ink} tabular-nums`}>{value}</span>
</div>
<div className={`h-2.5 rounded-full overflow-hidden ${track}`}>
<motion.div
className={`h-full rounded-full bg-gradient-to-r ${from} ${to}`}
initial={{ width: 0 }}
animate={{ width: `${Math.max(pct, 3)}%` }}
transition={{ duration: 0.8, ease: "easeOut" }}
/>
</div>
</div>
);

const action = (label: string, icon: React.ReactNode, href: string, primary?: boolean) => (
<button
onClick={() => router.push(href)}
className={`flex flex-col items-center gap-1.5 py-3 rounded-2xl text-xs font-bold transition-colors ${
primary
? "bg-violet-600 text-white hover:bg-violet-700"
: isSun ? "bg-slate-50 text-slate-700 hover:bg-slate-100" : "bg-white/5 text-slate-200 hover:bg-white/10"
}`}
>
<span className={primary ? "text-white" : "text-violet-500"}>{icon}</span>
{label}
</button>
);

return (
<motion.div
initial={{ opacity: 0, y: 16 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5 }}
className={`w-full rounded-3xl border ${card} p-5 sm:p-6 shadow-sm`}
>
{/* Identity row */}
<div className="flex items-center gap-4">
<div className={`relative w-20 h-20 rounded-2xl flex items-center justify-center shrink-0 ${isSun ? "bg-violet-50" : "bg-violet-500/10"}`}>
<OwlCompanion mood={streak > 0 ? "celebrating" : "idle"} showName={false} className="w-[78px] h-[78px]" />
<span className="absolute -bottom-2 left-1/2 -translate-x-1/2 px-2 py-0.5 rounded-full bg-violet-600 text-white text-[10px] font-bold shadow">
Lv {level}
</span>
</div>
<div className="flex-1 min-w-0">
<h2 className={`text-xl font-bold font-[family-name:var(--font-nunito)] truncate ${ink}`}>{name}</h2>
<p className={`text-xs font-semibold ${sub}`}>{levelTitle(level)}</p>
</div>
<div className={`flex items-center gap-1.5 px-3 py-2 rounded-xl shrink-0 ${isSun ? "bg-amber-50" : "bg-amber-500/10"}`}>
<Coins className="w-4 h-4 text-amber-500" />
<span className="text-sm font-bold text-amber-500 tabular-nums">{gold.toLocaleString()}</span>
</div>
</div>

{/* Vitality bars */}
<div className="grid sm:grid-cols-3 gap-3 mt-5">
<Bar icon={<Flame className="w-3.5 h-3.5 text-rose-500" />} label="Streak" value={`${streak}d`} pct={streakPct} from="from-rose-500" to="to-orange-500" />
<Bar icon={<Star className="w-3.5 h-3.5 text-amber-500" />} label="Level" value={`${xpIntoLevel}/1000`} pct={xpPct} from="from-amber-400" to="to-yellow-500" />
<Bar icon={<Scroll className="w-3.5 h-3.5 text-violet-500" />} label="Dailies" value={`${doneToday}/${totalToday}`} pct={questPct} from="from-violet-500" to="to-fuchsia-500" />
</div>

{/* Do-a-thing actions */}
<div className="grid grid-cols-4 gap-2 mt-5">
{action("Focus", <Timer className="w-5 h-5" />, "/focus", true)}
{action("Trial", <GraduationCap className="w-5 h-5" />, "/quiz")}
{action("Review", <Layers className="w-5 h-5" />, "/flashcards")}
{action("Write", <PenTool className="w-5 h-5" />, "/notes")}
</div>
</motion.div>
);
}
97 changes: 34 additions & 63 deletions brain-trails/components/dashboard/Dashboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import { motion } from "framer-motion";
import QuestLog from "./QuestLog";
import StudyRoom from "./StudyRoom";
import CharacterCard from "./CharacterCard";
import TopStatsBar from "./TopStatsBar";
import LeaderboardPodium from "./LeaderboardPodium";
import ActivityFeed from "./ActivityFeed";
Expand All @@ -13,90 +13,61 @@ import AmbientPlayer from "../ui/AmbientPlayer";
import { useTheme } from "@/context/ThemeContext";

/**
* Dashboard - Glassmorphism Layout
* Centerpiece owl, left study profile, right community hub, bottom adventures
* Dashboard — an "adventurer's board" (Habitica-flavoured): character sheet on
* top, today's dailies front-and-centre, party + subjects to the side.
*/
export default function Dashboard() {
const { theme } = useTheme();
const isSun = theme === "sun";

const panel = isSun
? "bg-white/70 border-white/60 backdrop-blur-xl"
: "bg-white/[0.04] border-white/[0.08] backdrop-blur-xl";

return (
<div className={`min-h-screen relative overflow-hidden bg-transparent ${isSun ? "text-slate-800" : "text-white"}`}>
<BackgroundLayer />
<DashboardTour />

<div className="relative min-h-screen flex flex-col z-10 pt-3 pb-24 px-4 sm:px-6 lg:px-10 max-w-[1440px] mx-auto">
{/* Minimal Top Nav */}
<header className="w-full mb-2 relative">
<div className="relative min-h-screen flex flex-col z-10 pt-3 pb-24 px-4 sm:px-6 lg:px-10 max-w-[1280px] mx-auto">
<header className="w-full mb-4">
<TopStatsBar />
</header>

{/* MAIN CONTENT */}
<main className="flex-1 flex flex-col items-center w-full gap-4 lg:gap-5">

{/* Top Row: Left Panel | Centerpiece Owl | Right Panel */}
<div className="w-full flex flex-col lg:flex-row items-center lg:items-start justify-center gap-4 lg:gap-6 relative">

{/* Left Side - My Study Profile */}
<motion.aside
initial={{ opacity: 0, x: -30 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: 0.1, duration: 0.6 }}
className="w-full lg:w-[280px] xl:w-[300px] order-2 lg:order-1 flex-shrink-0"
>
<SyllabusWidget />
</motion.aside>
{/* Character sheet hero */}
<CharacterCard />

{/* Centerpiece - Owl Mascot & Floating Stats */}
{/* Board: dailies (main) + party/subjects (side) */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-5 mt-5">
<div className="lg:col-span-2 flex flex-col gap-5">
<motion.section
initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ delay: 0.15, duration: 0.7 }}
className="order-1 lg:order-2 flex-1 flex flex-col items-center justify-start relative z-20 min-h-[340px] lg:min-h-[400px]"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.1 }}
className={`rounded-3xl border p-5 sm:p-6 ${panel}`}
>
<StudyRoom />
<QuestLog />
</motion.section>

{/* Right Side - Community Hub */}
<motion.aside
initial={{ opacity: 0, x: 30 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: 0.1, duration: 0.6 }}
className="w-full lg:w-[280px] xl:w-[300px] order-3 lg:order-3 flex-shrink-0"
<motion.section
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.18 }}
className={`rounded-3xl border p-5 sm:p-6 ${panel}`}
>
<ActivityFeed />
</motion.section>
</div>

<div className="flex flex-col gap-5">
<motion.aside initial={{ opacity: 0, x: 24 }} animate={{ opacity: 1, x: 0 }} transition={{ delay: 0.12 }}>
<SyllabusWidget />
</motion.aside>
<motion.aside initial={{ opacity: 0, x: 24 }} animate={{ opacity: 1, x: 0 }} transition={{ delay: 0.2 }}>
<LeaderboardPodium />
</motion.aside>
</div>

{/* Bottom Center - Adventures & Activity */}
<motion.div
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.25, duration: 0.6 }}
className="w-full max-w-4xl order-4 z-20"
>
<div className={`rounded-[28px] p-5 sm:p-7 flex flex-col md:flex-row gap-6 border backdrop-blur-xl ${
isSun
? "bg-white/30 border-white/50 shadow-[0_8px_32px_rgba(0,0,0,0.06)]"
: "bg-white/[0.04] border-white/[0.08] shadow-[0_8px_32px_rgba(0,0,0,0.3)]"
}`}
style={{
boxShadow: isSun
? "0 8px 32px rgba(0,0,0,0.06), inset 0 1px 0 rgba(255,255,255,0.6)"
: "0 8px 32px rgba(0,0,0,0.3), inset 0 1px 0 rgba(255,255,255,0.05), 0 0 0 1px rgba(255,255,255,0.03)"
}}
>
<div className={`flex-1 border-b md:border-b-0 md:border-r pb-5 md:pb-0 md:pr-6 ${
isSun ? "border-slate-200/50" : "border-white/[0.06]"
}`}>
<QuestLog />
</div>
<div className="flex-1">
<ActivityFeed />
</div>
</div>
</motion.div>
</main>
</div>
</div>

<AmbientPlayer />
Expand Down
Loading