owned this note
owned this note
Published
Linked with GitHub
# NOTE系統窄螢幕佈局
## 預設為中、右 pane,選擇病人的pane 改為side panel
- 理由: 對於比較窄(4:3)的螢幕來說,一開始有三個Panel會太擠,反而要花很多時間去調整理想的工作環境
- 不如把左邊 i.e. 選擇病人的功能,做成side panel


## Claude
https://claude.site/artifacts/cd77ed8d-a6d5-4f60-acaf-d1da45d626bd
## Source Code
```javascript
import React, { useState, useRef, useEffect, useCallback, useMemo } from 'react';
import { Search, Save, Send, GripVertical, Users } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Card, CardContent } from "@/components/ui/card";
import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetTrigger } from "@/components/ui/sheet";
const MarkdownEditor = () => {
const [patients, setPatients] = useState([
{ name: "John Doe", wardNumber: "A101", chartNumber: "12345" },
{ name: "Jane Smith", wardNumber: "B202", chartNumber: "67890" },
{ name: "Bob Johnson", wardNumber: "C303", chartNumber: "11223" },
]);
const [snippets, setSnippets] = useState(['# Heading', '## Subheading', '- List item']);
const [editorContent, setEditorContent] = useState('');
const [snippetSearchTerm, setSnippetSearchTerm] = useState('');
const [patientSearchTerm, setPatientSearchTerm] = useState('');
const [selectedPatient, setSelectedPatient] = useState(null);
const [autocompleteSnippets, setAutocompleteSnippets] = useState([]);
const [selectedAutocompleteIndex, setSelectedAutocompleteIndex] = useState(0);
const [cursorPosition, setCursorPosition] = useState(0);
const [author, setAuthor] = useState('');
const [category, setCategory] = useState('');
const [date, setDate] = useState('');
const [time, setTime] = useState('');
const [isDragging, setIsDragging] = useState(false);
const [splitPosition, setSplitPosition] = useState(50); // percentage
const dragRef = useRef(null);
const containerRef = useRef(null);
const textareaRef = useRef(null);
// Dragging functionality
const handleDragStart = useCallback((e) => {
e.preventDefault();
setIsDragging(true);
document.addEventListener('mousemove', handleDrag);
document.addEventListener('mouseup', handleDragEnd);
}, []);
const handleDrag = useCallback((e) => {
if (!containerRef.current) return;
const containerRect = containerRef.current.getBoundingClientRect();
const containerWidth = containerRect.width;
const mouseX = e.clientX - containerRect.left;
const newPosition = (mouseX / containerWidth) * 100;
const clampedPosition = Math.min(Math.max(newPosition, 20), 80);
setSplitPosition(clampedPosition);
}, []);
const handleDragEnd = useCallback(() => {
setIsDragging(false);
document.removeEventListener('mousemove', handleDrag);
document.removeEventListener('mouseup', handleDragEnd);
}, [handleDrag]);
// Other handlers
const handleSave = useCallback(() => {
console.log('Saving content:', editorContent);
}, [editorContent]);
const handlePublish = useCallback(() => {
console.log('Publishing content:', editorContent);
}, [editorContent]);
const handleEditorChange = useCallback((e) => {
const newContent = e.target.value;
setEditorContent(newContent);
setCursorPosition(e.target.selectionStart);
const lastWord = newContent.slice(0, e.target.selectionStart).split(/\s/).pop();
if (lastWord.length >= 1) {
const matches = snippets.filter(snippet =>
snippet.toLowerCase().startsWith(lastWord.toLowerCase())
);
setAutocompleteSnippets(matches);
setSelectedAutocompleteIndex(0);
} else {
setAutocompleteSnippets([]);
}
}, [snippets]);
const insertSnippet = useCallback((snippet) => {
const beforeCursor = editorContent.slice(0, cursorPosition);
const afterCursor = editorContent.slice(cursorPosition);
const lastWord = beforeCursor.split(/\s/).pop();
const completionPart = snippet.slice(lastWord.length);
const newContent = beforeCursor + completionPart + afterCursor;
setEditorContent(newContent);
setCursorPosition(cursorPosition + completionPart.length);
setAutocompleteSnippets([]);
}, [editorContent, cursorPosition]);
const handlePatientSearch = useCallback((e) => {
setPatientSearchTerm(e.target.value);
}, []);
const handleSnippetSearch = useCallback((e) => {
setSnippetSearchTerm(e.target.value);
}, []);
const highlightSearchTerm = useCallback((text, searchTerm) => {
if (!searchTerm) return text;
const parts = text.split(new RegExp(`(${searchTerm})`, 'gi'));
return parts.map((part, index) =>
part.toLowerCase() === searchTerm.toLowerCase() ?
<span key={index} style={{ backgroundColor: '#3D6869', color: 'white' }}>{part}</span> :
part
);
}, []);
const filteredPatients = useMemo(() =>
patients.filter(patient =>
patient.name.toLowerCase().includes(patientSearchTerm.toLowerCase()) ||
patient.wardNumber.toLowerCase().includes(patientSearchTerm.toLowerCase()) ||
patient.chartNumber.toLowerCase().includes(patientSearchTerm.toLowerCase())
), [patients, patientSearchTerm]);
const filteredSnippets = useMemo(() =>
snippets.filter(snippet =>
snippet.toLowerCase().includes(snippetSearchTerm.toLowerCase())
), [snippets, snippetSearchTerm]);
const PatientList = React.memo(() => {
const inputRef = useRef(null);
return (
<div className="space-y-4">
<div className="mb-4 relative">
<Input
ref={inputRef}
type="text"
placeholder="Search patients"
value={patientSearchTerm}
onChange={handlePatientSearch}
className="pl-10"
/>
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-gray-400" />
</div>
{filteredPatients.map((patient) => (
<Card
key={patient.chartNumber}
className="cursor-pointer hover:shadow-md transition-shadow hover:bg-gray-100"
onClick={() => setSelectedPatient(patient)}
>
<CardContent className="p-4">
<h3 className="font-semibold text-lg">{highlightSearchTerm(patient.name, patientSearchTerm)}</h3>
<p className="text-sm text-gray-600">Ward: {highlightSearchTerm(patient.wardNumber, patientSearchTerm)}</p>
<p className="text-sm text-gray-600">Chart: {highlightSearchTerm(patient.chartNumber, patientSearchTerm)}</p>
</CardContent>
</Card>
))}
</div>
);
});
return (
<div className="h-screen bg-gray-100 flex flex-col">
{/* Top Bar */}
<div className="bg-white p-4 flex items-center gap-2">
<Sheet>
<SheetTrigger asChild>
<Button variant="outline">
<Users className="mr-2 h-4 w-4" />
{selectedPatient ? selectedPatient.name : "選擇病人"}
</Button>
</SheetTrigger>
<SheetContent side="left" className="w-[400px]">
<SheetHeader>
<SheetTitle>選擇病人</SheetTitle>
</SheetHeader>
<PatientList />
</SheetContent>
</Sheet>
<Select onValueChange={setAuthor} value={author}>
<SelectTrigger className="w-[200px]">
<SelectValue placeholder="Select author" />
</SelectTrigger>
<SelectContent>
<SelectItem value="doctor_a">Doctor A</SelectItem>
<SelectItem value="doctor_b">Doctor B</SelectItem>
<SelectItem value="doctor_c">Doctor C</SelectItem>
</SelectContent>
</Select>
<Select onValueChange={setCategory} value={category} className="w-[200px]">
<SelectTrigger>
<SelectValue placeholder="Select category" />
</SelectTrigger>
<SelectContent>
<SelectItem value="progress_note">Progress Note</SelectItem>
<SelectItem value="admission_note">Admission Note</SelectItem>
<SelectItem value="procedure_note">Procedure Note</SelectItem>
</SelectContent>
</Select>
<Input
type="date"
className="w-[150px]"
value={date}
onChange={(e) => setDate(e.target.value)}
/>
<Input
type="time"
className="w-[150px]"
value={time}
onChange={(e) => setTime(e.target.value)}
/>
<div className="ml-auto flex gap-2">
<Button onClick={handleSave}>
<Save className="mr-2 h-4 w-4" /> Save
</Button>
<Button onClick={handlePublish}>
<Send className="mr-2 h-4 w-4" /> Publish
</Button>
</div>
</div>
{/* Main Content */}
<div className="flex-1 flex relative" ref={containerRef}>
{/* Editor Pane */}
<div
className="h-full overflow-hidden"
style={{ width: `${splitPosition}%` }}
>
<textarea
ref={textareaRef}
className="w-full h-full p-4 bg-white resize-none"
value={editorContent}
onChange={handleEditorChange}
placeholder="Write your markdown here..."
/>
</div>
{/* Draggable Divider */}
<div
ref={dragRef}
className={`w-4 h-full bg-gray-100 cursor-col-resize flex items-center justify-center hover:bg-gray-200 active:bg-gray-300 select-none ${isDragging ? 'bg-gray-300' : ''}`}
onMouseDown={handleDragStart}
>
<GripVertical className="h-6 w-6 text-gray-400" />
</div>
{/* Snippets Pane */}
<div
className="h-full bg-white overflow-y-auto"
style={{ width: `${100 - splitPosition}%` }}
>
<div className="p-4">
<h2 className="text-xl font-bold mb-4">Snippets</h2>
<div className="mb-4 relative">
<Input
type="text"
placeholder="Search snippets"
value={snippetSearchTerm}
onChange={handleSnippetSearch}
className="pl-10"
/>
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-gray-400" />
</div>
<ul>
{filteredSnippets.map((snippet, index) => (
<li
key={index}
className="mb-2 cursor-pointer hover:text-blue-500"
onClick={() => insertSnippet(snippet)}
>
{highlightSearchTerm(snippet, snippetSearchTerm)}
</li>
))}
</ul>
</div>
</div>
</div>
</div>
);
};
export default MarkdownEditor;
```