update
This commit is contained in:
parent
c1d50b4c7a
commit
16523150f5
@ -1,9 +1,7 @@
|
||||
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 { Navigate, BrowserRouter, Link, Outlet, Route, Routes } from "react-router";
|
||||
import { marked } from "marked";
|
||||
import { apiInstance, resourcesInstance } from "./api.js";
|
||||
import { resourcesInstance } from "./api.js";
|
||||
import { useStore } from "./store.js";
|
||||
import { useSubject, useTopic, useTopicSyncParams, useTopicsAround } from "./hooks.js";
|
||||
import {
|
||||
@ -20,6 +18,7 @@ import {
|
||||
TextDecreaseIcon,
|
||||
VRuleIcon,
|
||||
} from "./icons/Icons";
|
||||
import ResourcePage from "./ResourcePage.jsx";
|
||||
|
||||
export function App() {
|
||||
return (
|
||||
@ -36,6 +35,7 @@ export function App() {
|
||||
<Route index element={<TopicListView />} />
|
||||
<Route path=":topicId" element={<FileReader />} />
|
||||
</Route>
|
||||
<Route path="/edit" element={<ResourcePage />} />
|
||||
</Routes>
|
||||
</BrowserRouter>
|
||||
);
|
||||
@ -60,12 +60,12 @@ function Layout() {
|
||||
return (
|
||||
<div className="max-w-7xl mx-auto h-full relative flex flex-col">
|
||||
{config.displayTitle && (
|
||||
<div className="w-full px-4 py-2 font-medium text-large text-white bg-blue-600">
|
||||
<span>
|
||||
{topic
|
||||
? `${topic.sequence}: ${topic.title}`
|
||||
: `${subject.name} - Конспект за Държавен Изпит`}
|
||||
</span>
|
||||
<div
|
||||
className="w-full px-4 py-2 font-medium text-large text-white bg-blue-600"
|
||||
style={{ lineHeight: 1.2 }}
|
||||
>
|
||||
<div className="text-sm font-normal">{subject.name}</div>
|
||||
<span>{topic ? `${topic.sequence}: ${topic.title}` : `Конспект за Държавен Изпит`}</span>
|
||||
</div>
|
||||
)}
|
||||
<Outlet />
|
||||
@ -207,7 +207,7 @@ export function TopicListView() {
|
||||
function FileReader() {
|
||||
const topic = useTopicSyncParams();
|
||||
|
||||
if (topic.isLoading) {
|
||||
if (topic && topic.isLoading) {
|
||||
return null;
|
||||
//return "Loading...";
|
||||
}
|
||||
|
||||
338
reader/src/ResourcePage.jsx
Normal file
338
reader/src/ResourcePage.jsx
Normal file
@ -0,0 +1,338 @@
|
||||
import React, { useState, useEffect, useMemo, useRef, useLayoutEffect } from "react";
|
||||
import { useStore } from "./store.js";
|
||||
import { resourcesInstance, apiInstance } from "./api.js";
|
||||
|
||||
function LoadingWrapper() {
|
||||
const isLoading = useStore((state) => state.isLoading);
|
||||
|
||||
if (isLoading) {
|
||||
return "Loading...";
|
||||
}
|
||||
|
||||
return <ResourcePage />;
|
||||
}
|
||||
|
||||
function SelectResource({ onChange }) {
|
||||
const subjects = useStore((state) => state.subjects);
|
||||
|
||||
const [selectedSubject, setSelectedSubject] = useState(-1);
|
||||
const [selectedTopic, setSelectedTopic] = useState(-1);
|
||||
|
||||
function handleChange(newSubject, newTopic) {
|
||||
setSelectedSubject(newSubject);
|
||||
setSelectedTopic(newTopic);
|
||||
|
||||
if (newSubject === -1 || newTopic === -1) {
|
||||
onChange(null);
|
||||
return;
|
||||
}
|
||||
|
||||
const subject = subjects[newSubject];
|
||||
const topic = subject.topics[newTopic];
|
||||
const resource = topic.resources.at(-1);
|
||||
|
||||
onChange({
|
||||
subjectName: subject.name,
|
||||
topic,
|
||||
resource,
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
{/* File Selector */}
|
||||
<div className="bg-white border-b border-gray-200 p-4 flex space-x-6">
|
||||
<div>
|
||||
<label htmlFor="subjectSelect" className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Избери раздел:
|
||||
</label>
|
||||
<select
|
||||
id="subjectSelect"
|
||||
value={selectedSubject}
|
||||
onChange={(event) => {
|
||||
handleChange(parseInt(event.target.value), -1);
|
||||
}}
|
||||
className="px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent bg-white min-w-64"
|
||||
>
|
||||
<option value="-1" disabled>
|
||||
Избери раздел...
|
||||
</option>
|
||||
{subjects.map((subject, sIndex) => (
|
||||
<option key={subject.id} value={sIndex}>
|
||||
{subject.sequence}. {subject.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="topicSelect" className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Избери тема:
|
||||
</label>
|
||||
<select
|
||||
id="topicSelect"
|
||||
value={selectedTopic}
|
||||
onChange={(event) => {
|
||||
handleChange(selectedSubject, parseInt(event.target.value));
|
||||
}}
|
||||
className="px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent bg-white"
|
||||
>
|
||||
<option value="-1" disabled>
|
||||
{selectedSubject === -1 ? "Първо избери раздел" : "Избери тема..."}
|
||||
</option>
|
||||
{selectedSubject !== -1 &&
|
||||
subjects[selectedSubject].topics.map((topic, tIndex) => (
|
||||
<option key={topic.id} value={tIndex}>
|
||||
{topic.sequence}.{" "}
|
||||
{topic.title.length > 60 ? topic.title.substring(0, 60) + "..." : topic.title}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ResourcePage() {
|
||||
return (
|
||||
<Content token="aaaaaa" initialContent="# hola" subjectName="asdasd" topicTitle="tiilte" />
|
||||
);
|
||||
}
|
||||
|
||||
function useFileContent(file) {
|
||||
const [content, setContent] = useState(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
if (!file) {
|
||||
return;
|
||||
}
|
||||
let ignore = false;
|
||||
setIsLoading(true);
|
||||
setContent(null);
|
||||
resourcesInstance.get(`/${file.filename}`).then((res) => {
|
||||
if (!ignore) {
|
||||
setContent(res.data);
|
||||
setIsLoading(false);
|
||||
}
|
||||
});
|
||||
|
||||
return () => {
|
||||
ignore = true;
|
||||
};
|
||||
}, [file]);
|
||||
|
||||
return { content, isLoading };
|
||||
}
|
||||
|
||||
const Content = () => {
|
||||
const authToken = useStore((state) => state.config.token);
|
||||
const changeConfig = useStore((state) => state.changeConfig);
|
||||
const setAuthToken = (token) => changeConfig({ token });
|
||||
|
||||
const [selected, setSelected] = useState(null);
|
||||
const { isLoading, content: initialContent } = useFileContent(selected?.resource);
|
||||
|
||||
const [content, setContent] = useState(initialContent);
|
||||
const [tokenInput, setTokenInput] = useState("");
|
||||
|
||||
const [isSavingLoading, setIsSavingLoading] = useState(false);
|
||||
const [message, setMessage] = useState(null);
|
||||
|
||||
const [prevContent, setPrevContent] = useState(initialContent);
|
||||
if (prevContent !== initialContent) {
|
||||
setPrevContent(initialContent);
|
||||
setContent(initialContent);
|
||||
}
|
||||
|
||||
const handleTokenSubmit = () => {
|
||||
if (tokenInput.trim()) {
|
||||
setAuthToken(tokenInput);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
setIsSavingLoading(true);
|
||||
try {
|
||||
const res = await apiInstance.post(
|
||||
`/resources`,
|
||||
{
|
||||
topicId: selected.topic.id,
|
||||
content: content.trim(),
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
token: authToken,
|
||||
},
|
||||
},
|
||||
);
|
||||
setMessage(`Success! New filename: ${res.data.filename}`);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
setMessage("Something failed :(");
|
||||
if (error.response.status === 401) {
|
||||
setAuthToken(null);
|
||||
}
|
||||
} finally {
|
||||
setTimeout(() => {
|
||||
setMessage(null);
|
||||
}, 8000);
|
||||
|
||||
setIsSavingLoading(false);
|
||||
setContent("");
|
||||
setSelected(null);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
if (window.confirm("Сигурни ли сте, че искате да затворите страницата?")) {
|
||||
window.close();
|
||||
}
|
||||
};
|
||||
|
||||
const handleCopyContent = async () => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(content);
|
||||
alert("Съдържанието е копирано в клипборда!");
|
||||
} catch (err) {
|
||||
console.error("Грешка при копиране:", err);
|
||||
alert("Грешка при копиране на съдържанието");
|
||||
}
|
||||
};
|
||||
|
||||
function handleUndoInitial() {
|
||||
setContent(initialContent);
|
||||
}
|
||||
|
||||
const handleDeleteAll = () => {
|
||||
if (window.confirm("Сигурни ли сте, че искате да изтриете цялото съдържание?")) {
|
||||
setContent("");
|
||||
}
|
||||
};
|
||||
|
||||
// Token authentication screen
|
||||
if (!authToken) {
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 flex items-center justify-center p-8">
|
||||
<div className="bg-white rounded-lg shadow-md p-8 w-full max-w-md">
|
||||
<h2 className="text-2xl font-bold text-gray-800 mb-6 text-center">
|
||||
Въведете токен за достъп
|
||||
</h2>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label htmlFor="token" className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Токен:
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="token"
|
||||
value={tokenInput}
|
||||
onChange={(e) => setTokenInput(e.target.value)}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||
placeholder="Въведете вашия токен..."
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleTokenSubmit}
|
||||
className="w-full bg-blue-600 text-white py-2 px-4 rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 transition-colors"
|
||||
>
|
||||
Влизане
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Main content screen
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 flex flex-col">
|
||||
<SelectResource onChange={(values) => setSelected(values)} />
|
||||
|
||||
{message && <div className="text-lg p-12 text-green-600 bg-gray-200">{message}</div>}
|
||||
{selected && !isLoading && (
|
||||
<>
|
||||
{/* Header */}
|
||||
<div className="bg-white border-b border-gray-200 p-4">
|
||||
<div className="text-2xl font-bold text-gray-800">{selected.subjectName}</div>
|
||||
<div className="text-2xl/7 mt-2 text-gray-800 line-">
|
||||
Тема: {selected.topic.sequence}. {selected.topic.title}
|
||||
</div>
|
||||
<div className="text-sm text-gray-400 mt-6">
|
||||
Topic Id: <span className="font-bold">{selected.topic.id}</span>
|
||||
{" - "}
|
||||
Original Filename: <span className="font-bold">{selected.resource.filename}</span>
|
||||
{" - "}
|
||||
Current Version: <span className="font-bold">{selected.resource.version}</span>
|
||||
{" - "}
|
||||
New Version: <span className="font-bold">{selected.resource.version + 1}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Quick Actions */}
|
||||
<div className="bg-white border-b border-gray-200 p-4">
|
||||
<div className="flex flex-wrap gap-3">
|
||||
<button
|
||||
onClick={handleCopyContent}
|
||||
className="bg-green-600 text-white px-4 py-2 rounded-md hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-green-500 focus:ring-offset-2 transition-colors"
|
||||
>
|
||||
Копирай съдържанието
|
||||
</button>
|
||||
<button
|
||||
onClick={handleDeleteAll}
|
||||
className="bg-red-600 text-white px-4 py-2 rounded-md hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2 transition-colors"
|
||||
>
|
||||
Изтрий всичко
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={handleUndoInitial}
|
||||
className="bg-blue-600 text-white px-4 py-2 rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 transition-colors"
|
||||
>
|
||||
Възстанови оригинал
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Content Editor */}
|
||||
<div className="flex-1 bg-white p-4 flex flex-col">
|
||||
<label htmlFor="content" className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Markdown съдържание:
|
||||
</label>
|
||||
<div className="flex-1">
|
||||
<textarea
|
||||
id="content"
|
||||
value={content}
|
||||
onChange={(e) => setContent(e.target.value)}
|
||||
className="w-full h-full resize-none px-3 py-2 bg-gray-100 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent font-mono text-sm"
|
||||
placeholder="Въведете вашето Markdown съдържание тук..."
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Bottom Actions */}
|
||||
<div className="bg-white border-t border-gray-200 p-4">
|
||||
{isSavingLoading && <span className="flex justify-end mr-8">Saving...</span>}
|
||||
<div className={`flex justify-end gap-3 ${isSavingLoading ? "invisible" : ""}`}>
|
||||
<button
|
||||
onClick={handleCancel}
|
||||
className="bg-gray-600 text-white px-6 py-2 rounded-md hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-gray-500 focus:ring-offset-2 transition-colors"
|
||||
>
|
||||
Отказ
|
||||
</button>
|
||||
<button
|
||||
onClick={handleSave}
|
||||
className="bg-blue-600 text-white px-6 py-2 rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 transition-colors"
|
||||
>
|
||||
Запази
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default LoadingWrapper;
|
||||
@ -21,9 +21,20 @@ server {
|
||||
add_header Access-Control-Allow-Methods "GET, POST, OPTIONS" always;
|
||||
add_header Access-Control-Allow-Headers "Origin, Content-Type, Accept, Authorization" always;
|
||||
|
||||
# Aggressive caching - 1 year
|
||||
add_header Cache-Control "public, max-age=31536000, immutable" always;
|
||||
add_header Expires "Thu, 31 Dec 2037 23:55:55 GMT" always;
|
||||
|
||||
# Set proper content type for markdown files
|
||||
location ~* \.md$ {
|
||||
add_header Content-Type "text/markdown; charset=utf-8";
|
||||
# Aggressive caching for markdown too
|
||||
add_header Cache-Control "public, max-age=31536000, immutable" always;
|
||||
add_header Expires "Thu, 31 Dec 2037 23:55:55 GMT" always;
|
||||
}
|
||||
|
||||
# Enable gzip compression for better performance
|
||||
gzip on;
|
||||
gzip_types text/plain text/css text/markdown application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user