update
This commit is contained in:
@@ -1,8 +1,9 @@
|
||||
import { StrictMode, useLayoutEffect, useRef, useState, useEffect } from "react";
|
||||
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 "./index.css";
|
||||
import { useShallow } from "zustand/shallow";
|
||||
import { marked } from "marked";
|
||||
import { useStore } from "./store.js";
|
||||
import {
|
||||
ArrowBackIcon,
|
||||
@@ -11,6 +12,8 @@ import {
|
||||
MyLocationIcon,
|
||||
TitleIcon,
|
||||
WidthIcon,
|
||||
EllipsisIcon,
|
||||
CloseIcon,
|
||||
} from "./icons/Icons";
|
||||
|
||||
createRoot(document.getElementById("root")).render(
|
||||
@@ -43,6 +46,10 @@ function Layout() {
|
||||
);
|
||||
}
|
||||
|
||||
const SUBJECTS = [
|
||||
{ id: 0, name: "ВЪТРЕШНИ БОЛЕСТИ" },
|
||||
{ id: 1, name: "ФАРМАКОЛОГИЯ" },
|
||||
];
|
||||
export function TopicListView() {
|
||||
const itemRefs = useRef({});
|
||||
const topics = useStore(
|
||||
@@ -50,6 +57,12 @@ export function TopicListView() {
|
||||
);
|
||||
const selectedTopic = useStore((state) => state.selectedTopic);
|
||||
const selectTopic = useStore((state) => state.selectTopic);
|
||||
const selectedSubjectIndex = useStore((state) => state.subject);
|
||||
const setSelectedSubjectIndex = useStore((state) => state.selectSubject);
|
||||
const config = useStore((state) => state.config);
|
||||
const changeConfig = useStore((state) => state.changeConfig);
|
||||
|
||||
const [isSelectingSubject, setIsSelectingSubject] = useState(false);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (selectedTopic && selectedTopic.id !== null) {
|
||||
@@ -59,7 +72,7 @@ export function TopicListView() {
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex-1 overflow-y-scroll">
|
||||
<div className="flex-1 overflow-y-scroll pb-[156px]">
|
||||
{topics.map((topic, i) => (
|
||||
<Link
|
||||
key={topic.id}
|
||||
@@ -75,38 +88,90 @@ export function TopicListView() {
|
||||
>
|
||||
{i + 1}
|
||||
</div>
|
||||
<span className="ml-2">
|
||||
<span
|
||||
className={`leading-5 ${topic.id === selectedTopic?.id ? "font-medium" : "font-normal"}`}
|
||||
>
|
||||
{topic.title}
|
||||
</span>
|
||||
<span
|
||||
className={`ml-2 leading-5 ${topic.id === selectedTopic?.id ? "font-medium" : "font-normal"} ${config.wrapTopicTitles ? "truncate" : ""}`}
|
||||
>
|
||||
{topic.title}
|
||||
</span>
|
||||
</Link>
|
||||
))}
|
||||
{selectedTopic !== null && (
|
||||
<div className="sticky bottom-0 p-4 w-full flex flex-col">
|
||||
<div className="w-full flex justify-between items-center gap-2">
|
||||
<Link
|
||||
to={`/${selectedTopic.id}`}
|
||||
className="w-full p-2 bg-blue-600 hover:bg-blue-700 cursor-pointer truncate rounded-md text-sm text-white text-center shadow-md transition-colors"
|
||||
>
|
||||
<span>Продължи четенето:</span>
|
||||
<br />
|
||||
<span className="font-medium">{selectedTopic.title}</span>
|
||||
</Link>
|
||||
<button
|
||||
className="px-3 py-3 bg-teal-500 hover:bg-teal-300 cursor-pointer rounded-full flex items-center justify-center shadow-md transition-colors"
|
||||
onClick={() => {
|
||||
itemRefs.current?.[Math.max(selectedTopic.index - 3, 0)].scrollIntoView({
|
||||
behavior: "smooth",
|
||||
});
|
||||
}}
|
||||
>
|
||||
<MyLocationIcon className="h-5 w-5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="absolute bottom-0 p-4 pb-6 w-full flex flex-col">
|
||||
<div className="ml-auto p-2 flex space-x-1 h-[60px]">
|
||||
<button
|
||||
className={`cursor-pointer p-2 rounded-full text-white border ${config.displayTitle ? "bg-blue-100 border-blue-400" : "bg-gray-100 border-gray-400"}`}
|
||||
onClick={() => changeConfig({ displayTitle: !config.displayTitle })}
|
||||
>
|
||||
<TitleIcon className="fill-gray-600" />
|
||||
</button>
|
||||
|
||||
<button
|
||||
className={`cursor-pointer p-2 rounded-full text-white border ${config.wrapTopicTitles ? "bg-blue-100 border-blue-400" : "bg-gray-100 border-gray-400"}`}
|
||||
onClick={() => changeConfig({ wrapTopicTitles: !config.wrapTopicTitles })}
|
||||
>
|
||||
<EllipsisIcon className="fill-gray-600" />
|
||||
</button>
|
||||
|
||||
{selectedTopic !== null && (
|
||||
<button
|
||||
className="px-3 py-3 bg-teal-500 hover:bg-teal-300 cursor-pointer rounded-full flex items-center justify-center shadow-md transition-colors"
|
||||
onClick={() => {
|
||||
itemRefs.current?.[Math.max(selectedTopic.index - 3, 0)].scrollIntoView({
|
||||
behavior: "smooth",
|
||||
});
|
||||
}}
|
||||
>
|
||||
<MyLocationIcon className="h-5 w-5" />
|
||||
</button>
|
||||
)}
|
||||
<div className="relative flex ml-2">
|
||||
<button
|
||||
className={`${isSelectingSubject ? "invisible" : ""} flex-1 shadow-xl cursor-pointer px-2 py-1 rounded-md text-xs text-blue-800 font-medium whitespace-nowrap bg-blue-100/50 backdrop-blur hover:bg-blue-200/50 border border-blue-100`}
|
||||
onClick={() => setIsSelectingSubject(true)}
|
||||
>
|
||||
{SUBJECTS[selectedSubjectIndex].name}
|
||||
</button>
|
||||
{isSelectingSubject && (
|
||||
<>
|
||||
<button
|
||||
className={`absolute w-full h-full flex-1 flex justify-center items-center cursor-pointer rounded-md hover:backdrop-blur hover:bg-blue-100/30`}
|
||||
onClick={() => setIsSelectingSubject(false)}
|
||||
>
|
||||
<CloseIcon className="fill-gray-600" />
|
||||
</button>
|
||||
<div className="absolute bottom-full right-0 p-2 flex space-x-1">
|
||||
{SUBJECTS.map((subject, vIndex) => (
|
||||
<button
|
||||
key={subject.id}
|
||||
className={`flex-1 shadow-xl cursor-pointer px-2 py-1 rounded-md text-xs whitespace-nowrap h-[44px] ${
|
||||
selectedSubjectIndex === vIndex
|
||||
? "bg-blue-100 text-blue-800 font-medium border border-blue-400"
|
||||
: "bg-gray-100 hover:bg-gray-200 border border-gray-400"
|
||||
}`}
|
||||
onClick={() => {
|
||||
setSelectedSubjectIndex(vIndex);
|
||||
setIsSelectingSubject(false);
|
||||
}}
|
||||
>
|
||||
{subject.name}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{selectedTopic !== null && (
|
||||
<Link
|
||||
to={`/${selectedTopic.id}`}
|
||||
className="w-full p-2 bg-blue-600 hover:bg-blue-700 cursor-pointer truncate rounded-md text-sm text-white text-center shadow-md transition-colors"
|
||||
>
|
||||
<span>Продължи четенето:</span>
|
||||
<br />
|
||||
<span className="font-medium">
|
||||
{selectedTopic.index + 1}. {selectedTopic.title}
|
||||
</span>
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
@@ -129,7 +194,6 @@ export function FileReader() {
|
||||
}
|
||||
|
||||
export function Reader({ topic }) {
|
||||
console.log("render!");
|
||||
const config = useStore((state) => state.config);
|
||||
const changeConfig = useStore((state) => state.changeConfig);
|
||||
const selectedVersion = useStore((state) => state.topicVersions[topic.id] ?? 0);
|
||||
@@ -139,21 +203,26 @@ export function Reader({ topic }) {
|
||||
const prevTopic = getTopicAtIndex(topic.index - 1);
|
||||
const nextTopic = getTopicAtIndex(topic.index + 1);
|
||||
|
||||
const [isSelectingVersion, setIsSelectingVersion] = useState(false);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex-1">
|
||||
<PDFViewer file={topic.files[selectedVersion]} compact={config.narrowMode} />
|
||||
<div className="absolute bottom-10 flex justify-between px-4 py-2 w-full z-999">
|
||||
<div className="flex space-x-2">
|
||||
<Link to="/" className="cursor-pointer p-2 rounded-full bg-blue-600 text-white">
|
||||
<div className="flex w-full space-x-2">
|
||||
<Link to="/" className="cursor-pointer p-2 rounded-full bg-blue-600 text-white mr-auto">
|
||||
<MenuBookIcon className="fill-gray-100" />
|
||||
</Link>
|
||||
</div>
|
||||
<div className="flex space-x-1">
|
||||
<button
|
||||
className={`cursor-pointer p-2 rounded-full text-white border ${config.displayTitle ? "bg-blue-100 border-blue-400" : "bg-gray-100 border-gray-400"}`}
|
||||
onClick={() => changeConfig({ displayTitle: !config.displayTitle })}
|
||||
>
|
||||
<TitleIcon className="fill-gray-600" />
|
||||
</button>
|
||||
|
||||
{window.innerWidth > 576 && (
|
||||
<button
|
||||
className={`cursor-pointer p-2 rounded-full text-white border ${config.narrowMode ? "bg-blue-100 border-blue-400" : "bg-gray-100 border-gray-400"}`}
|
||||
@@ -162,18 +231,41 @@ export function Reader({ topic }) {
|
||||
<WidthIcon className="fill-gray-600" />
|
||||
</button>
|
||||
)}
|
||||
{topic.files.length > 1 && (
|
||||
<div className="relative flex ml-2">
|
||||
<button
|
||||
key={topic.files[selectedVersion].file}
|
||||
className={`${isSelectingVersion ? "invisible" : ""} flex-1 shadow-xl cursor-pointer px-2 py-1 rounded-md text-xs text-blue-800 font-medium whitespace-nowrap bg-blue-100/50 backdrop-blur hover:bg-blue-200/50 border border-blue-100`}
|
||||
onClick={() => setIsSelectingVersion(true)}
|
||||
>
|
||||
Версия {selectedVersion + 1}
|
||||
</button>
|
||||
{isSelectingVersion && (
|
||||
<button
|
||||
key={topic.files[selectedVersion].file}
|
||||
className={`absolute w-full h-full flex-1 flex justify-center items-center cursor-pointer rounded-md hover:backdrop-blur hover:bg-blue-100/30`}
|
||||
onClick={() => setIsSelectingVersion(false)}
|
||||
>
|
||||
<CloseIcon className="fill-gray-600" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{topic.files.length > 1 && (
|
||||
<div className="flex space-x-1">
|
||||
{isSelectingVersion && (
|
||||
<div className="absolute bottom-full right-0 px-4 flex space-x-1 h-10">
|
||||
{topic.files.map((file, vIndex) => (
|
||||
<button
|
||||
key={file}
|
||||
className={`flex-1 cursor-pointer px-2 py-1 rounded-md text-xs whitespace-nowrap ${
|
||||
className={`flex-1 shadow-xl cursor-pointer px-2 py-1 rounded-md text-xs whitespace-nowrap ${
|
||||
selectedVersion === vIndex
|
||||
? "bg-blue-100 text-blue-800 font-medium"
|
||||
: "bg-gray-100 hover:bg-gray-200"
|
||||
? "bg-blue-100 text-blue-800 font-medium border border-blue-400"
|
||||
: "bg-gray-100 hover:bg-gray-200 border border-gray-400"
|
||||
}`}
|
||||
onClick={() => selectVersion(topic.id, vIndex)}
|
||||
onClick={() => {
|
||||
selectVersion(topic.id, vIndex);
|
||||
setIsSelectingVersion(false);
|
||||
}}
|
||||
>
|
||||
Версия {vIndex + 1}
|
||||
</button>
|
||||
@@ -218,29 +310,15 @@ export function Reader({ topic }) {
|
||||
|
||||
export function PDFViewer({ file, compact }) {
|
||||
const [content, setContent] = useState(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [error, setError] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchFile = async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const response = await fetch(`/files_html/${file}`);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to load file: ${response.status}`);
|
||||
}
|
||||
|
||||
let fileContent = await response.text();
|
||||
|
||||
// If no head tag, add a basic HTML structure with fonts
|
||||
fileContent = `
|
||||
const htmlContent = useMemo(() => {
|
||||
const fileContent = `
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/github-markdown-css/5.4.0/github-markdown-light.min.css">
|
||||
<style>
|
||||
body {
|
||||
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
|
||||
@@ -248,7 +326,13 @@ export function PDFViewer({ file, compact }) {
|
||||
color: #333;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
padding-bottom: 40px;
|
||||
margin: 0;
|
||||
padding: 0 12px 40px;
|
||||
${compact ? "max-width: 36rem; margin: 0 auto;" : ""}
|
||||
}
|
||||
p {
|
||||
text-align: justify;
|
||||
text-justify: inter-word;
|
||||
}
|
||||
pre, code {
|
||||
font-family: 'SF Mono', Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace;
|
||||
@@ -256,10 +340,28 @@ export function PDFViewer({ file, compact }) {
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
${fileContent}
|
||||
${content}
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
return fileContent;
|
||||
}, [content, compact]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [error, setError] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchFile = async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const mdPath = `/files_md/${file.filename}`;
|
||||
const response = await fetch(mdPath);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to load file: ${response.status}`);
|
||||
}
|
||||
|
||||
let fileContent = await response.text();
|
||||
fileContent = marked.parse(fileContent);
|
||||
|
||||
setContent(fileContent);
|
||||
setError(null);
|
||||
@@ -283,9 +385,9 @@ export function PDFViewer({ file, compact }) {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`w-full h-full overflow-hidden ${compact ? "max-w-xl mx-auto" : ""}`}>
|
||||
<div className={`w-full h-full overflow-hidden`}>
|
||||
<iframe
|
||||
srcDoc={content}
|
||||
srcDoc={htmlContent}
|
||||
title={`File: ${file}`}
|
||||
className="w-full h-full border-0"
|
||||
key={file}
|
||||
|
||||
Reference in New Issue
Block a user