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