| 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> |
| ); |
| } |
|
|