|
1 | 1 | 'use client'; |
2 | 2 |
|
| 3 | +import { useState } from 'react'; |
3 | 4 | import parse from 'html-react-parser'; |
4 | 5 | import type { Problem } from '@/lib/problems'; |
5 | 6 |
|
6 | 7 | interface ProblemDescriptionProps { |
7 | 8 | problem: Problem; |
8 | 9 | } |
9 | 10 |
|
| 11 | +type Tab = 'description' | 'examples' | 'hints'; |
| 12 | + |
10 | 13 | export default function ProblemDescription({ problem }: ProblemDescriptionProps) { |
| 14 | + const [activeTab, setActiveTab] = useState<Tab>('description'); |
| 15 | + |
11 | 16 | const difficultyColors = { |
12 | 17 | easy: 'bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-300', |
13 | 18 | medium: 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900/30 dark:text-yellow-300', |
14 | 19 | hard: 'bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-300', |
15 | 20 | }; |
16 | 21 |
|
| 22 | + const tabs: { id: Tab; label: string; count?: number }[] = [ |
| 23 | + { id: 'description', label: 'Description' }, |
| 24 | + { id: 'examples', label: 'Examples', count: problem.examples.length }, |
| 25 | + { id: 'hints', label: 'Hints', count: problem.hints.length }, |
| 26 | + ]; |
| 27 | + |
17 | 28 | return ( |
18 | | - <div className="space-y-8"> |
19 | | - <div> |
20 | | - <div className="flex items-center gap-3 mb-4 flex-wrap"> |
21 | | - <h1 className="text-3xl font-bold text-gray-900 dark:text-gray-100 tracking-tight leading-tight"> |
| 29 | + <div className="flex flex-col h-full"> |
| 30 | + {/* Header - Always visible */} |
| 31 | + <div className="flex-shrink-0 pb-4 border-b border-gray-200 dark:border-gray-700"> |
| 32 | + <div className="flex items-center gap-3 mb-2 flex-wrap"> |
| 33 | + <h1 className="text-2xl font-bold text-gray-900 dark:text-gray-100 tracking-tight leading-tight"> |
22 | 34 | {problem.title} |
23 | 35 | </h1> |
24 | 36 | <span |
25 | | - className={`px-3 py-1.5 rounded-md text-xs font-semibold uppercase tracking-wide ${difficultyColors[problem.difficulty]}`} |
| 37 | + className={`px-2.5 py-1 rounded-md text-xs font-semibold uppercase tracking-wide ${difficultyColors[problem.difficulty]}`} |
26 | 38 | > |
27 | 39 | {problem.difficulty} |
28 | 40 | </span> |
29 | 41 | </div> |
30 | | - <p className="text-base text-gray-700 dark:text-gray-300 font-semibold">{problem.category}</p> |
| 42 | + <p className="text-sm text-gray-600 dark:text-gray-400 font-medium">{problem.category}</p> |
31 | 43 | </div> |
32 | 44 |
|
33 | | - <div className="prose prose-lg dark:prose-invert max-w-none prose-headings:font-bold prose-headings:text-gray-900 dark:prose-headings:text-gray-100 prose-p:text-gray-900 dark:prose-p:text-gray-100 prose-p:leading-relaxed prose-p:mb-4 prose-li:text-gray-900 dark:prose-li:text-gray-100"> |
34 | | - <div className="text-lg leading-relaxed"> |
35 | | - {parse(problem.description)} |
36 | | - </div> |
| 45 | + {/* Tabs */} |
| 46 | + <div className="flex-shrink-0 flex gap-1 pt-4 pb-2 border-b border-gray-200 dark:border-gray-700"> |
| 47 | + {tabs.map((tab) => ( |
| 48 | + <button |
| 49 | + key={tab.id} |
| 50 | + onClick={() => setActiveTab(tab.id)} |
| 51 | + className={`px-4 py-2 text-sm font-medium rounded-t-lg transition-colors relative ${ |
| 52 | + activeTab === tab.id |
| 53 | + ? 'bg-gray-100 dark:bg-gray-800 text-gray-900 dark:text-gray-100' |
| 54 | + : 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-800/50' |
| 55 | + }`} |
| 56 | + > |
| 57 | + {tab.label} |
| 58 | + {tab.count !== undefined && tab.count > 0 && ( |
| 59 | + <span className={`ml-1.5 px-1.5 py-0.5 text-xs rounded-full ${ |
| 60 | + activeTab === tab.id |
| 61 | + ? 'bg-gray-300 dark:bg-gray-600 text-gray-700 dark:text-gray-300' |
| 62 | + : 'bg-gray-200 dark:bg-gray-700 text-gray-600 dark:text-gray-400' |
| 63 | + }`}> |
| 64 | + {tab.count} |
| 65 | + </span> |
| 66 | + )} |
| 67 | + </button> |
| 68 | + ))} |
37 | 69 | </div> |
38 | 70 |
|
39 | | - {problem.examples.length > 0 && ( |
40 | | - <div className="mt-8"> |
41 | | - <h2 className="text-xl font-semibold mb-5 pb-3 border-b-2 border-gray-300 dark:border-gray-600 text-gray-900 dark:text-gray-100"> |
42 | | - Examples |
43 | | - </h2> |
44 | | - <div className="space-y-6"> |
45 | | - {problem.examples.map((example, index) => ( |
46 | | - <div |
47 | | - key={index} |
48 | | - className="bg-gray-50 dark:bg-gray-900/50 rounded-lg p-6 border-2 border-gray-200 dark:border-gray-700 shadow-sm" |
49 | | - > |
50 | | - <div className="mb-4"> |
51 | | - <span className="text-base font-bold text-gray-800 dark:text-gray-200"> |
52 | | - Example {index + 1}: |
53 | | - </span> |
54 | | - </div> |
55 | | - <div className="space-y-4"> |
56 | | - <div> |
57 | | - <span className="text-sm font-semibold text-gray-800 dark:text-gray-200 block mb-2 uppercase tracking-wide"> |
58 | | - Input: |
59 | | - </span> |
60 | | - <pre className="mt-1 p-4 bg-white dark:bg-gray-800 rounded-lg font-mono text-sm overflow-x-auto leading-relaxed border border-gray-200 dark:border-gray-700 text-gray-900 dark:text-gray-100"> |
61 | | - {example.input} |
62 | | - </pre> |
63 | | - </div> |
64 | | - <div> |
65 | | - <span className="text-sm font-semibold text-gray-800 dark:text-gray-200 block mb-2 uppercase tracking-wide"> |
66 | | - Output: |
| 71 | + {/* Tab Content */} |
| 72 | + <div className="flex-1 overflow-y-auto pt-4"> |
| 73 | + {/* Description Tab */} |
| 74 | + {activeTab === 'description' && ( |
| 75 | + <div className="prose prose-sm dark:prose-invert max-w-none prose-headings:font-bold prose-headings:text-gray-900 dark:prose-headings:text-gray-100 prose-p:text-gray-800 dark:prose-p:text-gray-200 prose-p:leading-relaxed prose-li:text-gray-800 dark:prose-li:text-gray-200 prose-code:text-sm prose-code:bg-gray-100 dark:prose-code:bg-gray-800 prose-code:px-1.5 prose-code:py-0.5 prose-code:rounded"> |
| 76 | + {parse(problem.description)} |
| 77 | + </div> |
| 78 | + )} |
| 79 | + |
| 80 | + {/* Examples Tab */} |
| 81 | + {activeTab === 'examples' && ( |
| 82 | + <div className="space-y-4"> |
| 83 | + {problem.examples.length > 0 ? ( |
| 84 | + problem.examples.map((example, index) => ( |
| 85 | + <div |
| 86 | + key={index} |
| 87 | + className="bg-gray-50 dark:bg-gray-900/50 rounded-lg p-4 border border-gray-200 dark:border-gray-700" |
| 88 | + > |
| 89 | + <div className="mb-3"> |
| 90 | + <span className="text-sm font-bold text-gray-800 dark:text-gray-200"> |
| 91 | + Example {index + 1}: |
67 | 92 | </span> |
68 | | - <pre className="mt-1 p-4 bg-white dark:bg-gray-800 rounded-lg font-mono text-sm overflow-x-auto leading-relaxed border border-gray-200 dark:border-gray-700 text-gray-900 dark:text-gray-100"> |
69 | | - {example.output} |
70 | | - </pre> |
71 | 93 | </div> |
72 | | - {example.explanation && ( |
73 | | - <div className="text-sm text-gray-700 dark:text-gray-300 italic pt-2 leading-relaxed border-t border-gray-200 dark:border-gray-700 mt-4"> |
74 | | - {example.explanation} |
| 94 | + <div className="space-y-3"> |
| 95 | + <div> |
| 96 | + <span className="text-xs font-semibold text-gray-600 dark:text-gray-400 block mb-1 uppercase tracking-wide"> |
| 97 | + Input: |
| 98 | + </span> |
| 99 | + <pre className="p-3 bg-white dark:bg-gray-800 rounded-md font-mono text-sm overflow-x-auto border border-gray-200 dark:border-gray-700 text-gray-900 dark:text-gray-100"> |
| 100 | + {example.input} |
| 101 | + </pre> |
75 | 102 | </div> |
76 | | - )} |
| 103 | + <div> |
| 104 | + <span className="text-xs font-semibold text-gray-600 dark:text-gray-400 block mb-1 uppercase tracking-wide"> |
| 105 | + Output: |
| 106 | + </span> |
| 107 | + <pre className="p-3 bg-white dark:bg-gray-800 rounded-md font-mono text-sm overflow-x-auto border border-gray-200 dark:border-gray-700 text-gray-900 dark:text-gray-100"> |
| 108 | + {example.output} |
| 109 | + </pre> |
| 110 | + </div> |
| 111 | + {example.explanation && ( |
| 112 | + <div className="text-sm text-gray-600 dark:text-gray-400 italic pt-2 border-t border-gray-200 dark:border-gray-700"> |
| 113 | + {example.explanation} |
| 114 | + </div> |
| 115 | + )} |
| 116 | + </div> |
77 | 117 | </div> |
78 | | - </div> |
79 | | - ))} |
| 118 | + )) |
| 119 | + ) : ( |
| 120 | + <p className="text-gray-500 dark:text-gray-400 text-sm">No examples available.</p> |
| 121 | + )} |
80 | 122 | </div> |
81 | | - </div> |
82 | | - )} |
| 123 | + )} |
83 | 124 |
|
84 | | - {problem.hints.length > 0 && ( |
85 | | - <details className="bg-blue-50 dark:bg-blue-900/20 border-2 border-blue-200 dark:border-blue-800 rounded-lg p-6 transition-all duration-200 hover:bg-blue-100 dark:hover:bg-blue-900/30 mt-6 shadow-sm"> |
86 | | - <summary className="cursor-pointer font-bold text-blue-900 dark:text-blue-200 mb-4 hover:text-blue-700 dark:hover:text-blue-100 transition-colors duration-200 focus:outline-none focus:ring-2 focus:ring-blue-400 focus:ring-offset-2 dark:focus:ring-offset-gray-800 rounded px-2 py-1 text-lg"> |
87 | | - 💡 Hints |
88 | | - </summary> |
89 | | - <ul className="list-disc list-inside space-y-3 mt-4 text-base text-blue-900 dark:text-blue-200 leading-relaxed pl-2"> |
90 | | - {problem.hints.map((hint, index) => ( |
91 | | - <li key={index} className="text-gray-800 dark:text-gray-200">{hint}</li> |
92 | | - ))} |
93 | | - </ul> |
94 | | - </details> |
95 | | - )} |
| 125 | + {/* Hints Tab */} |
| 126 | + {activeTab === 'hints' && ( |
| 127 | + <div> |
| 128 | + {problem.hints.length > 0 ? ( |
| 129 | + <ul className="space-y-3"> |
| 130 | + {problem.hints.map((hint, index) => ( |
| 131 | + <li |
| 132 | + key={index} |
| 133 | + className="flex gap-3 p-3 bg-blue-50 dark:bg-blue-900/20 rounded-lg border border-blue-200 dark:border-blue-800" |
| 134 | + > |
| 135 | + <span className="text-blue-500 dark:text-blue-400 flex-shrink-0">💡</span> |
| 136 | + <span className="text-sm text-gray-800 dark:text-gray-200">{hint}</span> |
| 137 | + </li> |
| 138 | + ))} |
| 139 | + </ul> |
| 140 | + ) : ( |
| 141 | + <p className="text-gray-500 dark:text-gray-400 text-sm">No hints available.</p> |
| 142 | + )} |
| 143 | + </div> |
| 144 | + )} |
| 145 | + </div> |
96 | 146 | </div> |
97 | 147 | ); |
98 | 148 | } |
0 commit comments