Initial commit

This commit is contained in:
2026-08-07 23:53:15 +05:30
commit 90f14a73b2
222 changed files with 35000 additions and 0 deletions

131
src/pages/University.jsx Normal file
View File

@@ -0,0 +1,131 @@
import React, { useState, useMemo } from 'react';
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';
const DIFFS = [
{ v: 'all', label: 'All' },
{ v: 'beginner', label: 'Beginner' },
{ v: 'intermediate', label: 'Intermediate' },
{ 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 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; }, {});
if (!profile) return <div className="flex justify-center py-20"><Loader2 className="w-6 h-6 animate-spin text-[#0A39DF]" /></div>;
return (
<div className="max-w-5xl mx-auto">
<CapabilityTimeline profile={profile} compact />
{/* 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>
<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>
)}
{/* 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="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" />
</div>
<div className="flex items-center gap-1 bg-[#F1F5F9] rounded-full 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]'}`}
>
{d.label}
</button>
))}
</div>
</div>
</div>
{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>
) : filtered.length === 0 ? (
<p className="text-[13px] text-[#94A3B8] py-10 text-center">No challenges match your filter.</p>
) : (
<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} />)}
</div>
)}
</section>
</div>
);
}