diff --git a/reader/index.html b/reader/index.html index eb48804..acc8ccd 100644 --- a/reader/index.html +++ b/reader/index.html @@ -5,7 +5,6 @@ - Конспект diff --git a/reader/src/App.jsx b/reader/src/App.jsx new file mode 100644 index 0000000..6902412 --- /dev/null +++ b/reader/src/App.jsx @@ -0,0 +1,514 @@ +import { StrictMode, useLayoutEffect, useRef, useState, useEffect, useMemo } from "react"; +import { createRoot } from "react-dom/client"; +import { useParams, Navigate, BrowserRouter, Link, Outlet, Route, Routes } from "react-router"; +import { useShallow } from "zustand/shallow"; +import { marked } from "marked"; +import { apiInstance, resourcesInstance } from "./api.js"; +import { useStore } from "./store.js"; +import { useSubject, useTopic, useTopicSyncParams, useTopicsAround } from "./hooks.js"; +import { + ArrowBackIcon, + ArrowForwardIcon, + MenuBookIcon, + MyLocationIcon, + TitleIcon, + WidthIcon, + EllipsisIcon, + CloseIcon, + JustifyTextIcon, + TextIncreaseIcon, + TextDecreaseIcon, + VRuleIcon, +} from "./icons/Icons"; + +export function App() { + return ( + + + + + + } + > + } /> + } /> + + + + ); +} + +function LoadingWrapper({ children }) { + const isLoading = useStore((state) => state.isLoading); + + if (isLoading) { + return "Loading..."; + } + + return children; +} + +function Layout() { + const config = useStore((state) => state.config); + const subject = useSubject(); + const topic = useTopic(); + + return ( +
+ {config.displayTitle && ( +
+ + {topic + ? `${topic.sequence}: ${topic.title}` + : `${subject.name} - Конспект за Държавен Изпит`} + +
+ )} + +
+ ); +} + +export function TopicListView() { + const itemRefs = useRef({}); + + const subjects = useStore((state) => state.subjects); + + const selectedSubject = useSubject(); + const selectSubject = useStore((state) => state.selectSubject); + const [isSelectingSubject, setIsSelectingSubject] = useState(false); + + const selectedTopic = useTopic(); + const selectTopic = useStore((state) => state.selectTopic); + + const config = useStore((state) => state.config); + const changeConfig = useStore((state) => state.changeConfig); + + useLayoutEffect(() => { + if (selectedTopic) { + itemRefs.current?.[Math.max(selectedTopic.index - 3, 0)].scrollIntoView(); + } + }, [selectedTopic]); + + return ( + <> +
+ {selectedSubject.topics.map((topic, topicIdx) => ( + { + itemRefs.current[topicIdx] = node; + }} + to={`/${topic.id}`} + onClick={() => selectTopic(topicIdx)} + className={`flex px-2 py-1 rounded-md cursor-pointer border-l-4 ${topic.id === selectedTopic?.id ? "bg-blue-100 border-blue-500" : "border-transparent hover:bg-gray-100"}`} + > +
+ {topic.sequence} +
+ + {topic.title} + + + ))} +
+
+
+ + + + + {selectedTopic && ( + + )} +
+ + {isSelectingSubject && ( + <> + +
+ {subjects.map((subject, subjectIdx) => ( + + ))} +
+ + )} +
+
+ {selectedTopic && ( + + Продължи четенето: +
+ + {selectedTopic.sequence}. {selectedTopic.title} + + + )} +
+ + ); +} + +function FileReader() { + const topic = useTopicSyncParams(); + const topicIdx = useStore((state) => state.topicIdx); + + if (!topic) { + return ; + } + + console.log({ topic, topicIdx }); + + return ; +} + +export function Reader({ topic }) { + const config = useStore((state) => state.config); + const changeConfig = useStore((state) => state.changeConfig); + + const { prevTopic, nextTopic } = useTopicsAround(); + + const resourceIdx = useStore((state) => state.resourceIdx); + const selectResource = useStore((state) => state.selectResource); + const [isSelectingResource, setIsSelectingResource] = useState(false); + const selectedResource = topic.resources[resourceIdx]; + + console.log("reader!"); + if (!prevTopic || !nextTopic) { + return "balb"; + } + + return ( + <> +
+ +
+
+ + + +
+
+
+ {config.contentZoomLevel}% +
+ + + + + + + + + + + + {window.innerWidth > 576 && ( + + )} + {topic.resources.length > 1 && ( +
+ + {isSelectingResource && ( + + )} +
+ )} +
+ {isSelectingResource && ( +
+ {topic.resources.map((resource, rIndex) => ( + + ))} +
+ )} +
+
+
+
+ {prevTopic === null ? ( +
+ ) : ( + + + + {prevTopic.sequence - 1 - 1}: {prevTopic.title} + + + )} + {nextTopic === null ? ( +
+ ) : ( + + + {nextTopic.sequence - 1 + 1}: {nextTopic.title} + + + + )} +
+
+ + ); +} + +export function PDFViewer({ file, compact, zoomFactor, justifyText }) { + const iframeRef = useRef(null); + const [content, setContent] = useState(null); + const htmlContent = useMemo(() => { + const fileContent = ` + + + + + + + + + + + ${content} + + + `; + return fileContent; + }, [content, compact, justifyText, zoomFactor]); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(null); + + const contentZoomLevel = useStore((state) => state.config.contentZoomLevel); + + useEffect(() => { + if (iframeRef.current && iframeRef.current.contentDocument) { + iframeRef.current.contentDocument.body.style.zoom = `${contentZoomLevel}%`; + } + }, [contentZoomLevel]); + + useEffect(() => { + const fetchFile = async () => { + try { + setIsLoading(true); + const response = await resourcesInstance.get(`/${file.filename}`); + + if (!response.ok) { + throw new Error(`Failed to load file: ${response.status}`); + } + + let fileContent = await response.text(); + if (fileContent === "") { + fileContent = "**No Data!**"; + } + fileContent = marked.parse(fileContent); + + setContent(fileContent); + setError(null); + } catch (err) { + console.error("Error loading file:", err); + setError(err.message); + } finally { + setIsLoading(false); + } + }; + + fetchFile(); + }, [file]); + + if (error) { + return
Error: {error}
; + } + + if (isLoading) { + return ; + } + + return ( +
+