Reuben_OS / app /components /TextEditor.tsx
Reubencf's picture
Fix quiz system: separate answers, backward compat, and bug fixes
7f6d612
'use client'
import React, { useState, useEffect } from 'react'
import Editor from '@monaco-editor/react'
import {
FloppyDisk,
X,
Minus,
Square,
Check,
FileText,
WarningCircle
} from '@phosphor-icons/react'
import Window from './Window'
interface TextEditorProps {
onClose: () => void
onMinimize?: () => void
onMaximize?: () => void
onFocus?: () => void
zIndex?: number
initialContent?: string
fileName?: string
filePath?: string
passkey?: string
}
export function TextEditor({
onClose,
onMinimize,
onMaximize,
onFocus,
zIndex,
initialContent = '',
fileName = 'untitled.txt',
filePath = '',
passkey = ''
}: TextEditorProps) {
const [code, setCode] = useState(initialContent)
const [isSaving, setIsSaving] = useState(false)
const [saveStatus, setSaveStatus] = useState<'idle' | 'saving' | 'saved' | 'error'>('idle')
const [lastSaved, setLastSaved] = useState<Date | null>(null)
const [hasChanges, setHasChanges] = useState(false)
// Note: PDF compilation is not available in the web environment
// Users should download .tex files and compile locally
// Detect file language from extension
const getLanguage = (filename: string) => {
const ext = filename.split('.').pop()?.toLowerCase()
switch (ext) {
case 'tex':
return 'latex'
case 'js':
case 'jsx':
return 'javascript'
case 'ts':
case 'tsx':
return 'typescript'
case 'py':
return 'python'
case 'java':
return 'java'
case 'cpp':
case 'cc':
case 'cxx':
return 'cpp'
case 'c':
case 'h':
return 'c'
case 'cs':
return 'csharp'
case 'json':
return 'json'
case 'xml':
return 'xml'
case 'html':
case 'htm':
return 'html'
case 'css':
return 'css'
case 'md':
return 'markdown'
case 'sh':
case 'bash':
return 'shell'
case 'dart':
return 'dart'
default:
return 'plaintext'
}
}
const language = getLanguage(fileName)
const isLatexFile = language === 'latex'
// Track changes
useEffect(() => {
if (code !== initialContent) {
setHasChanges(true)
setSaveStatus('idle')
}
}, [code, initialContent])
// Download file
const handleDownload = () => {
const ext = fileName.split('.').pop()?.toLowerCase() || 'txt'
let mimeType = 'text/plain'
const mimeTypes: Record<string, string> = {
'js': 'text/javascript',
'ts': 'text/typescript',
'tsx': 'text/typescript',
'jsx': 'text/javascript',
'json': 'application/json',
'html': 'text/html',
'css': 'text/css',
'md': 'text/markdown',
'py': 'text/x-python',
'java': 'text/x-java',
'c': 'text/x-c',
'cpp': 'text/x-c++',
'dart': 'application/vnd.dart',
'tex': 'application/x-tex',
'xml': 'text/xml',
'yaml': 'text/yaml',
'yml': 'text/yaml',
'sh': 'application/x-sh',
'sql': 'application/sql'
}
if (mimeTypes[ext]) {
mimeType = mimeTypes[ext]
}
const blob = new Blob([code], { type: mimeType })
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = fileName
a.click()
URL.revokeObjectURL(url)
}
const handleSave = async () => {
// Determine if this is a public file or secure file
// If passkey is empty, we assume it's a public file (unless explicitly told otherwise, but we don't have an isPublic prop)
// Ideally, we should have an isPublic prop, but for now, empty passkey implies public context in this app structure.
const isPublic = !passkey
if (!passkey && !isPublic) {
alert('Please enter your passkey first!')
return
}
setIsSaving(true)
setSaveStatus('saving')
try {
const endpoint = isPublic ? '/api/public' : '/api/data'
const response = await fetch(endpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
action: 'save_file',
passkey: passkey, // Optional for public
fileName: fileName,
content: code,
folder: filePath
})
})
const result = await response.json()
if (result.success) {
setSaveStatus('saved')
setLastSaved(new Date())
setHasChanges(false)
// Reset status after 3 seconds
setTimeout(() => {
setSaveStatus('idle')
}, 3000)
} else {
setSaveStatus('error')
alert(`Error saving file: ${result.error || 'Unknown error'}`)
}
} catch (error) {
console.error('Save error:', error)
setSaveStatus('error')
alert('Failed to save file. Please try again.')
} finally {
setIsSaving(false)
}
}
// Keyboard shortcut: Cmd/Ctrl + S to save
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if ((e.metaKey || e.ctrlKey) && e.key === 's') {
e.preventDefault()
handleSave()
}
}
window.addEventListener('keydown', handleKeyDown)
return () => window.removeEventListener('keydown', handleKeyDown)
}, [code, passkey, fileName, filePath])
return (
<Window
id="text-editor"
title={`Text Editor - ${fileName}${isLatexFile ? ' (LaTeX)' : ''}`}
isOpen={true}
onClose={onClose}
onMinimize={onMinimize}
onMaximize={onMaximize}
onFocus={onFocus}
zIndex={zIndex}
width={1000}
height={700}
x={100}
y={80}
className="text-editor-window"
>
<div className="flex flex-col h-full bg-[#1e1e1e]">
{/* Toolbar */}
<div className="h-10 sm:h-12 bg-[#2d2d2d] border-b border-[#1e1e1e] flex items-center justify-between px-2 sm:px-4 gap-2">
<div className="flex items-center gap-1.5 sm:gap-3 min-w-0 flex-1">
<div className="flex items-center gap-1.5 sm:gap-2 px-2 sm:px-3 py-1 sm:py-1.5 bg-[#1e1e1e] rounded text-[10px] sm:text-xs text-gray-400 border border-[#333] min-w-0">
<FileText size={12} className="text-blue-400 sm:w-3.5 sm:h-3.5 flex-shrink-0" />
<span className="truncate max-w-[60px] xs:max-w-[100px] sm:max-w-none">{fileName}</span>
</div>
{hasChanges && (
<span className="text-[10px] sm:text-xs text-yellow-500 flex-shrink-0"><span className="hidden xs:inline">Modified</span></span>
)}
{saveStatus === 'saved' && (
<div className="hidden xs:flex items-center gap-1 text-[10px] sm:text-xs text-green-500">
<Check size={12} className="sm:w-3.5 sm:h-3.5" />
<span className="hidden sm:inline">Saved {lastSaved?.toLocaleTimeString()}</span>
<span className="sm:hidden">Saved</span>
</div>
)}
{saveStatus === 'error' && (
<div className="flex items-center gap-1 text-[10px] sm:text-xs text-red-500">
<WarningCircle size={12} className="sm:w-3.5 sm:h-3.5" />
<span className="hidden xs:inline">Save failed</span>
</div>
)}
</div>
<div className="flex items-center gap-1 sm:gap-2 flex-shrink-0">
<button
onClick={handleDownload}
className="flex items-center gap-1 sm:gap-2 px-2 sm:px-3 py-1 sm:py-1.5 bg-gray-700 hover:bg-gray-600 text-white rounded text-[10px] sm:text-xs font-medium transition-colors"
title="Download file"
>
<svg width="12" height="12" viewBox="0 0 256 256" fill="currentColor" className="sm:w-3.5 sm:h-3.5">
<path d="M224,144v64a8,8,0,0,1-8,8H40a8,8,0,0,1-8-8V144a8,8,0,0,1,16,0v56H208V144a8,8,0,0,1,16,0Zm-101.66,5.66a8,8,0,0,0,11.32,0l40-40a8,8,0,0,0-11.32-11.32L136,124.69V40a8,8,0,0,0-16,0v84.69L93.66,98.34a8,8,0,0,0-11.32,11.32Z"></path>
</svg>
<span className="hidden xs:inline">Download</span>
</button>
<div className="hidden sm:block h-4 w-[1px] bg-gray-600 mx-1" />
<button
onClick={handleSave}
disabled={isSaving || !hasChanges}
className="flex items-center gap-1 sm:gap-2 px-2 sm:px-4 py-1 sm:py-1.5 bg-blue-600 hover:bg-blue-700 disabled:bg-gray-600 disabled:cursor-not-allowed text-white rounded text-[10px] sm:text-xs font-medium transition-colors"
>
{isSaving ? (
<>
<div className="w-2.5 h-2.5 sm:w-3 sm:h-3 border-2 border-white border-t-transparent rounded-full animate-spin" />
<span className="hidden xs:inline">Saving...</span>
</>
) : (
<>
<FloppyDisk size={12} weight="fill" className="sm:w-3.5 sm:h-3.5" />
<span className="hidden xs:inline">Save</span>
<span className="hidden sm:inline"> (⌘S)</span>
</>
)}
</button>
</div>
</div>
{/* Editor */}
<div className="flex-1">
<Editor
height="100%"
language={language}
theme="vs-dark"
value={code}
onChange={(value) => setCode(value || '')}
options={{
minimap: { enabled: typeof window !== 'undefined' && window.innerWidth >= 768 },
fontSize: typeof window !== 'undefined' && window.innerWidth < 640 ? 12 : 14,
fontFamily: "'JetBrains Mono', 'Fira Code', 'Cascadia Code', monospace",
lineNumbers: 'on',
scrollBeyondLastLine: false,
automaticLayout: true,
padding: { top: 12, bottom: 12 },
renderLineHighlight: 'all',
smoothScrolling: true,
cursorBlinking: 'smooth',
cursorSmoothCaretAnimation: 'on',
wordWrap: 'on',
wrappingIndent: 'indent',
tabSize: 2,
insertSpaces: true
}}
/>
</div>
{/* Status Bar */}
<div className="h-5 sm:h-6 bg-[#007acc] flex items-center justify-between px-2 sm:px-4 text-[9px] sm:text-xs text-white">
<div className="flex items-center gap-2 sm:gap-4">
<span>{language.toUpperCase()}</span>
<span className="hidden xs:inline">|</span>
<span className="hidden xs:inline">{code.split('\n').length} lines</span>
<span className="hidden sm:inline">|</span>
<span className="hidden sm:inline">{code.length} chars</span>
</div>
<div className="hidden md:block">
{isLatexFile && <span>💡 Download and compile .tex file locally</span>}
</div>
</div>
</div>
</Window>
)
}