RYP / src /components /CodingSheet.tsx
Soumya79's picture
Upload 1361 files
f91a684 verified
import { useMemo, useState } from 'react';
import { motion } from 'motion/react';
import { Badge } from '@/components/ui/badge';
import { CodingQuestion, codingQuestions } from '@/data/codingQuestions';
import {
DIFFICULTY_TOTALS,
TOTAL_CODING_QUESTIONS,
getQuestionCaseCount,
getQuestionReward,
getSolvedDifficultyCounts,
} from '@/lib/codingProgress';
import { cn } from '@/lib/utils';
import {
BookOpen,
CheckCircle2,
Code2,
Coins,
Search,
Target,
Trophy,
} from 'lucide-react';
interface CodingSheetProps {
onSelectQuestion: (question: CodingQuestion, mode: 'code' | 'solution') => void;
solvedQuestionIds: string[];
}
const categories = Array.from(new Set(codingQuestions.map((question) => question.category)));
export default function CodingSheet({
onSelectQuestion,
solvedQuestionIds,
}: CodingSheetProps) {
const [searchQuery, setSearchQuery] = useState('');
const [selectedCategory, setSelectedCategory] = useState<string | null>(null);
const solvedSet = useMemo(() => new Set(solvedQuestionIds), [solvedQuestionIds]);
const difficultySolved = useMemo(
() => getSolvedDifficultyCounts(solvedQuestionIds),
[solvedQuestionIds],
);
const filteredQuestions = useMemo(() => {
return codingQuestions.filter((question) => {
const matchesSearch =
question.title.toLowerCase().includes(searchQuery.toLowerCase()) ||
question.category.toLowerCase().includes(searchQuery.toLowerCase());
const matchesCategory = selectedCategory ? question.category === selectedCategory : true;
return matchesSearch && matchesCategory;
});
}, [searchQuery, selectedCategory]);
const solvedCount = solvedSet.size;
const progressPercent = Math.min(
100,
Math.round((solvedCount / TOTAL_CODING_QUESTIONS) * 100),
);
return (
<div className="mx-auto max-w-5xl space-y-8 pb-12">
<div className="flex flex-col gap-4 md:flex-row md:items-start md:justify-between">
<div>
<div className="flex items-center gap-2 text-xs font-black uppercase tracking-[0.28em] text-emerald-400/80">
<Target size={16} />
Problem Set
</div>
<h2 className="mt-3 text-4xl font-black tracking-tight text-white">Coding Question Bank</h2>
<p className="mt-2 max-w-2xl text-sm leading-6 text-slate-400">
Track real progress across all {TOTAL_CODING_QUESTIONS} coding questions with shared solved state, live rewards, and category-by-category completion.
</p>
</div>
</div>
<div className="grid grid-cols-2 gap-4 md:grid-cols-5">
<div className="relative col-span-2 overflow-hidden rounded-[28px] border border-emerald-500/20 bg-emerald-500/5 p-6 backdrop-blur-sm">
<div className="absolute -right-10 -top-10 h-40 w-40 rounded-full bg-emerald-500/10 blur-[60px]" />
<div className="relative z-10">
<div className="flex items-center justify-between">
<span className="text-[11px] font-black uppercase tracking-[0.26em] text-emerald-300/70">Your Progress</span>
<Trophy size={16} className="text-emerald-400" />
</div>
<div className="mt-2 flex items-end gap-2">
<span className="text-4xl font-black text-white">{solvedCount}</span>
<span className="mb-1 text-lg font-bold text-emerald-400/60">/ {TOTAL_CODING_QUESTIONS}</span>
</div>
<div className="mt-4 h-2 w-full overflow-hidden rounded-full bg-white/5">
<motion.div
className="h-full rounded-full bg-gradient-to-r from-emerald-500 to-emerald-400 shadow-[0_0_12px_rgba(16,185,129,0.5)]"
initial={{ width: 0 }}
animate={{ width: `${progressPercent}%` }}
transition={{ duration: 1, ease: 'easeOut' }}
/>
</div>
<div className="mt-1.5 text-right text-xs font-bold text-emerald-400">{progressPercent}% Complete</div>
</div>
</div>
{[
{
label: 'Easy',
solved: difficultySolved.Easy,
total: DIFFICULTY_TOTALS.Easy,
color: 'text-emerald-400',
glow: 'bg-emerald-500/5',
},
{
label: 'Medium',
solved: difficultySolved.Medium,
total: DIFFICULTY_TOTALS.Medium,
color: 'text-amber-400',
glow: 'bg-amber-500/5',
},
{
label: 'Hard',
solved: difficultySolved.Hard,
total: DIFFICULTY_TOTALS.Hard,
color: 'text-rose-400',
glow: 'bg-rose-500/5',
},
].map((card) => (
<div
key={card.label}
className="relative overflow-hidden rounded-[28px] border border-zinc-800/80 bg-zinc-950/50 p-6 backdrop-blur-sm"
>
<div className={cn('absolute -right-6 -top-6 h-24 w-24 rounded-full blur-[40px]', card.glow)} />
<div className="relative z-10 space-y-1">
<span className="text-[10px] font-black uppercase tracking-widest text-zinc-500">
{card.label}
</span>
<div className={cn('text-3xl font-black', card.color)}>{card.solved}</div>
<div className="text-xs font-medium text-zinc-500">{card.solved} / {card.total} solved</div>
</div>
</div>
))}
</div>
<div className="flex flex-col gap-4">
<div className="relative">
<Search className="absolute left-4 top-1/2 -translate-y-1/2 text-zinc-500" size={18} />
<input
type="text"
placeholder="Search problems by name or category..."
value={searchQuery}
onChange={(event) => setSearchQuery(event.target.value)}
className="w-full rounded-2xl border border-zinc-800 bg-zinc-950/60 py-3.5 pl-12 pr-4 text-sm text-white outline-none transition-all placeholder:text-zinc-600 focus:border-emerald-500/50 focus:ring-1 focus:ring-emerald-500/30"
/>
</div>
<div className="flex flex-wrap gap-2">
<button
onClick={() => setSelectedCategory(null)}
className={cn(
'rounded-xl px-4 py-2 text-xs font-bold uppercase tracking-wider transition-all',
!selectedCategory
? 'bg-emerald-500 text-white shadow-[0_0_12px_rgba(16,185,129,0.3)]'
: 'border border-zinc-800 bg-zinc-900/80 text-zinc-400 hover:border-zinc-700 hover:text-zinc-200',
)}
>
All
</button>
{categories.map((category) => (
<button
key={category}
onClick={() => setSelectedCategory(category)}
className={cn(
'rounded-xl px-4 py-2 text-xs font-bold uppercase tracking-wider transition-all',
selectedCategory === category
? 'bg-emerald-500 text-white shadow-[0_0_12px_rgba(16,185,129,0.3)]'
: 'border border-zinc-800 bg-zinc-900/80 text-zinc-400 hover:border-zinc-700 hover:text-zinc-200',
)}
>
{category}
</button>
))}
</div>
</div>
<div className="space-y-10">
{categories.map((category) => {
const categoryQuestions = filteredQuestions.filter((question) => question.category === category);
if (categoryQuestions.length === 0) {
return null;
}
const categorySolved = categoryQuestions.filter((question) => solvedSet.has(question.id)).length;
const categoryPercent =
categoryQuestions.length > 0
? Math.round((categorySolved / categoryQuestions.length) * 100)
: 0;
return (
<section key={category} className="space-y-4">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="h-5 w-1.5 rounded-full bg-gradient-to-b from-emerald-400 to-emerald-600" />
<h2 className="text-lg font-black tracking-tight text-white">{category}</h2>
<Badge className="rounded-full border border-zinc-700 bg-zinc-900 px-3 py-0.5 text-[10px] font-black uppercase tracking-widest text-zinc-400">
{categoryQuestions.length} problems
</Badge>
</div>
<div className="flex items-center gap-3">
<span className="text-xs font-semibold text-zinc-400">
{categorySolved}/{categoryQuestions.length} solved
</span>
<div className="flex items-center gap-1.5">
<div className="h-1.5 w-24 overflow-hidden rounded-full bg-zinc-800">
<div
className="h-full rounded-full bg-emerald-500 transition-all duration-700"
style={{ width: `${categoryPercent}%` }}
/>
</div>
<span className="text-[10px] font-bold text-emerald-400">{categoryPercent}%</span>
</div>
</div>
</div>
<div className="overflow-hidden rounded-[20px] border border-zinc-800/80 bg-zinc-950/50 shadow-xl backdrop-blur-sm">
<div className="overflow-x-auto">
<table className="w-full whitespace-nowrap text-left text-sm">
<thead className="border-b border-zinc-800 bg-zinc-900/80 text-xs text-zinc-500">
<tr>
<th className="w-14 px-4 py-3 text-center font-semibold uppercase tracking-wider">Status</th>
<th className="px-4 py-3 font-semibold uppercase tracking-wider">Title</th>
<th className="w-28 px-4 py-3 text-center font-semibold uppercase tracking-wider">Test Cases</th>
<th className="w-24 px-4 py-3 font-semibold uppercase tracking-wider">Difficulty</th>
<th className="w-24 px-4 py-3 text-center font-semibold uppercase tracking-wider">Reward</th>
<th className="w-40 px-4 py-3 text-right font-semibold uppercase tracking-wider">Actions</th>
</tr>
</thead>
<tbody>
{categoryQuestions.map((question, index) => {
const isSolved = solvedSet.has(question.id);
const reward = getQuestionReward(question.difficulty);
const testCaseCount = getQuestionCaseCount(question);
return (
<motion.tr
key={question.id}
initial={{ opacity: 0, y: 8 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: index * 0.025 }}
className={cn(
'group cursor-pointer border-b border-zinc-800/50 transition-colors hover:bg-zinc-800/60',
index % 2 === 0 ? 'bg-zinc-900/30' : 'bg-zinc-900/10',
)}
>
<td className="px-4 py-3.5 text-center align-middle">
<div className="flex justify-center">
{isSolved ? (
<div className="flex h-6 w-6 items-center justify-center rounded-full bg-emerald-500/20 text-emerald-400">
<CheckCircle2 size={14} strokeWidth={3} />
</div>
) : (
<div className="flex h-6 w-6 items-center justify-center rounded-full border-2 border-zinc-600 text-zinc-400">
<span className="text-[10px] font-bold">{index + 1}</span>
</div>
)}
</div>
</td>
<td
className={cn(
'px-4 py-3.5 font-semibold transition-colors',
isSolved ? 'text-emerald-400' : 'text-white group-hover:text-white',
)}
>
{question.title}
</td>
<td className="px-4 py-3.5 text-center font-medium text-zinc-400">
{testCaseCount} checks
</td>
<td className="px-4 py-3.5">
<span
className={cn(
'text-xs font-black tracking-wide',
question.difficulty === 'Easy'
? 'text-emerald-400'
: question.difficulty === 'Medium'
? 'text-amber-400'
: 'text-rose-400',
)}
>
{question.difficulty}
</span>
</td>
<td className="px-4 py-3.5 text-center">
<div className="inline-flex items-center gap-1 rounded-lg border border-amber-500/20 bg-amber-500/10 px-2 py-1 text-amber-400">
<Coins size={11} />
<span className="text-xs font-black">{reward}</span>
</div>
</td>
<td className="px-4 py-3.5 text-right">
<div className="flex items-center justify-end gap-2">
<button
onClick={() => onSelectQuestion(question, 'solution')}
className="flex items-center gap-1.5 rounded-xl border border-zinc-700 bg-zinc-800/80 px-3 py-1.5 text-xs font-bold text-zinc-300 transition-all hover:border-zinc-600 hover:bg-zinc-700 hover:text-white"
>
<BookOpen size={12} />
Solution
</button>
<button
onClick={() => onSelectQuestion(question, 'code')}
className="flex items-center gap-1.5 rounded-xl bg-emerald-600 px-3 py-1.5 text-xs font-bold text-white shadow-lg shadow-emerald-900/30 transition-all hover:bg-emerald-500 hover:shadow-emerald-500/20"
>
<Code2 size={12} />
Solve
</button>
</div>
</td>
</motion.tr>
);
})}
</tbody>
</table>
</div>
</div>
</section>
);
})}
{filteredQuestions.length === 0 && (
<div className="flex flex-col items-center justify-center gap-4 rounded-[28px] border border-zinc-800 bg-zinc-950/40 py-20 text-zinc-600">
<Search size={32} className="opacity-40" />
<p className="text-base font-semibold">No problems match your search</p>
</div>
)}
</div>
</div>
);
}