Skip to content

Commit c7b65ad

Browse files
committed
update ui
1 parent 6a1718a commit c7b65ad

16 files changed

Lines changed: 1230 additions & 332 deletions

bun.lockb

1.89 KB
Binary file not shown.

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
"lint": "next lint"
1111
},
1212
"dependencies": {
13+
"@monaco-editor/react": "^4.7.0",
14+
"classnames": "^2.5.1",
1315
"lucide-react": "^0.487.0",
1416
"next": "15.2.4",
1517
"react": "^19.0.0",

src/components/CodeEditor.tsx

Lines changed: 44 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -1,74 +1,53 @@
1-
import React, { useState } from 'react';
2-
import { Play, Copy, CheckCircle2 } from 'lucide-react';
1+
'use client'
2+
3+
import React, { useRef, useEffect } from 'react'
4+
import { useTheme } from '@/lib/hooks/useTheme'
5+
import { Editor} from '@monaco-editor/react'
6+
import * as monaco from 'monaco-editor'
37

48
interface CodeEditorProps {
5-
code: string;
6-
onChange: (code: string) => void;
7-
onRun: () => void;
8-
isRunning: boolean;
9+
code: string
10+
onChange: (value: string) => void
11+
disabled?: boolean
912
}
1013

11-
/**
12-
* Component for editing code with basic editor features
13-
*/
14-
const CodeEditor: React.FC<CodeEditorProps> = ({
15-
code,
16-
onChange,
17-
onRun,
18-
isRunning,
19-
}) => {
20-
const [copySuccess, setCopySuccess] = useState(false);
14+
const CodeEditor: React.FC<CodeEditorProps> = ({ code, onChange, disabled }) => {
15+
const editorRef = useRef<monaco.editor.IStandaloneCodeEditor | null>(null)
16+
const { isDarkTheme } = useTheme()
17+
18+
const handleEditorDidMount = (editor: monaco.editor.IStandaloneCodeEditor) => {
19+
editorRef.current = editor
20+
}
21+
22+
2123

22-
// Copy code to clipboard
23-
const copyToClipboard = () => {
24-
navigator.clipboard.writeText(code);
25-
setCopySuccess(true);
26-
setTimeout(() => setCopySuccess(false), 2000);
27-
};
24+
useEffect(() => {
25+
if (editorRef.current) {
26+
editorRef.current.updateOptions({ readOnly: disabled })
27+
}
28+
}, [disabled])
2829

2930
return (
30-
<div className="flex flex-col h-full">
31-
<div className="flex items-center justify-between p-2 border-b border-gray-200 bg-gray-50">
32-
<div className="text-sm font-medium">TypeScript Editor</div>
33-
<div className="flex items-center space-x-2">
34-
<button
35-
className="p-1.5 text-gray-500 hover:text-gray-700 transition-colors"
36-
onClick={copyToClipboard}
37-
title="Copy code"
38-
>
39-
{copySuccess ? (
40-
<CheckCircle2 size={16} className="text-green-500" />
41-
) : (
42-
<Copy size={16} />
43-
)}
44-
</button>
45-
<button
46-
className={`flex items-center space-x-1 px-3 py-1.5 rounded bg-blue-600 text-white hover:bg-blue-700 transition-colors ${isRunning ? 'opacity-75 cursor-not-allowed' : ''
47-
}`}
48-
onClick={onRun}
49-
disabled={isRunning}
50-
>
51-
<Play size={14} />
52-
<span className="text-sm">Run</span>
53-
</button>
54-
</div>
55-
</div>
56-
<div className="flex-1 overflow-auto p-0 font-mono text-sm bg-white">
57-
<textarea
58-
className="w-full h-full resize-none p-4 outline-none"
59-
value={code}
60-
onChange={(e) => onChange(e.target.value)}
61-
spellCheck={false}
62-
style={{
63-
// Basic styling for readability
64-
fontFamily: 'var(--font-geist-mono), monospace',
65-
lineHeight: '1.5',
66-
tabSize: 2
67-
}}
68-
/>
69-
</div>
31+
<div className="flex-1 overflow-hidden rounded-md border border-gray-700 min-h-[300px]">
32+
<Editor
33+
defaultLanguage="typescript"
34+
value={code}
35+
theme={isDarkTheme ? 'vs-dark' : 'light'}
36+
onChange={(value) => onChange(value || '')}
37+
onMount={handleEditorDidMount}
38+
options={{
39+
minimap: { enabled: false },
40+
scrollBeyondLastLine: false,
41+
fontSize: 14,
42+
wordWrap: 'on',
43+
readOnly: disabled,
44+
tabSize: 2,
45+
smoothScrolling: true,
46+
cursorBlinking: 'smooth',
47+
automaticLayout: true
48+
}}
49+
/>
7050
</div>
71-
);
72-
};
73-
51+
)
52+
}
7453
export default CodeEditor;

src/components/Console.tsx

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
'use client'
2+
3+
import React, { useRef, useEffect } from 'react'
4+
import { ConsoleOutput } from '@/lib/types/example'
5+
import classNames from 'classnames'
6+
7+
interface ConsoleProps {
8+
outputs: ConsoleOutput[]
9+
}
10+
11+
export default function Console({ outputs }: ConsoleProps) {
12+
const consoleRef = useRef<HTMLDivElement>(null)
13+
14+
// Auto-scroll to bottom when new outputs are added
15+
useEffect(() => {
16+
if (consoleRef.current) {
17+
consoleRef.current.scrollTop = consoleRef.current.scrollHeight
18+
}
19+
}, [outputs])
20+
21+
// Format timestamp to readable time
22+
const formatTimestamp = (timestamp: number): string => {
23+
const date = new Date(timestamp)
24+
return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' })
25+
}
26+
27+
return (
28+
<div className="flex flex-col h-1/2 min-h-[200px]">
29+
<div className="flex justify-between items-center bg-gray-800 p-2 rounded-t-md">
30+
<h3 className="text-sm font-medium">Console Output</h3>
31+
<span className="text-xs opacity-70">{outputs.length} messages</span>
32+
</div>
33+
34+
<div
35+
ref={consoleRef}
36+
className="flex-1 bg-gray-900 text-gray-200 font-mono text-sm p-2 overflow-y-auto rounded-b-md"
37+
>
38+
{outputs.length === 0 ? (
39+
<div className="h-full flex items-center justify-center text-gray-500">
40+
Run code to see output here
41+
</div>
42+
) : (
43+
<div className="space-y-1">
44+
{outputs.map((output, index) => (
45+
<div
46+
key={index}
47+
className={classNames(
48+
'py-1 border-b border-gray-800 last:border-b-0',
49+
{
50+
'text-red-400': output.type === 'error',
51+
'text-yellow-400': output.type === 'warning',
52+
'text-green-400': output.type === 'log' && output.content.includes('Success'),
53+
}
54+
)}
55+
>
56+
<span className="text-xs text-gray-500 mr-2">[{formatTimestamp(output.timestamp)}]</span>
57+
<span className="whitespace-pre-wrap">{output.content}</span>
58+
</div>
59+
))}
60+
</div>
61+
)}
62+
</div>
63+
</div>
64+
)
65+
}

src/components/ExampleSelector.tsx

Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
'use client'
2+
3+
import React, { useState, useEffect } from 'react'
4+
import { Example } from '@/lib/types/example'
5+
6+
interface ExampleSelectorProps {
7+
examples: Example[]
8+
selectedExampleId: string
9+
onExampleChange: (exampleId: string) => void
10+
}
11+
12+
export default function ExampleSelector({
13+
examples,
14+
selectedExampleId,
15+
onExampleChange
16+
}: ExampleSelectorProps) {
17+
const [filter, setFilter] = useState<string>('')
18+
const [filteredExamples, setFilteredExamples] = useState<Example[]>(examples)
19+
20+
{/*
21+
const exampleCategories = Array.from(
22+
new Set(examples.flatMap(example => example.categories))
23+
).sort() */}
24+
25+
26+
useEffect(() => {
27+
if (!filter) {
28+
setFilteredExamples(examples)
29+
return
30+
}
31+
32+
const lowercaseFilter = filter.toLowerCase()
33+
setFilteredExamples(
34+
examples.filter(example =>
35+
example.name.toLowerCase().includes(lowercaseFilter) ||
36+
example.description.toLowerCase().includes(lowercaseFilter) ||
37+
example.categories.some(category =>
38+
category.toLowerCase().includes(lowercaseFilter)
39+
)
40+
)
41+
)
42+
}, [filter, examples])
43+
44+
// Group examples by level
45+
const beginnerExamples = filteredExamples.filter(ex => ex.level === 'beginner')
46+
const intermediateExamples = filteredExamples.filter(ex => ex.level === 'intermediate')
47+
const advancedExamples = filteredExamples.filter(ex => ex.level === 'advanced')
48+
49+
return (
50+
<div className="space-y-2">
51+
<label className="block text-sm font-medium">
52+
Select Example
53+
</label>
54+
55+
{/* Search input */}
56+
<div className="relative">
57+
<input
58+
type="text"
59+
value={filter}
60+
onChange={(e) => setFilter(e.target.value)}
61+
placeholder="Search examples..."
62+
className="w-full py-2 px-3 bg-transparent border border-gray-700 rounded-md text-sm focus:border-primary-500 focus:outline-none focus:ring-1 focus:ring-primary-500"
63+
/>
64+
{filter && (
65+
<button
66+
onClick={() => setFilter('')}
67+
className="absolute right-2 top-1/2 transform -translate-y-1/2 text-gray-400 hover:text-gray-200"
68+
>
69+
×
70+
</button>
71+
)}
72+
</div>
73+
74+
{/* Examples list */}
75+
<div className="border border-gray-700 rounded-md max-h-[300px] overflow-y-auto">
76+
{filteredExamples.length === 0 ? (
77+
<div className="p-4 text-center text-sm text-gray-400">
78+
No examples match your search
79+
</div>
80+
) : (
81+
<div className="divide-y divide-gray-700">
82+
{/* Beginner examples */}
83+
{beginnerExamples.length > 0 && (
84+
<ExampleGroup
85+
title="Beginner"
86+
examples={beginnerExamples}
87+
selectedExampleId={selectedExampleId}
88+
onExampleChange={onExampleChange}
89+
/>
90+
)}
91+
92+
{/* Intermediate examples */}
93+
{intermediateExamples.length > 0 && (
94+
<ExampleGroup
95+
title="Intermediate"
96+
examples={intermediateExamples}
97+
selectedExampleId={selectedExampleId}
98+
onExampleChange={onExampleChange}
99+
/>
100+
)}
101+
102+
{/* Advanced examples */}
103+
{advancedExamples.length > 0 && (
104+
<ExampleGroup
105+
title="Advanced"
106+
examples={advancedExamples}
107+
selectedExampleId={selectedExampleId}
108+
onExampleChange={onExampleChange}
109+
/>
110+
)}
111+
</div>
112+
)}
113+
</div>
114+
</div>
115+
)
116+
}
117+
118+
interface ExampleGroupProps {
119+
title: string
120+
examples: Example[]
121+
selectedExampleId: string
122+
onExampleChange: (exampleId: string) => void
123+
}
124+
125+
function ExampleGroup({
126+
title,
127+
examples,
128+
selectedExampleId,
129+
onExampleChange
130+
}: ExampleGroupProps) {
131+
return (
132+
<div>
133+
<div className="px-3 py-2 bg-gray-800 text-xs font-medium">
134+
{title}
135+
</div>
136+
<div className="divide-y divide-gray-800">
137+
{examples.map(example => (
138+
<button
139+
key={example.id}
140+
onClick={() => onExampleChange(example.id)}
141+
className={`
142+
w-full px-3 py-2 text-left hover:bg-gray-800 transition-colors
143+
${selectedExampleId === example.id ? 'bg-gray-800' : ''}
144+
`}
145+
>
146+
<div className="flex justify-between items-center">
147+
<span className="font-medium text-sm">{example.name}</span>
148+
<div className="flex gap-1">
149+
{example.categories.map(category => (
150+
<span
151+
key={category}
152+
className="bg-gray-700 text-xs px-1.5 py-0.5 rounded"
153+
>
154+
{category}
155+
</span>
156+
))}
157+
</div>
158+
</div>
159+
<p className="text-xs text-gray-400 mt-1">
160+
{example.description}
161+
</p>
162+
</button>
163+
))}
164+
</div>
165+
</div>
166+
)
167+
}

0 commit comments

Comments
 (0)