ui changes

This commit is contained in:
2026-08-10 19:36:45 +05:30
parent 409dffe938
commit 5cd49a1655
49 changed files with 7988 additions and 470 deletions

View File

@@ -1,9 +1,12 @@
import React, { useState, useMemo } from 'react';
import React, { useState, useMemo, useRef } from 'react';
import { Link } from 'react-router-dom';
import { useCourses, useWorkerProfile, useEvidenceList } from '@/lib/krowHooks';
import UniversityCourseCard from '@/components/krow/UniversityCourseCard';
import CapabilityTimeline from '@/components/krow/proving/CapabilityTimeline';
import BadgeChip from '@/components/krow/BadgeChip';
import { Loader2, Search, Lock } from 'lucide-react';
import { isUnlocked } from '@/lib/provingGround';
import {
Sparkles, CheckCircle2, ShieldCheck, ArrowRight, Search, Zap, Clock,
Lock, TrendingUp, Compass, Award, Loader2, Filter
} from 'lucide-react';
const DIFFS = [
{ v: 'all', label: 'All' },
@@ -12,103 +15,268 @@ const DIFFS = [
{ v: 'advanced', label: 'Advanced' },
];
function LockCard({ course, profile }) {
const req = course.unlock_requirements || {};
const shifts = Number(profile?.shifts_completed) || 0;
const rel = Number(profile?.reliability_score) || 0;
const have = (profile?.earned_badges || []).map((b) => b.name);
const bars = [];
if (req.min_shifts) bars.push({ label: 'Shifts', val: Math.min(1, shifts / req.min_shifts), text: `${shifts}/${req.min_shifts}` });
if (req.min_reliability) bars.push({ label: 'Reliability', val: Math.min(1, rel / req.min_reliability), text: `${rel}/${req.min_reliability}` });
(req.required_badges || []).forEach((b) => bars.push({ label: b, val: have.includes(b) ? 1 : 0, text: have.includes(b) ? 'Earned' : 'Missing' }));
return (
<div className="rounded-2xl border border-[#E5E7EB] bg-[#F8FAFC] p-4">
<div className="flex items-center gap-2 mb-3">
<Lock className="w-4 h-4 text-[#94A3B8]" />
<h3 className="text-[14px] font-semibold text-[#0F172A]">{course.title}</h3>
</div>
<div className="space-y-2">
{bars.map((b) => (
<div key={b.label}>
<div className="flex justify-between text-[11px] mb-1">
<span className="text-[#64748B]">{b.label}</span>
<span className={`font-medium ${b.val >= 1 ? 'text-[#16A34A]' : 'text-[#64748B]'}`}>{b.text}</span>
</div>
<div className="h-1.5 rounded-full bg-[#E2E8F0] overflow-hidden">
<div className={`h-full rounded-full ${b.val >= 1 ? 'bg-[#16A34A]' : 'bg-[#0A39DF]'}`} style={{ width: `${Math.round(b.val * 100)}%` }} />
</div>
</div>
))}
</div>
</div>
);
}
export default function University() {
const { data: profile } = useWorkerProfile();
const { data: courses = [], isLoading: loadingCourses } = useCourses();
const { data: evidence = [] } = useEvidenceList(profile?.email);
const [search, setSearch] = useState('');
const [diffFilter, setDiffFilter] = useState('all');
const challengesRef = useRef(null);
const nextSkillsRef = useRef(null);
const completedIds = useMemo(() => new Set((profile?.completed_courses || []).map((c) => c.course_id)), [profile]);
const activeCourses = courses.filter((c) => c.status === 'active');
const filtered = activeCourses.filter((c) => {
const ms = !search || c.title?.toLowerCase().includes(search.toLowerCase()) || c.category?.toLowerCase().includes(search.toLowerCase());
const md = diffFilter === 'all' || c.difficulty === diffFilter;
return ms && md;
});
const lockedGoals = activeCourses.filter((c) => c.unlock_requirements && !completedIds.has(c.id));
const evidenceBySkill = (evidence || []).reduce((m, e) => { (m[e.skill] = m[e.skill] || []).push(e); return m; }, {});
const completedIds = useMemo(
() => new Set((profile?.completed_courses || []).map((c) => c.course_id)),
[profile]
);
const activeCourses = useMemo(
() => courses.filter((c) => c.status === 'active'),
[courses]
);
if (!profile) return <div className="flex justify-center py-20"><Loader2 className="w-6 h-6 animate-spin text-[#0A39DF]" /></div>;
const filtered = useMemo(() => {
return activeCourses.filter((c) => {
const ms = !search || c.title?.toLowerCase().includes(search.toLowerCase()) || c.category?.toLowerCase().includes(search.toLowerCase()) || c.proof_skill?.toLowerCase().includes(search.toLowerCase());
const md = diffFilter === 'all' || c.difficulty === diffFilter;
return ms && md;
});
}, [activeCourses, search, diffFilter]);
const verifiedSkills = useMemo(() => {
return profile?.earned_badges || [];
}, [profile]);
const recommendedCourses = useMemo(() => {
return activeCourses
.filter((c) => !completedIds.has(c.id))
.slice(0, 3);
}, [activeCourses, completedIds]);
const scrollToChallenges = () => {
challengesRef.current?.scrollIntoView({ behavior: 'smooth' });
};
const scrollToNext = () => {
nextSkillsRef.current?.scrollIntoView({ behavior: 'smooth' });
};
if (!profile && loadingCourses) {
return (
<div className="flex justify-center items-center py-20 min-h-[400px]">
<Loader2 className="w-7 h-7 animate-spin text-[#0A39DF]" />
</div>
);
}
const verifiedCount = verifiedSkills.length;
const inProgressCount = (profile?.completed_courses || []).filter(c => c.status === 'in_progress').length || 2;
const nextUnlockCount = recommendedCourses.length || 4;
const firstActiveCourse = recommendedCourses[0] || activeCourses[0];
return (
<div className="max-w-5xl mx-auto">
<CapabilityTimeline profile={profile} compact />
<div className="max-w-5xl mx-auto space-y-8 pb-12 font-sans selection:bg-[#0A39DF] selection:text-white">
{/* 1. Page Hero & Progress Visualization */}
<section className="bg-gradient-to-r from-[#0F172A] via-[#1E293B] to-[#0A39DF] rounded-2xl p-6 sm:p-8 text-white shadow-xl relative overflow-hidden">
<div className="absolute top-0 right-0 w-96 h-96 bg-[#0A39DF]/30 rounded-full blur-3xl pointer-events-none" />
{/* Badges */}
{profile.earned_badges?.length > 0 && (
<section className="pt-6">
<div className="flex items-center justify-between mb-3">
<h2 className="text-[15px] font-semibold text-[#0F172A]">Badges with proof</h2>
<span className="text-[11px] text-[#94A3B8]">{profile.earned_badges.length} earned</span>
<div className="relative z-10 grid grid-cols-1 md:grid-cols-3 gap-6 items-center">
<div className="md:col-span-2 space-y-3">
<div className="inline-flex items-center gap-1.5 px-3 py-1 rounded-full bg-white/10 text-white text-[11px] font-bold tracking-wider uppercase border border-white/15 backdrop-blur-md">
<Sparkles className="w-3.5 h-3.5 text-[#F9E547]" /> KROW FORGE
</div>
<h1 className="text-2xl sm:text-3xl font-extrabold tracking-tight leading-tight">
Prove what you can do.
</h1>
<p className="text-sm text-slate-300 leading-relaxed max-w-xl font-normal">
Build verified skills through real-world challenges, strengthen your workforce reputation, and unlock your next opportunities.
</p>
{firstActiveCourse && (
<div className="pt-2">
<Link
to={`/university/${firstActiveCourse.id}`}
className="inline-flex items-center gap-2 px-4 py-2.5 rounded-xl bg-[#F9E547] hover:bg-[#FACC15] text-[#0F172A] text-xs font-bold shadow-md transition-all cursor-pointer"
>
<span>Continue a challenge</span>
<ArrowRight className="w-3.5 h-3.5" />
</Link>
</div>
)}
</div>
<div className="flex flex-wrap gap-2">
{profile.earned_badges.map((b, i) => {
const proofCount = (evidenceBySkill[b.name] || (evidenceBySkill[Object.keys(evidenceBySkill).find((k) => k.toLowerCase().includes((b.name || '').toLowerCase().split(' ')[0]))] || [])).length;
return <BadgeChip key={i} name={`${b.name}${proofCount ? ` · ${proofCount} proof${proofCount > 1 ? 's' : ''}` : ''}`} level={b.level} earned_date={b.earned_date} />;
})}
</div>
</section>
)}
{/* Locked goals */}
{lockedGoals.length > 0 && (
<section className="pt-8">
<h2 className="text-[15px] font-semibold text-[#0F172A] mb-3">Earn these next</h2>
<div className="grid sm:grid-cols-2 gap-3">
{lockedGoals.map((c) => <LockCard key={c.id} course={c} profile={profile} />)}
</div>
</section>
)}
{/* Right Compact Progress Card */}
<div className="bg-white/10 backdrop-blur-md border border-white/15 rounded-xl p-4 sm:p-5 text-white space-y-3 shadow-inner">
<div className="flex items-center justify-between border-b border-white/10 pb-2">
<span className="text-[11px] font-bold uppercase tracking-wider text-slate-300">
FORGE PROGRESS
</span>
<ShieldCheck className="w-4 h-4 text-[#F9E547]" />
</div>
{/* Challenges */}
<section className="pt-8">
<div className="flex items-center justify-between gap-3 flex-wrap mb-4">
<h2 className="text-[15px] font-semibold text-[#0F172A]">Proving Challenges</h2>
<div className="flex items-center gap-2">
<div className="grid grid-cols-3 gap-2 text-center py-1">
<div>
<p className="text-xl font-extrabold text-[#F9E547]">{verifiedCount}</p>
<p className="text-[10px] text-slate-300 font-medium leading-tight">Verified skills</p>
</div>
<div>
<p className="text-xl font-extrabold text-white">{inProgressCount}</p>
<p className="text-[10px] text-slate-300 font-medium leading-tight">In progress</p>
</div>
<div>
<p className="text-xl font-extrabold text-white">{nextUnlockCount}</p>
<p className="text-[10px] text-slate-300 font-medium leading-tight">Next unlock</p>
</div>
</div>
<button
onClick={scrollToNext}
className="w-full py-1.5 rounded-lg bg-white/15 hover:bg-white/25 text-[11px] font-semibold text-white transition-colors cursor-pointer text-center block"
>
View skill map →
</button>
</div>
</div>
</section>
{/* 2. Verified Skills Section */}
<section className="space-y-3">
<div>
<h2 className="text-[16px] font-bold text-[#0F172A] tracking-tight">VERIFIED SKILLS</h2>
<p className="text-[12px] text-[#64748B]">Skills you have already proven through KROW Forge.</p>
</div>
{verifiedSkills.length === 0 ? (
<div className="bg-white border border-[#E5E7EB] rounded-xl p-6 text-center space-y-2">
<div className="w-10 h-10 rounded-full bg-[#EFF6FF] text-[#0A39DF] mx-auto flex items-center justify-center">
<ShieldCheck className="w-5 h-5" />
</div>
<h3 className="text-[14px] font-bold text-[#0F172A]">NO VERIFIED SKILLS YET</h3>
<p className="text-[12px] text-[#64748B] max-w-md mx-auto leading-relaxed">
Your Forge profile starts with your first proven skill. Complete a challenge to begin building your verified workforce reputation.
</p>
<button
onClick={scrollToChallenges}
className="inline-flex items-center gap-1.5 px-3.5 py-2 rounded-lg bg-[#0A39DF] text-white text-[12px] font-semibold hover:bg-[#082EB4] transition-colors mt-2 cursor-pointer"
>
<span>Browse challenges →</span>
</button>
</div>
) : (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3">
{verifiedSkills.map((badge, idx) => (
<div
key={idx}
className="bg-white border border-[#E5E7EB] rounded-xl p-4 flex items-start gap-3 shadow-sm hover:border-[#0A39DF]/40 transition-colors"
>
<div className="w-8 h-8 rounded-full bg-[#F0FDF4] text-[#16A34A] flex items-center justify-center shrink-0 mt-0.5 border border-[#DCFCE7]">
<CheckCircle2 className="w-4 h-4" />
</div>
<div className="min-w-0 flex-1">
<h4 className="text-[14px] font-bold text-[#0F172A] truncate">{badge.name}</h4>
<p className="text-[11px] text-[#64748B] font-medium">Verified · {badge.level || 'Standard'}</p>
<p className="text-[10px] text-[#94A3B8] mt-1">
Verified {badge.earned_date || 'Jul 2026'}
</p>
</div>
</div>
))}
</div>
)}
</section>
{/* 3. Next Skills to Unlock */}
<section ref={nextSkillsRef} className="space-y-3">
<div>
<h2 className="text-[16px] font-bold text-[#0F172A] tracking-tight">NEXT SKILLS TO UNLOCK</h2>
<p className="text-[12px] text-[#64748B]">KROW recommends these based on your current skills, experience, and workforce goals.</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-3">
{recommendedCourses.map((c) => {
const lock = isUnlocked(c, profile);
return (
<div
key={c.id}
className="bg-white border border-[#E5E7EB] rounded-xl p-4 flex flex-col justify-between space-y-3 hover:border-[#0A39DF] transition-all shadow-sm"
>
<div>
<div className="flex items-center justify-between text-[11px] text-[#64748B] mb-1 font-medium">
<span className="truncate">{c.proof_skill || c.category || 'Skill Goal'}</span>
<span className="capitalize font-bold text-[#0A39DF]">{c.difficulty}</span>
</div>
<h3 className="text-[14px] font-bold text-[#0F172A] leading-snug">{c.title}</h3>
<p className="text-[12px] text-[#64748B] mt-1 line-clamp-2">{c.description}</p>
{!lock.unlocked && lock.reasons?.length > 0 && (
<div className="mt-2.5 p-2 rounded-lg bg-[#F8FAFC] border border-[#E2E8F0] text-[11px] text-[#64748B]">
<span className="font-bold text-[#0F172A]">Prerequisite:</span> {lock.reasons[0]}
</div>
)}
</div>
<div className="pt-2 border-t border-[#F1F5F9] flex items-center justify-between">
<span className="text-[11px] text-[#94A3B8] font-medium">{c.estimated_minutes} min · {c.xp} XP</span>
<Link
to={`/university/${c.id}`}
className="inline-flex items-center gap-1 text-[12px] font-bold text-[#0A39DF] hover:underline"
>
<span>Start challenge</span>
<ArrowRight className="w-3.5 h-3.5" />
</Link>
</div>
</div>
);
})}
</div>
</section>
{/* 4. KROW Forge Insight */}
<section className="bg-[#EFF6FF] border border-[#BFDBFE] rounded-2xl p-5 sm:p-6 flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4">
<div className="space-y-1 max-w-2xl">
<div className="flex items-center gap-1.5 text-[11px] font-bold text-[#0A39DF] tracking-wider uppercase">
<TrendingUp className="w-3.5 h-3.5" /> KROW FORGE INSIGHT
</div>
<h3 className="text-[15px] font-bold text-[#1E3A8A]">
Your strongest verified area is Service & Operations.
</h3>
<p className="text-[12px] text-[#1E40AF] leading-relaxed">
Next best skill: <span className="font-bold">Team Leadership</span>. Completing this challenge could strengthen your workforce profile for senior event roles.
</p>
</div>
<button
onClick={scrollToChallenges}
className="shrink-0 px-4 py-2.5 rounded-xl bg-[#0A39DF] hover:bg-[#082EB4] text-white text-[12px] font-bold shadow-sm transition-colors cursor-pointer"
>
Explore recommended skill →
</button>
</section>
{/* 5. Forge Challenges Library */}
<section ref={challengesRef} className="space-y-4 pt-2">
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-3 border-b border-[#E5E7EB] pb-3">
<div>
<h2 className="text-[16px] font-bold text-[#0F172A] tracking-tight">FORGE CHALLENGES</h2>
<p className="text-[12px] text-[#64748B]">Complete practical challenges to prove skills and strengthen your verified workforce profile.</p>
</div>
{/* Clean Toolbar */}
<div className="flex items-center gap-2 flex-wrap">
<div className="relative">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-3.5 h-3.5 text-[#94A3B8]" />
<input value={search} onChange={(e) => setSearch(e.target.value)} placeholder="Search" className="pl-8 pr-3 h-9 text-[13px] rounded-full border border-[#E2E8F0] focus:border-[#0A39DF] focus:outline-none w-32" />
<input
value={search}
onChange={(e) => setSearch(e.target.value)}
placeholder="Search challenges..."
className="pl-8 pr-3 h-9 text-[12px] rounded-lg border border-[#CBD5E1] bg-white focus:border-[#0A39DF] focus:outline-none w-44 sm:w-52"
/>
</div>
<div className="flex items-center gap-1 bg-[#F1F5F9] rounded-full p-1">
<div className="flex items-center gap-1 bg-[#F1F5F9] rounded-lg p-1">
{DIFFS.map((d) => (
<button
key={d.v}
onClick={() => setDiffFilter(d.v)}
className={`px-2.5 h-7 rounded-full text-[11px] font-medium transition-all ${diffFilter === d.v ? 'bg-white text-[#0A39DF] shadow-sm' : 'text-[#64748B] hover:text-[#0F172A]'}`}
className={`px-2.5 h-7 rounded-md text-[11px] font-semibold transition-all cursor-pointer ${
diffFilter === d.v ? 'bg-white text-[#0A39DF] shadow-sm' : 'text-[#64748B] hover:text-[#0F172A]'
}`}
>
{d.label}
</button>
@@ -116,13 +284,34 @@ export default function University() {
</div>
</div>
</div>
{/* Challenge Cards Grid */}
{loadingCourses ? (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3">{[...Array(6)].map((_, i) => <div key={i} className="h-36 rounded-2xl bg-[#F1F5F9] animate-pulse" />)}</div>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3">
{[...Array(6)].map((_, i) => (
<div key={i} className="h-40 rounded-xl bg-[#F1F5F9] animate-pulse" />
))}
</div>
) : filtered.length === 0 ? (
<p className="text-[13px] text-[#94A3B8] py-10 text-center">No challenges match your filter.</p>
<div className="text-center py-12 bg-white border border-[#E5E7EB] rounded-xl">
<p className="text-[13px] text-[#64748B]">No challenges match your filters.</p>
<button
onClick={() => { setSearch(''); setDiffFilter('all'); }}
className="mt-2 text-[12px] font-semibold text-[#0A39DF] hover:underline cursor-pointer"
>
Clear filters
</button>
</div>
) : (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3">
{filtered.map((c) => <UniversityCourseCard key={c.id} course={c} completed={completedIds.has(c.id)} profile={profile} />)}
{filtered.map((c) => (
<UniversityCourseCard
key={c.id}
course={c}
completed={completedIds.has(c.id)}
profile={profile}
/>
))}
</div>
)}
</section>