Skip to content

Instantly share code, notes, and snippets.

@htlin222
Created July 24, 2024 01:04
Show Gist options
  • Save htlin222/2d7655da0ca662252787c5ee5c002d95 to your computer and use it in GitHub Desktop.
Save htlin222/2d7655da0ca662252787c5ee5c002d95 to your computer and use it in GitHub Desktop.
import React, {
useState,
useRef,
useEffect,
useCallback,
useMemo,
} from "react";
import {
Search,
Save,
Send,
PanelLeftClose,
PanelRightClose,
Menu,
} 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 {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
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 [leftPaneVisible, setLeftPaneVisible] = useState(true);
const [rightPaneVisible, setRightPaneVisible] = useState(true);
const [leftPaneWidth, setLeftPaneWidth] = useState(25);
const [rightPaneWidth, setRightPaneWidth] = useState(25);
const [author, setAuthor] = useState("");
const [category, setCategory] = useState("");
const [date, setDate] = useState("");
const [time, setTime] = useState("");
const [isMobile, setIsMobile] = useState(false);
const [isPatientListOpen, setIsPatientListOpen] = useState(false);
const [selectedPatient, setSelectedPatient] = useState(null);
const [autocompleteSnippets, setAutocompleteSnippets] = useState([]);
const [selectedAutocompleteIndex, setSelectedAutocompleteIndex] = useState(0);
const [cursorPosition, setCursorPosition] = useState(0);
const [editorHeight, setEditorHeight] = useState("100%");
const textareaRef = useRef(null);
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 handleKeyDown = useCallback(
(e) => {
if (autocompleteSnippets.length > 0) {
if (e.key === "Enter") {
e.preventDefault();
insertSnippet(autocompleteSnippets[selectedAutocompleteIndex]);
} else if (e.key === "Tab") {
e.preventDefault();
setSelectedAutocompleteIndex(
(prevIndex) => (prevIndex + 1) % autocompleteSnippets.length,
);
} else if (e.key === "Escape") {
setAutocompleteSnippets([]);
}
}
},
[autocompleteSnippets, selectedAutocompleteIndex, insertSnippet],
);
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 filteredSnippets = useMemo(
() =>
snippets.filter((snippet) =>
snippet.toLowerCase().includes(snippetSearchTerm.toLowerCase()),
),
[snippets, snippetSearchTerm],
);
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],
);
useEffect(() => {
const handleResize = () => {
setIsMobile(window.innerWidth < 768);
};
window.addEventListener("resize", handleResize);
handleResize();
return () => window.removeEventListener("resize", handleResize);
}, []);
useEffect(() => {
if (isMobile) {
setLeftPaneVisible(false);
setRightPaneVisible(false);
}
}, [isMobile]);
useEffect(() => {
if (textareaRef.current) {
textareaRef.current.setSelectionRange(cursorPosition, cursorPosition);
}
}, [cursorPosition]);
useEffect(() => {
const updateEditorHeight = () => {
if (isMobile) {
const vh = window.innerHeight * 0.01;
document.documentElement.style.setProperty("--vh", `${vh}px`);
setEditorHeight(`calc(100 * var(--vh, 1vh) - 200px)`);
} else {
setEditorHeight("100%");
}
};
window.addEventListener("resize", updateEditorHeight);
updateEditorHeight();
return () => window.removeEventListener("resize", updateEditorHeight);
}, [isMobile]);
const PatientList = React.memo(({ isDialog = false }) => {
const inputRef = useRef(null);
useEffect(() => {
if (isDialog && inputRef.current) {
inputRef.current.focus();
}
}, [isDialog]);
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);
if (isDialog) setIsPatientListOpen(false);
}}
>
<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="flex h-screen bg-gray-100">
{leftPaneVisible && !isMobile && (
<div
className="bg-white overflow-y-auto relative"
style={{ width: `${leftPaneWidth}%` }}
>
<div className="p-4">
<h2 className="text-xl font-bold mb-4">Patients</h2>
<PatientList />
</div>
</div>
)}
<div className="flex-grow flex flex-col">
<div className="bg-white p-4 flex items-center">
{isMobile ? (
<Button onClick={() => setIsPatientListOpen(true)} className="mr-2">
<Menu className="h-4 w-4" />
</Button>
) : (
<Button
onClick={() => setLeftPaneVisible(!leftPaneVisible)}
className="mr-2"
>
<PanelLeftClose className="h-4 w-4" />
</Button>
)}
<Select onValueChange={setAuthor} value={author}>
<SelectTrigger className="flex-grow mx-2">
<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="flex-grow mx-2"
>
<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>
{!isMobile && (
<Button
onClick={() => setRightPaneVisible(!rightPaneVisible)}
className="ml-2"
>
<PanelRightClose className="h-4 w-4" />
</Button>
)}
</div>
{selectedPatient && (
<div className="bg-white p-4 border-t border-gray-200">
<h3 className="font-semibold text-lg">{selectedPatient.name}</h3>
<p className="text-sm text-gray-600">
Ward: {selectedPatient.wardNumber} | Chart:{" "}
{selectedPatient.chartNumber}
</p>
</div>
)}
<div className="bg-white p-4 flex items-center">
<Input
type="date"
className="mr-2"
value={date}
onChange={(e) => setDate(e.target.value)}
/>
<Input
type="time"
value={time}
onChange={(e) => setTime(e.target.value)}
/>
</div>
<div className="bg-white p-4 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 className="relative flex-grow" style={{ height: editorHeight }}>
<textarea
ref={textareaRef}
className="w-full h-full p-4 bg-white resize-none"
value={editorContent}
onChange={handleEditorChange}
onKeyDown={handleKeyDown}
placeholder="Write your markdown here..."
/>
{autocompleteSnippets.length > 0 && (
<div className="absolute bottom-full left-0 bg-white border border-gray-300 rounded-md shadow-lg">
{autocompleteSnippets.map((snippet, index) => (
<div
key={index}
className={`p-2 cursor-pointer ${index === selectedAutocompleteIndex ? "bg-blue-100" : ""}`}
onClick={() => insertSnippet(snippet)}
>
{snippet}
</div>
))}
</div>
)}
</div>
</div>
{rightPaneVisible && !isMobile && (
<div
className="bg-white overflow-y-auto relative"
style={{ width: `${rightPaneWidth}%` }}
>
<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>
)}
<Dialog open={isPatientListOpen} onOpenChange={setIsPatientListOpen}>
<DialogContent>
<DialogHeader>
<DialogTitle>Select Patient</DialogTitle>
</DialogHeader>
<PatientList isDialog={true} />
</DialogContent>
</Dialog>
</div>
);
};
export default MarkdownEditor;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment