RYP / src /components /CodingSection.tsx
Soumya79's picture
Upload 1361 files
f91a684 verified
import { useEffect, useMemo, useState } from 'react';
import { motion, AnimatePresence } from 'motion/react';
import { codingQuestions, type CodingQuestion } from '@/data/codingQuestions';
import { fetchDSAQuestions } from '@/lib/dsaQuestionsClient';
import { getQuestionReward } from '@/lib/codingProgress';
import type { ProgressLeaderboardEntry } from '@/lib/authClient';
import { cn } from '@/lib/utils';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { useSubscription } from '@/contexts/SubscriptionContext';
import ProgressHeader from './ProgressHeader';
import {
ArrowLeft,
BookOpen,
CheckCircle2,
ChevronLeft,
ChevronRight,
Circle,
Code2,
Coins,
Filter,
FolderOpen,
Layers,
Search,
Terminal,
Trophy,
Zap, Lock
} from 'lucide-react';
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
interface CodingSectionProps {
onSelectQuestion: (question: CodingQuestion, mode: 'code' | 'solution') => void;
solvedQuestionIds: string[];
solvedCount?: number;
userId?: string;
leaderboard?: ProgressLeaderboardEntry[];
onViewFullLeaderboard?: () => void;
}
const BLIND_56_DATA: Record<string, string[]> = {
Array: ['Pair Sum Target', 'Optimal Stock Trading', 'Duplicate Finder', 'Array Product Exclusion', 'Maximum Sum Segment', 'Maximum Product Subarray', 'Find Minimum in Rotated Sorted Array', 'Search in Rotated Sorted Array'],
Binary: ['Sum of Two Integers', 'Number of 1 Bits', 'Counting Bits', 'Missing Number', 'Reverse Bits'],
DP: ['Climbing Stairs', 'Coin Change II', 'Longest Increasing Subsequence', 'Longest Common Subsequence', 'Word Break', 'Combination Sum', 'Maximum Sum of Non-Adjacent Elements', 'House Robber', 'Decode Ways'],
Graph: ['Clone Graph', 'Course Schedule I', 'Pacific Atlantic Water Flow', 'Number of Islands', 'Longest Consecutive Sequence', 'Alien Dictionary', 'Graph Valid Tree'],
Interval: ['Insert Interval', 'Merge Intervals', 'Non-overlapping Intervals', 'Repeating and Missing Number'],
'Linked List': ['Reverse a Linked List', 'Detect a Loop in Linked List', 'Merge Two Sorted Lists', 'Merge K Sorted Lists', 'Remove Nth Node From End of List'],
Matrix: ['Set Matrix Zeroes', 'Spiral Matrix', 'Rotate Image'],
Tree: ['Maximum Depth in BT', 'Check if two trees are identical', 'Invert/Flip Binary Tree', 'Maximum Path Sum', 'Level Order Traversal', 'Serialize and De-serialize BT'],
String: ['Longest Substring Without Repeating Characters', 'Longest Repeating Character Replacement', 'Minimum Window Substring', 'Valid Anagram', 'Group Words by Anagrams', 'Balanced Paranthesis', 'Palindrome Number', 'Longest Palindromic Substring', 'Palindromic Substrings'],
};
type SheetCategory = {
name: string;
count: number;
completedCount: number;
progress: number;
};
const COMPANY_DOMAINS: Record<string, string> = {
accenture: 'accenture.com',
adobe: 'adobe.com',
airbnb: 'airbnb.com',
amazon: 'amazon.com',
apple: 'apple.com',
atlassian: 'atlassian.com',
baidu: 'baidu.com',
bloomberg: 'bloomberg.com',
bookingcom: 'booking.com',
bytedance: 'bytedance.com',
capgemini: 'capgemini.com',
cisco: 'cisco.com',
citadel: 'citadel.com',
citrix: 'citrix.com',
codeforces: 'codeforces.com',
codesignal: 'codesignal.com',
coursera: 'coursera.org',
cred: 'cred.club',
databricks: 'databricks.com',
dhl: 'dhl.com',
directi: 'directi.com',
doordash: 'doordash.com',
dropbox: 'dropbox.com',
expedia: 'expedia.com',
facebook: 'facebook.com',
flipkart: 'flipkart.com',
freshworks: 'freshworks.com',
goldmansachs: 'goldmansachs.com',
google: 'google.com',
grab: 'grab.com',
hackerrank: 'hackerrank.com',
hcl: 'hcltech.com',
ibm: 'ibm.com',
infosys: 'infosys.com',
intuit: 'intuit.com',
janestreet: 'janestreet.com',
jio: 'jio.com',
jpmorgan: 'jpmorgan.com',
juspay: 'juspay.in',
leetcode: 'leetcode.com',
linkedin: 'linkedin.com',
lyft: 'lyft.com',
meta: 'meta.com',
microsoft: 'microsoft.com',
mindtree: 'ltimindtree.com',
morganstanley: 'morganstanley.com',
netflix: 'netflix.com',
nvidia: 'nvidia.com',
ola: 'olacabs.com',
oracle: 'oracle.com',
palantir: 'palantir.com',
paypal: 'paypal.com',
paytm: 'paytm.com',
pinterest: 'pinterest.com',
qualcomm: 'qualcomm.com',
quora: 'quora.com',
razorpay: 'razorpay.com',
robinhood: 'robinhood.com',
salesforce: 'salesforce.com',
samsung: 'samsung.com',
sap: 'sap.com',
shopee: 'shopee.com',
shopify: 'shopify.com',
snap: 'snap.com',
snapchat: 'snapchat.com',
snapdeal: 'snapdeal.com',
spotify: 'spotify.com',
stripe: 'stripe.com',
swiggy: 'swiggy.com',
tcs: 'tcs.com',
tomtom: 'tomtom.com',
twitch: 'twitch.tv',
twitter: 'twitter.com',
uber: 'uber.com',
walmart: 'walmart.com',
wipro: 'wipro.com',
yahoo: 'yahoo.com',
yelp: 'yelp.com',
zenefits: 'zenefits.com',
zillow: 'zillow.com',
zoho: 'zoho.com',
};
const COMPANY_ICON_SLUGS: Record<string, string> = {
adobe: 'adobe',
airbnb: 'airbnb',
amazon: 'amazon',
apple: 'apple',
atlassian: 'atlassian',
baidu: 'baidu',
bookingcom: 'bookingdotcom',
bytedance: 'bytedance',
cisco: 'cisco',
codeforces: 'codeforces',
coursera: 'coursera',
databricks: 'databricks',
dhl: 'dhl',
doordash: 'doordash',
dropbox: 'dropbox',
expedia: 'expedia',
facebook: 'facebook',
flipkart: 'flipkart',
freshworks: 'freshworks',
google: 'google',
hackerrank: 'hackerrank',
ibm: 'ibm',
infosys: 'infosys',
intuit: 'intuit',
leetcode: 'leetcode',
linkedin: 'linkedin',
lyft: 'lyft',
meta: 'meta',
microsoft: 'microsoft',
netflix: 'netflix',
nvidia: 'nvidia',
oracle: 'oracle',
paypal: 'paypal',
pinterest: 'pinterest',
qualcomm: 'qualcomm',
quora: 'quora',
robinhood: 'robinhood',
salesforce: 'salesforce',
samsung: 'samsung',
sap: 'sap',
shopify: 'shopify',
snapchat: 'snapchat',
spotify: 'spotify',
stripe: 'stripe',
twitch: 'twitch',
twitter: 'x',
uber: 'uber',
walmart: 'walmart',
wipro: 'wipro',
yahoo: 'yahoo',
yelp: 'yelp',
zillow: 'zillow',
zoho: 'zoho',
};
const COMPANY_LOGO_OVERRIDES: Record<string, string[]> = {
apple: [
'https://cdn.simpleicons.org/apple/white',
],
google: [
'https://www.google.com/images/branding/googleg/1x/googleg_standard_color_128dp.png',
],
ibm: [
'/images/ibm-white.svg',
],
ola: [
'https://cdn.simpleicons.org/olacabs/white',
],
paytm: [
'https://upload.wikimedia.org/wikipedia/commons/2/24/Paytm_Logo_%28standalone%29.svg',
],
samsung: [
'https://cdn.simpleicons.org/samsung/white',
],
uber: [
'https://cdn.simpleicons.org/uber/white',
],
wipro: [
'https://cdn.simpleicons.org/wipro/white',
],
};
export default function CodingSection({
onSelectQuestion,
solvedQuestionIds,
solvedCount = 0,
userId = '',
leaderboard = [],
onViewFullLeaderboard,
}: CodingSectionProps) {
const [searchQuery, setSearchQuery] = useState('');
const [currentPage, setCurrentPage] = useState(1);
const ITEMS_PER_PAGE = 30;
const [selectedForDialog, setSelectedForDialog] = useState<CodingQuestion | null>(null);
const [selectedSheet, setSelectedSheet] = useState<string | null>(null);
const [showSheets, setShowSheets] = useState(false);
const [sheetContext, setSheetContext] = useState<'all' | 'blind56' | 'rypquest'>('all');
const [questions, setQuestions] = useState<CodingQuestion[]>([]);
const [isLoadingQuestions, setIsLoadingQuestions] = useState(true);
const [questionsError, setQuestionsError] = useState<string | null>(null);
const solvedSet = useMemo(() => new Set(solvedQuestionIds), [solvedQuestionIds]);
useEffect(() => {
let cancelled = false;
async function loadQuestions() {
setIsLoadingQuestions(true);
setQuestionsError(null);
try {
const mongoQuestions = await fetchDSAQuestions();
if (cancelled) return;
setQuestions(mongoQuestions.length > 0 ? mongoQuestions : codingQuestions);
if (mongoQuestions.length === 0) {
setQuestionsError('MongoDB returned no DSA questions, so local fallback data is shown.');
}
} catch (error) {
if (cancelled) return;
console.error('Failed to load DSA questions from MongoDB:', error);
setQuestions(codingQuestions);
setQuestionsError(error instanceof Error ? error.message : 'Failed to load DSA questions from MongoDB.');
} finally {
if (!cancelled) {
setIsLoadingQuestions(false);
}
}
}
loadQuestions();
return () => {
cancelled = true;
};
}, []);
const categories = useMemo<SheetCategory[]>(() => {
if (sheetContext === 'blind56') {
return Object.entries(BLIND_56_DATA).map(([name, titles]) => {
const questionsInCategory = questions.filter((question) =>
titles.some((title) => matchesBlind56Question(question, title)),
);
const completedCount = questionsInCategory.filter((question) => solvedSet.has(question.id)).length;
return {
name,
count: questionsInCategory.length,
completedCount,
progress: questionsInCategory.length > 0 ? (completedCount / questionsInCategory.length) * 100 : 0,
};
});
}
return Array.from(new Set(questions.map((question) => question.category))).map((category) => {
const questionsInCategory = questions.filter((question) => question.category === category);
const completedCount = questionsInCategory.filter((question) => solvedSet.has(question.id)).length;
return {
name: category,
count: questionsInCategory.length,
completedCount,
progress: questionsInCategory.length > 0 ? (completedCount / questionsInCategory.length) * 100 : 0,
};
});
}, [questions, sheetContext, solvedSet]);
const rootQuestions = useMemo(() => {
const normalizedSearch = searchQuery.toLowerCase();
return questions.filter((question) => (
question.title.toLowerCase().includes(normalizedSearch) ||
question.category.toLowerCase().includes(normalizedSearch) ||
question.difficulty.toLowerCase().includes(normalizedSearch) ||
(question.companies ?? []).some((company) => company.toLowerCase().includes(normalizedSearch))
));
}, [questions, searchQuery]);
const filteredQuestions = useMemo(() => {
return questions.filter((question) => {
const matchesSearch =
question.title.toLowerCase().includes(searchQuery.toLowerCase()) ||
question.category.toLowerCase().includes(searchQuery.toLowerCase()) ||
(question.companies ?? []).some((company) => company.toLowerCase().includes(searchQuery.toLowerCase()));
if (!matchesSearch) {
return false;
}
if (selectedSheet) {
if (sheetContext === 'blind56') {
const titles = BLIND_56_DATA[selectedSheet] || [];
return titles.some((title) => matchesBlind56Question(question, title));
}
return question.category === selectedSheet;
}
if (sheetContext === 'blind56') {
const blind56Titles = Object.values(BLIND_56_DATA).flat();
return blind56Titles.some((title) => matchesBlind56Question(question, title));
}
return true;
});
}, [questions, searchQuery, selectedSheet, sheetContext]);
const progressStats = useMemo(() => {
const easyQuestions = filteredQuestions.filter((question) => question.difficulty === 'Easy');
const mediumQuestions = filteredQuestions.filter((question) => question.difficulty === 'Medium');
const hardQuestions = filteredQuestions.filter((question) => question.difficulty === 'Hard');
return {
totalQuestions: filteredQuestions.length,
totalSolved: filteredQuestions.filter((question) => solvedSet.has(question.id)).length,
categories: [
{
label: 'EASY',
solved: easyQuestions.filter((question) => solvedSet.has(question.id)).length,
total: easyQuestions.length,
color: 'text-emerald-400',
barColor: 'bg-emerald-500',
chartColor: '#10b981',
},
{
label: 'MED.',
solved: mediumQuestions.filter((question) => solvedSet.has(question.id)).length,
total: mediumQuestions.length,
color: 'text-amber-400',
barColor: 'bg-amber-500',
chartColor: '#f59e0b',
},
{
label: 'HARD',
solved: hardQuestions.filter((question) => solvedSet.has(question.id)).length,
total: hardQuestions.length,
color: 'text-rose-400',
barColor: 'bg-rose-500',
chartColor: '#ef4444',
},
],
};
}, [filteredQuestions, solvedSet]);
const leaderboardEntries = leaderboard;
const visibleEntries = useMemo(() => leaderboardEntries.slice(0, 3), [leaderboardEntries]);
const currentUserEntry = leaderboardEntries.find((entry) => entry.id === userId) || leaderboardEntries.find((entry) => entry.isCurrentUser);
const leaderboardRank = currentUserEntry?.rank ?? leaderboardEntries.length + 1;
const leaderboardGap = useMemo(() => {
const currentIndex = leaderboardEntries.findIndex((entry) => entry.id === userId || entry.isCurrentUser);
if (currentIndex <= 0) return 0;
return Math.max(0, leaderboardEntries[currentIndex - 1].score - leaderboardEntries[currentIndex].score + 1);
}, [leaderboardEntries, userId]);
const codingSolvedIds = useMemo(() => solvedQuestionIds.filter(id => !id.startsWith('sd-') && !id.startsWith('cs-') && !id.startsWith('apt-')), [solvedQuestionIds]);
const recordedSolvedCount = Math.max(solvedCount, codingSolvedIds.length);
return (
<div className="mx-auto w-full max-w-[1400px] pb-12">
{/* MAIN GRID: Content + Sidebar */}
<div className="grid grid-cols-1 xl:grid-cols-[1fr_340px] gap-6 xl:gap-8 items-start">
<div className="space-y-6 min-h-0">
{/* Header */}
<div className="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
<div className="flex items-center gap-3">
{showSheets || selectedSheet ? (
<Button
variant="ghost"
size="icon"
onClick={() => {
if (selectedSheet) {
setSelectedSheet(null);
} else {
setShowSheets(false);
}
setCurrentPage(1);
}}
className="mr-2 h-12 w-12 text-zinc-400 hover:bg-white/5 hover:text-white"
>
<ArrowLeft size={24} />
</Button>
) : (
<div className="flex h-12 w-12 items-center justify-center rounded-2xl border border-cyan-500/20 bg-cyan-500/10 text-cyan-400">
<Code2 size={24} />
</div>
)}
<div>
<h2 className="text-2xl font-black tracking-tight text-white">
{!showSheets
? 'Coding Hub'
: selectedSheet
? selectedSheet
: sheetContext === 'all'
? 'All Questions'
: sheetContext === 'blind56'
? 'Blind 56 Sheets'
: 'RYP Quest Sheet'}
</h2>
</div>
</div>
<div className="flex items-center gap-3">
<div className="relative group">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 text-zinc-600 transition-colors group-focus-within:text-cyan-400" size={18} />
<input
type="text"
placeholder="Search problems..."
value={searchQuery}
onChange={(event) => { setSearchQuery(event.target.value); setCurrentPage(1); }}
className="w-full rounded-xl border border-zinc-800/80 bg-[#111] py-2.5 pl-10 pr-4 text-sm text-zinc-200 transition-all focus:border-cyan-500/50 focus:outline-none focus:ring-2 focus:ring-cyan-500/10 md:w-72 placeholder:text-zinc-600"
/>
</div>
<Button variant="outline" size="icon" className="border-zinc-800/80 bg-[#111] text-zinc-500 hover:text-cyan-400 hover:border-cyan-500/30">
<Filter size={18} />
</Button>
</div>
</div>
{questionsError && (
<div className="rounded-2xl border border-amber-500/20 bg-amber-500/5 px-4 py-3 text-xs font-semibold text-amber-300">
{questionsError}
</div>
)}
<AnimatePresence mode="wait">
{!showSheets ? (
<motion.div
key="root"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, scale: 0.95 }}
className="grid grid-cols-1 gap-4 md:grid-cols-2"
>
<motion.div
whileHover={{ y: -4, scale: 1.01 }}
className="group relative cursor-pointer overflow-hidden rounded-2xl border border-teal-500/10 bg-[#0d0d0d] p-6 transition-all hover:border-teal-500/30"
onClick={() => {
setSheetContext('all');
setShowSheets(true);
setCurrentPage(1);
}}
>
<div className="absolute -top-8 -right-8 h-28 w-28 rounded-full blur-3xl opacity-10 bg-teal-500 transition-opacity group-hover:opacity-20" />
<div className="absolute bottom-0 left-0 right-0 h-[2px] bg-gradient-to-r from-teal-400 to-cyan-500 opacity-0 transition-opacity group-hover:opacity-100" />
<div className="flex items-center gap-4">
<div className="flex h-14 w-14 shrink-0 items-center justify-center rounded-2xl bg-gradient-to-br from-teal-400/15 to-cyan-500/15 transition-transform group-hover:scale-110">
<FolderOpen size={28} className="text-teal-400" />
</div>
<div className="flex-1">
<h3 className="text-lg font-black text-white transition-colors group-hover:text-teal-400">All Questions</h3>
<p className="text-xs text-zinc-600 font-semibold mt-0.5">
{isLoadingQuestions ? 'Loading from MongoDB...' : `${questions.length} problems · All categories`}
</p>
</div>
<ChevronRight size={20} className="text-zinc-700 transition-all group-hover:text-teal-400 group-hover:translate-x-1" />
</div>
</motion.div>
<motion.div
whileHover={{ y: -4, scale: 1.01 }}
className="group relative cursor-pointer overflow-hidden rounded-2xl border border-violet-500/10 bg-[#0d0d0d] p-6 transition-all hover:border-violet-500/30"
onClick={() => {
setSheetContext('blind56');
setShowSheets(true);
setSelectedSheet(null);
setCurrentPage(1);
}}
>
<div className="absolute -top-8 -right-8 h-28 w-28 rounded-full blur-3xl opacity-10 bg-violet-500 transition-opacity group-hover:opacity-20" />
<div className="absolute bottom-0 left-0 right-0 h-[2px] bg-gradient-to-r from-violet-400 to-purple-600 opacity-0 transition-opacity group-hover:opacity-100" />
<div className="flex items-center gap-4">
<div className="flex h-14 w-14 shrink-0 items-center justify-center rounded-2xl bg-gradient-to-br from-violet-400/15 to-purple-600/15 transition-transform group-hover:scale-110">
<Zap size={28} className="text-violet-400" />
</div>
<div className="flex-1">
<h3 className="text-lg font-black text-white transition-colors group-hover:text-violet-400">Blind 56</h3>
<p className="text-xs text-zinc-600 font-semibold mt-0.5">Curated interview essentials</p>
</div>
<ChevronRight size={20} className="text-zinc-700 transition-all group-hover:text-violet-400 group-hover:translate-x-1" />
</div>
</motion.div>
<motion.div
whileHover={{ y: -4, scale: 1.01 }}
className="group relative cursor-pointer overflow-hidden rounded-2xl border border-amber-500/10 bg-[#0d0d0d] p-6 transition-all hover:border-amber-500/30"
onClick={() => {
setSheetContext('rypquest');
setShowSheets(true);
setSelectedSheet(null);
setCurrentPage(1);
}}
>
<div className="absolute -top-8 -right-8 h-28 w-28 rounded-full blur-3xl opacity-10 bg-amber-500 transition-opacity group-hover:opacity-20" />
<div className="absolute bottom-0 left-0 right-0 h-[2px] bg-gradient-to-r from-amber-400 to-orange-500 opacity-0 transition-opacity group-hover:opacity-100" />
<div className="flex items-center gap-4">
<div className="flex h-14 w-14 shrink-0 items-center justify-center rounded-2xl bg-gradient-to-br from-amber-400/15 to-orange-500/15 transition-transform group-hover:scale-110">
<Trophy size={28} className="text-amber-400" />
</div>
<div className="flex-1">
<h3 className="text-lg font-black text-white transition-colors group-hover:text-amber-400">RYP Quest Sheet</h3>
<p className="text-xs text-zinc-600 font-semibold mt-0.5">Platform-curated problem set</p>
</div>
<ChevronRight size={20} className="text-zinc-700 transition-all group-hover:text-amber-400 group-hover:translate-x-1" />
</div>
</motion.div>
<motion.div
initial={{ opacity: 0, y: 12 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.35, delay: 0.08 }}
className="space-y-3 md:col-span-2"
>
<div className="flex flex-wrap items-end justify-between gap-3 px-1">
<div>
<h3 className="text-lg font-black tracking-tight text-white">Problems</h3>
<p className="mt-0.5 text-xs font-semibold text-zinc-600">
{isLoadingQuestions ? 'Loading questions...' : `${rootQuestions.length} questions`}
</p>
</div>
<button
type="button"
onClick={() => {
setSheetContext('all');
setShowSheets(true);
setCurrentPage(1);
}}
className="inline-flex items-center gap-1.5 rounded-xl border border-cyan-500/20 bg-cyan-500/5 px-3 py-2 text-[10px] font-black uppercase tracking-wider text-cyan-400 transition-all hover:border-cyan-500/40 hover:bg-cyan-500/10"
>
View Sheets
<ChevronRight size={13} />
</button>
</div>
<QuestionTable
questions={rootQuestions}
solvedSet={solvedSet}
onOpenQuestion={setSelectedForDialog}
currentPage={currentPage}
onPageChange={setCurrentPage}
itemsPerPage={12}
emptyMessage={isLoadingQuestions ? 'Loading DSA questions...' : 'No DSA questions match this view.'}
/>
</motion.div>
</motion.div>
) : sheetContext === 'all' && !selectedSheet ? (
<motion.div
key="sheets"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
className="grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-3"
>
{categories.map((category, index) => (
<motion.div
key={category.name}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: index * 0.05 }}
>
<Card
className="group relative flex h-full cursor-pointer flex-col overflow-hidden border-zinc-800 bg-zinc-900/50 transition-all hover:border-emerald-500/50 hover:bg-zinc-900"
onClick={() => setSelectedSheet(category.name)}
>
<CardContent className="flex h-full flex-col p-8">
<div className="absolute right-0 top-0 p-8 text-emerald-500 opacity-5 transition-opacity group-hover:opacity-10">
<Layers size={100} />
</div>
<div className="mb-6 flex items-start justify-between">
<div className="flex h-14 w-14 items-center justify-center rounded-2xl bg-emerald-500/10 text-emerald-400 transition-transform group-hover:scale-110 group-hover:bg-emerald-500/20">
<Layers size={28} />
</div>
<Badge variant="outline" className="border-emerald-500/20 bg-emerald-500/10 text-emerald-300">
{category.completedCount}/{category.count}
</Badge>
</div>
<h3 className="mb-2 text-xl font-bold text-white transition-colors group-hover:text-emerald-400">
{category.name} Sheet
</h3>
<div className="mt-auto space-y-4 pt-4">
<div className="h-1.5 w-full overflow-hidden rounded-full bg-zinc-800/80">
<div
className="h-full rounded-full bg-emerald-500 transition-all duration-700"
style={{ width: `${Math.round(category.progress)}%` }}
/>
</div>
<div className="relative z-10 flex items-center justify-between border-t border-white/5 pt-4">
<span className="text-xs font-bold uppercase tracking-widest text-zinc-500">
{category.count} Questions
</span>
<div className="flex items-center gap-1 text-xs font-bold uppercase tracking-wider text-emerald-400">
Open Sheet
<ChevronRight size={14} className="transition-transform group-hover:translate-x-1" />
</div>
</div>
</div>
</CardContent>
</Card>
</motion.div>
))}
</motion.div>
) : (
<motion.div
key="questions"
initial={{ opacity: 0, x: 20 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: -20 }}
className="space-y-8"
>
{sheetContext === 'blind56' ? (
Object.entries(BLIND_56_DATA).map(([category, titles]) => {
const categoryQuestions = filteredQuestions.filter((question) =>
titles.some((title) => matchesBlind56Question(question, title)),
);
if (categoryQuestions.length === 0) {
return null;
}
return (
<div key={category} className="space-y-4">
<div className="flex items-center gap-3 px-2">
<h3 className="flex items-center gap-2 text-lg font-bold uppercase tracking-widest text-purple-400">
<Layers size={18} />
{category}
</h3>
<div className="h-px flex-1 bg-zinc-800/50" />
<Badge variant="outline" className="border-purple-500/20 bg-purple-500/10 text-purple-400">
{categoryQuestions.filter((question) => solvedSet.has(question.id)).length} / {categoryQuestions.length}
</Badge>
</div>
<QuestionTable
questions={categoryQuestions}
solvedSet={solvedSet}
onOpenQuestion={setSelectedForDialog}
/>
</div>
);
})
) : (
<QuestionTable
questions={filteredQuestions}
solvedSet={solvedSet}
onOpenQuestion={setSelectedForDialog}
currentPage={currentPage}
onPageChange={setCurrentPage}
itemsPerPage={ITEMS_PER_PAGE}
/>
)}
</motion.div>
)}
</AnimatePresence>
<Dialog open={Boolean(selectedForDialog)} onOpenChange={(open) => !open && setSelectedForDialog(null)}>
<DialogContent className="border-zinc-800 bg-zinc-950 text-zinc-200 sm:max-w-[425px]">
<DialogHeader>
<DialogTitle className="text-xl font-bold text-white">{selectedForDialog?.title}</DialogTitle>
</DialogHeader>
<div className="grid grid-cols-2 gap-4 py-4">
<Button
onClick={() => {
if (selectedForDialog) {
onSelectQuestion(selectedForDialog, 'code');
}
setSelectedForDialog(null);
}}
className="group flex h-24 flex-col gap-2 border border-zinc-800 bg-zinc-900 text-zinc-200 hover:border-emerald-500/50 hover:bg-zinc-800"
>
<Terminal className="text-emerald-400 transition-transform group-hover:scale-110" size={24} />
<span className="font-bold">Solve Code</span>
</Button>
<Button
onClick={() => {
if (selectedForDialog) {
onSelectQuestion(selectedForDialog, 'solution');
}
setSelectedForDialog(null);
}}
className="group flex h-24 flex-col gap-2 border border-zinc-800 bg-zinc-900 text-zinc-200 hover:border-cyan-500/50 hover:bg-zinc-800"
>
<BookOpen className="text-cyan-400 transition-transform group-hover:scale-110" size={24} />
<span className="font-bold">View Solution</span>
</Button>
</div>
</DialogContent>
</Dialog>
</div>
{/* Right Sidebar: Session Progress + Leaderboard */}
<div className="flex flex-col gap-6 xl:sticky xl:top-6 w-full">
{/* Leaderboard Rank - Compact */}
<motion.div
initial={{ opacity: 0, y: 12 }}
animate={{ opacity: 1, y: 0 }}
className="relative overflow-hidden rounded-2xl border border-violet-500/10 bg-[#0d0d0d] p-4 backdrop-blur-xl shadow-xl"
>
<div className="absolute bottom-0 left-0 right-0 h-[2px] bg-gradient-to-r from-violet-400 to-purple-600" />
<div className="flex items-center gap-3">
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-xl bg-gradient-to-br from-violet-400/20 to-purple-600/20">
<Zap size={18} className="text-violet-400" />
</div>
<div className="flex-1 min-w-0">
<p className="text-[9px] uppercase tracking-[0.2em] text-zinc-600 font-black">Rank</p>
<p className="text-2xl font-black text-white tracking-tighter" style={{ fontFamily: 'JetBrains Mono, monospace' }}>#{leaderboardRank}</p>
</div>
<span className="text-[8px] font-black uppercase tracking-[0.2em] text-violet-500/60 border border-violet-500/15 bg-violet-500/5 rounded-full px-2 py-0.5">● LIVE</span>
</div>
{leaderboardGap > 0 && <p className="text-[10px] text-zinc-600 mt-2 font-semibold">{leaderboardGap} pts to next</p>}
</motion.div>
{/* Session Progress */}
<ProgressHeader
totalQuestions={progressStats.totalQuestions}
totalSolved={progressStats.totalSolved}
categories={progressStats.categories}
/>
{/* Leaderboard */}
<motion.div
initial={{ opacity: 0, x: 20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ duration: 0.5, delay: 0.2 }}
className="rounded-2xl border border-purple-500/10 bg-[#0d0d0d] p-5 backdrop-blur-xl shadow-xl overflow-hidden"
>
<div className="absolute bottom-0 left-0 right-0 h-[2px] bg-gradient-to-r from-purple-500 to-violet-600" />
<div className="flex items-center justify-between mb-4">
<div className="flex items-center gap-2">
<div className="flex h-8 w-8 items-center justify-center rounded-xl bg-gradient-to-br from-purple-400/20 to-violet-600/20">
<Trophy size={15} className="text-purple-400" />
</div>
<div>
<h2 className="text-sm font-black text-white">Leaderboard</h2>
<p className="text-[10px] text-zinc-600 font-semibold">Rank #{leaderboardRank}</p>
</div>
</div>
<span className="text-[9px] font-black border border-purple-500/15 bg-purple-500/5 text-purple-400 rounded-full px-2.5 py-1 uppercase tracking-[0.2em]">● Live</span>
</div>
<div className="space-y-2">
{visibleEntries.map((entry, index) => (
<motion.div
key={entry.id}
initial={{ opacity: 0, x: 10 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: 0.3 + index * 0.07 }}
className={cn(
'flex items-center gap-3 rounded-xl border px-3 py-2.5',
entry.isCurrentUser
? 'border-purple-400/25 bg-gradient-to-r from-purple-500/10 to-violet-500/5'
: 'border-white/[0.04] bg-white/[0.02]',
)}
>
<div
className={cn(
'flex h-7 w-7 shrink-0 items-center justify-center rounded-full text-[11px] font-black',
entry.rank === 1
? 'bg-gradient-to-br from-yellow-400/30 to-amber-500/20 text-yellow-300'
: entry.rank === 2
? 'bg-gradient-to-br from-slate-300/20 to-zinc-400/10 text-slate-300'
: entry.rank === 3
? 'bg-gradient-to-br from-orange-400/20 to-amber-600/10 text-orange-300'
: 'bg-white/[0.05] text-zinc-500',
)}
style={{ fontFamily: 'JetBrains Mono, monospace' }}
>
#{entry.rank}
</div>
<div className="flex-1 min-w-0">
<p className={cn('text-xs font-bold truncate', entry.isCurrentUser ? 'text-white' : 'text-zinc-300')}>{entry.displayName}</p>
<p className="text-[10px] text-zinc-600">{entry.solvedCount} solved · {entry.codingStreak ?? entry.currentStreak}d streak</p>
</div>
<div className="text-[11px] font-black text-zinc-400 bg-white/[0.04] rounded-lg px-2 py-1" style={{ fontFamily: 'JetBrains Mono, monospace' }}>{entry.score} pt</div>
</motion.div>
))}
</div>
{leaderboardGap > 0 && (
<p className="mt-3 text-[10px] text-zinc-600 text-center font-semibold">{leaderboardGap} more points to climb up</p>
)}
<motion.button
type="button"
onClick={onViewFullLeaderboard}
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.97 }}
className="mt-4 w-full rounded-xl border border-purple-500/20 bg-purple-500/5 py-2.5 text-xs font-black text-purple-400 transition-all hover:bg-purple-500/10 flex items-center justify-center gap-2 uppercase tracking-wider"
>
<Trophy size={13} />
View Full Leaderboard
</motion.button>
</motion.div>
</div>
</div>
</div>
);
}
type QuestionTableProps = {
questions: CodingQuestion[];
solvedSet: Set<string>;
onOpenQuestion: (question: CodingQuestion) => void;
startIndex?: number;
currentPage?: number;
onPageChange?: (page: number) => void;
itemsPerPage?: number;
emptyMessage?: string;
};
function QuestionTable({
questions,
solvedSet,
onOpenQuestion,
startIndex = 0,
currentPage = 1,
onPageChange,
itemsPerPage = 30,
emptyMessage = 'No DSA questions match this view.',
}: QuestionTableProps) {
const { tier } = useSubscription();
const totalPages = Math.ceil(questions.length / itemsPerPage);
const paginatedQuestions = onPageChange
? questions.slice((currentPage - 1) * itemsPerPage, currentPage * itemsPerPage)
: questions;
const pageStartIndex = onPageChange ? (currentPage - 1) * itemsPerPage : startIndex;
return (
<div className="space-y-3">
<div className="overflow-hidden rounded-2xl border border-zinc-800/60 bg-[#0a0a0a] shadow-2xl">
<div className="overflow-hidden">
<table className="w-full table-fixed text-left text-[13px]">
<thead className="border-b border-zinc-800 bg-[#111] text-[10px] uppercase tracking-[0.2em] text-zinc-600 font-black">
<tr>
<th className="w-10 px-3 py-3 text-center sm:w-12">#</th>
<th className="w-12 px-2 py-3 text-center">Status</th>
<th className="px-3 py-3">Title</th>
<th className="w-24 px-3 py-3 sm:w-28">Difficulty</th>
<th className="w-32 px-2 py-3 text-center sm:w-40">Companies</th>
<th className="hidden w-24 px-3 py-3 text-right 2xl:table-cell">Reward</th>
</tr>
</thead>
<tbody>
{paginatedQuestions.length === 0 && (
<tr>
<td colSpan={6} className="px-4 py-10 text-center text-sm font-semibold text-zinc-500">
{emptyMessage}
</td>
</tr>
)}
{paginatedQuestions.map((question, index) => {
const isSolved = solvedSet.has(question.id);
const reward = getQuestionReward(question.difficulty);
const displayIndex = pageStartIndex + index + 1;
const isLocked = tier === 'free' && displayIndex > 200;
return (
<motion.tr
key={question.id}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: Math.min(index * 0.015, 0.5) }}
className={cn(
'group h-12 border-b border-zinc-800/30 transition-all duration-200 relative',
isLocked ? 'cursor-not-allowed opacity-40 grayscale' : 'cursor-pointer hover:bg-cyan-500/[0.03] hover:border-l-2 hover:border-l-cyan-500/50',
index % 2 === 0 ? 'bg-[#0a0a0a]' : 'bg-[#0d0d0d]',
)}
onClick={() => {
if (!isLocked) onOpenQuestion(question);
}}
>
<td className="px-3 py-2 text-center">
<span className="text-xs text-zinc-600 font-bold" style={{ fontFamily: 'JetBrains Mono, monospace' }}>{displayIndex}</span>
</td>
<td className="px-2 py-2 text-center">
<div className={cn('mx-auto flex h-4 w-4 items-center justify-center rounded-full', isSolved ? 'bg-emerald-500/15 text-emerald-400' : 'text-zinc-700')}>
{isSolved ? <CheckCircle2 size={14} /> : <Circle size={14} />}
</div>
</td>
<td className="min-w-0 px-3 py-2 font-bold text-zinc-300 transition-colors group-hover:text-cyan-400">
<span className="block max-w-full truncate">{question.title}</span>
</td>
<td className="px-3 py-2">
<span className={cn(
'text-[10px] font-black uppercase tracking-wider px-2 py-0.5 rounded-md',
question.difficulty === 'Easy' ? 'text-teal-400 bg-teal-500/10' : question.difficulty === 'Medium' ? 'text-orange-400 bg-orange-500/10' : 'text-rose-400 bg-rose-500/10',
)}>{question.difficulty}</span>
</td>
<td className="px-2 py-2 relative">
<CompanyLogos companies={question.companies ?? []} />
{isLocked && <div className="absolute inset-0 flex items-center justify-center backdrop-blur-[1px]"><Lock size={12} className="text-zinc-500" /></div>}
</td>
<td className="hidden px-3 py-2 text-right 2xl:table-cell">
<div className="inline-flex items-center gap-1 rounded-md border border-amber-500/15 bg-amber-500/5 px-2 py-0.5 text-amber-400">
<Coins size={11} />
<span className="text-[10px] font-black" style={{ fontFamily: 'JetBrains Mono, monospace' }}>{reward}</span>
</div>
</td>
</motion.tr>
);
})}
</tbody>
</table>
</div>
</div>
{/* Pagination */}
{onPageChange && totalPages > 1 && (
<div className="flex items-center justify-between px-2">
<p className="text-[10px] font-black text-zinc-600 uppercase tracking-wider">
Showing {pageStartIndex + 1}-{Math.min(pageStartIndex + itemsPerPage, questions.length)} of {questions.length}
</p>
<div className="flex items-center gap-1">
<button
onClick={() => onPageChange(Math.max(1, currentPage - 1))}
disabled={currentPage === 1}
className="flex h-8 w-8 items-center justify-center rounded-lg border border-zinc-800/60 bg-[#111] text-zinc-500 transition-all hover:text-white hover:border-cyan-500/30 disabled:opacity-30 disabled:cursor-not-allowed"
>
<ChevronLeft size={14} />
</button>
{Array.from({ length: Math.min(5, totalPages) }, (_, i) => {
let page: number;
if (totalPages <= 5) { page = i + 1; }
else if (currentPage <= 3) { page = i + 1; }
else if (currentPage >= totalPages - 2) { page = totalPages - 4 + i; }
else { page = currentPage - 2 + i; }
return (
<button
key={page}
onClick={() => onPageChange(page)}
className={cn(
'flex h-8 w-8 items-center justify-center rounded-lg text-xs font-black transition-all',
currentPage === page
? 'bg-cyan-500/15 text-cyan-400 border border-cyan-500/30'
: 'border border-zinc-800/60 bg-[#111] text-zinc-500 hover:text-white hover:border-cyan-500/20',
)}
style={{ fontFamily: 'JetBrains Mono, monospace' }}
>
{page}
</button>
);
})}
<button
onClick={() => onPageChange(Math.min(totalPages, currentPage + 1))}
disabled={currentPage === totalPages}
className="flex h-8 w-8 items-center justify-center rounded-lg border border-zinc-800/60 bg-[#111] text-zinc-500 transition-all hover:text-white hover:border-cyan-500/30 disabled:opacity-30 disabled:cursor-not-allowed"
>
<ChevronRight size={14} />
</button>
</div>
</div>
)}
</div>
);
}
function CompanyLogos({ companies }: { companies: string[] }) {
const { tier } = useSubscription();
const uniqueCompanies = Array.from(new Set(companies.map((company) => company.trim()).filter(Boolean)));
const visibleCompanies = uniqueCompanies.slice(0, 3);
const extraCompanyCount = Math.max(uniqueCompanies.length - visibleCompanies.length, 0);
if (uniqueCompanies.length === 0) {
return (
<div className="flex h-7 items-center justify-center overflow-hidden">
<span
title="No company listed"
className="inline-flex h-7 w-7 items-center justify-center text-[9px] font-black text-zinc-700"
>
NA
</span>
</div>
);
}
return (
<div className={`flex h-7 max-w-full items-center justify-center gap-1.5 overflow-hidden relative ${tier === 'free' ? 'blur-sm opacity-50' : ''}`}>
{visibleCompanies.map((company, index) => (
<CompanyLogo key={company} company={company} index={index} />
))}
{extraCompanyCount > 0 && (
<span
title={uniqueCompanies.slice(visibleCompanies.length).join(', ')}
className="inline-flex h-7 min-w-7 shrink-0 items-center justify-center rounded-md border border-zinc-800 bg-zinc-900/80 px-1.5 text-[9px] font-black text-zinc-500"
style={{ fontFamily: 'JetBrains Mono, monospace' }}
>
+{extraCompanyCount}
</span>
)}
</div>
);
}
function CompanyLogo({ company, index }: { company: string; index: number }) {
const logoUrls = useMemo(() => getCompanyLogoUrls(company), [company]);
const [logoIndex, setLogoIndex] = useState(0);
const initials = getCompanyInitials(company);
const logoUrl = logoUrls[logoIndex];
return (
<span
title={company}
aria-label={company}
className="company-logo-mark inline-flex h-7 w-7 shrink-0 items-center justify-center text-[10px] font-black text-cyan-300 transition-transform hover:scale-110"
style={{ animationDelay: `${index * 0.22}s` }}
>
{logoUrl ? (
<img
src={logoUrl}
alt={`${company} logo`}
loading="lazy"
onError={() => setLogoIndex((current) => current + 1)}
className="company-logo-icon h-full w-full object-contain"
/>
) : (
<span>{initials}</span>
)}
</span>
);
}
function getCompanyLogoUrls(company: string) {
const normalizedCompany = normalizeCompanyName(company);
const overrides = COMPANY_LOGO_OVERRIDES[normalizedCompany] ?? [];
const domain = COMPANY_DOMAINS[normalizedCompany] ?? `${normalizedCompany}.com`;
const simpleIconSlug = COMPANY_ICON_SLUGS[normalizedCompany] ?? normalizedCompany;
return [
...overrides,
`https://cdn.simpleicons.org/${simpleIconSlug}`,
`https://logo.clearbit.com/${domain}`,
`https://www.google.com/s2/favicons?sz=64&domain=${domain}`,
];
}
function normalizeCompanyName(company: string) {
return company.toLowerCase().replace(/[^a-z0-9]/g, '');
}
function getCompanyInitials(company: string) {
const words = company.match(/[a-z0-9]+/gi) ?? [];
const initials = words.slice(0, 2).map((word) => word[0]?.toUpperCase()).join('');
return initials || '?';
}
function matchesBlind56Question(question: CodingQuestion, title: string) {
const questionTitle = question.title.toLowerCase();
const blind56Title = title.toLowerCase();
return questionTitle.includes(blind56Title) || blind56Title.includes(questionTitle);
}