mirror of
https://github.com/wgh136/nysoure.git
synced 2025-09-27 04:17:23 +00:00
serve frontend
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -2,3 +2,4 @@
|
|||||||
*.iml
|
*.iml
|
||||||
test.db
|
test.db
|
||||||
.idea/
|
.idea/
|
||||||
|
build/
|
15
build.py
Normal file
15
build.py
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import subprocess
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
|
||||||
|
if os.path.exists("build"):
|
||||||
|
shutil.rmtree("build")
|
||||||
|
os.mkdir("build")
|
||||||
|
|
||||||
|
subprocess.run(["go", "build", "-o", "build/", "main.go"])
|
||||||
|
|
||||||
|
os.chdir("./frontend")
|
||||||
|
subprocess.run(["npm", "install"], shell=True)
|
||||||
|
subprocess.run(["npm", "run", "build"], shell=True)
|
||||||
|
os.chdir("..")
|
||||||
|
shutil.copytree("./frontend/dist", "./build/static")
|
@@ -1,11 +1,41 @@
|
|||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<base href="/">
|
||||||
|
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta content="IE=Edge" http-equiv="X-UA-Compatible">
|
||||||
|
<meta name="description" content="{{Description}}">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Vite + React + TS</title>
|
|
||||||
|
<!-- SEO meta -->
|
||||||
|
<meta name="twitter:card" content="summary_large_image">
|
||||||
|
<meta name="twitter:title" content="{{Title}}">
|
||||||
|
<meta name="twitter:description" content="{{Description}}">
|
||||||
|
<meta name="twitter:image" content="{{Preview}}">
|
||||||
|
<meta property="og:title" content="{{Title}}">
|
||||||
|
<meta property="og:type" content="website">
|
||||||
|
<meta property="og:url" content="{{Url}}">
|
||||||
|
<meta property="og:image" content="{{Preview}}">
|
||||||
|
<meta property="og:description" content="{{Description}}">
|
||||||
|
<meta property="og:site_name" content={{SiteName}}>
|
||||||
|
|
||||||
|
<!-- iOS meta tags & icons -->
|
||||||
|
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||||
|
<meta name="apple-mobile-web-app-status-bar-style" content="black">
|
||||||
|
<meta name="apple-mobile-web-app-title" content="Nysoure">
|
||||||
|
<link rel="apple-touch-icon" href="/icon-192.png">
|
||||||
|
|
||||||
|
<!-- Favicon -->
|
||||||
|
<link rel="icon" href="/favicon.ico" type="image/x-icon">
|
||||||
|
|
||||||
|
<title>{{Title}}</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
<script>
|
||||||
|
window.serverName = "{{SiteName}}";
|
||||||
|
window.cloudflareTurnstileSiteKey = "{{CFTurnstileSiteKey}}";
|
||||||
|
</script>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
<script type="module" src="/src/main.tsx"></script>
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
</body>
|
</body>
|
||||||
|
BIN
frontend/public/favicon.ico
Normal file
BIN
frontend/public/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 5.1 KiB |
BIN
frontend/public/icon-192.png
Normal file
BIN
frontend/public/icon-192.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 15 KiB |
@@ -1,12 +1,19 @@
|
|||||||
import {User} from "./network/models.ts";
|
import {User} from "./network/models.ts";
|
||||||
|
|
||||||
|
interface MyWindow extends Window {
|
||||||
|
serverName?: string;
|
||||||
|
cloudflareTurnstileSiteKey?: string;
|
||||||
|
}
|
||||||
|
|
||||||
class App {
|
class App {
|
||||||
appName = "资源库"
|
appName = "Nysoure"
|
||||||
|
|
||||||
user: User | null = null;
|
user: User | null = null;
|
||||||
|
|
||||||
token: string | null = null;
|
token: string | null = null;
|
||||||
|
|
||||||
|
cloudflareTurnstileSiteKey: string | null = null;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.init();
|
this.init();
|
||||||
}
|
}
|
||||||
@@ -20,6 +27,8 @@ class App {
|
|||||||
if (tokenJson) {
|
if (tokenJson) {
|
||||||
this.token = JSON.parse(tokenJson);
|
this.token = JSON.parse(tokenJson);
|
||||||
}
|
}
|
||||||
|
this.appName = (window as MyWindow).serverName || this.appName;
|
||||||
|
this.cloudflareTurnstileSiteKey = (window as MyWindow).cloudflareTurnstileSiteKey || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
saveData() {
|
saveData() {
|
||||||
|
@@ -10,7 +10,7 @@ export function ErrorAlert({ message, className }: { message: string, className?
|
|||||||
|
|
||||||
export function InfoAlert({ message, className }: { message: string, className?: string }) {
|
export function InfoAlert({ message, className }: { message: string, className?: string }) {
|
||||||
return <div role="alert" className={`alert alert-info ${className}`}>
|
return <div role="alert" className={`alert alert-info ${className}`}>
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="h-6 w-6 shrink-0 stroke-current">
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" className="h-6 w-6 shrink-0 stroke-current">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
||||||
</svg>
|
</svg>
|
||||||
<span>{message}</span>
|
<span>{message}</span>
|
||||||
|
@@ -32,7 +32,7 @@ export default function Navigator() {
|
|||||||
<SearchBar />
|
<SearchBar />
|
||||||
<UploadingSideBar />
|
<UploadingSideBar />
|
||||||
{
|
{
|
||||||
app.isAdmin() && <button className={"btn btn-circle btn-ghost"} onClick={() => {
|
app.isLoggedIn() && <button className={"btn btn-circle btn-ghost"} onClick={() => {
|
||||||
navigate("/manage");
|
navigate("/manage");
|
||||||
}}>
|
}}>
|
||||||
<MdSettings size={24} />
|
<MdSettings size={24} />
|
||||||
|
@@ -116,4 +116,6 @@ export interface ServerConfig {
|
|||||||
allow_register: boolean;
|
allow_register: boolean;
|
||||||
cloudflare_turnstile_site_key: string;
|
cloudflare_turnstile_site_key: string;
|
||||||
cloudflare_turnstile_secret_key: string;
|
cloudflare_turnstile_secret_key: string;
|
||||||
|
server_name: string;
|
||||||
|
server_description: string;
|
||||||
}
|
}
|
||||||
|
@@ -1,6 +1,12 @@
|
|||||||
|
import { useEffect } from "react";
|
||||||
import ResourcesView from "../components/resources_view.tsx";
|
import ResourcesView from "../components/resources_view.tsx";
|
||||||
import {network} from "../network/network.ts";
|
import {network} from "../network/network.ts";
|
||||||
|
import { app } from "../app.ts";
|
||||||
|
|
||||||
export default function HomePage() {
|
export default function HomePage() {
|
||||||
|
useEffect(() => {
|
||||||
|
document.title = app.appName;
|
||||||
|
}, [])
|
||||||
|
|
||||||
return <ResourcesView loader={(page) => network.getResources(page)}></ResourcesView>
|
return <ResourcesView loader={(page) => network.getResources(page)}></ResourcesView>
|
||||||
}
|
}
|
@@ -1,4 +1,4 @@
|
|||||||
import {FormEvent, useState} from "react";
|
import {FormEvent, useEffect, useState} from "react";
|
||||||
import {network} from "../network/network.ts";
|
import {network} from "../network/network.ts";
|
||||||
import {app} from "../app.ts";
|
import {app} from "../app.ts";
|
||||||
import {useNavigate} from "react-router";
|
import {useNavigate} from "react-router";
|
||||||
@@ -32,6 +32,10 @@ export default function LoginPage() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
document.title = t("Login");
|
||||||
|
}, [])
|
||||||
|
|
||||||
return <div className={"flex items-center justify-center w-full h-full bg-base-200"} id={"login-page"}>
|
return <div className={"flex items-center justify-center w-full h-full bg-base-200"} id={"login-page"}>
|
||||||
<div className={"w-96 card card-border bg-base-100 border-base-300"}>
|
<div className={"w-96 card card-border bg-base-100 border-base-300"}>
|
||||||
<form onSubmit={onSubmit}>
|
<form onSubmit={onSubmit}>
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
import {MdMenu, MdOutlineBadge, MdOutlinePerson, MdOutlineStorage} from "react-icons/md";
|
import { MdMenu, MdOutlineBadge, MdOutlinePerson, MdOutlineStorage } from "react-icons/md";
|
||||||
import {ReactNode, useEffect, useState} from "react";
|
import { ReactNode, useEffect, useState } from "react";
|
||||||
import StorageView from "./manage_storage_page.tsx";
|
import StorageView from "./manage_storage_page.tsx";
|
||||||
import UserView from "./manage_user_page.tsx";
|
import UserView from "./manage_user_page.tsx";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
@@ -24,6 +24,10 @@ export default function ManagePage() {
|
|||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
document.title = t("Manage");
|
||||||
|
}, [])
|
||||||
|
|
||||||
const buildItem = (title: string, icon: ReactNode, p: number) => {
|
const buildItem = (title: string, icon: ReactNode, p: number) => {
|
||||||
return <li key={title} onClick={() => {
|
return <li key={title} onClick={() => {
|
||||||
setPage(p);
|
setPage(p);
|
||||||
@@ -49,20 +53,20 @@ export default function ManagePage() {
|
|||||||
]
|
]
|
||||||
|
|
||||||
const pageComponents = [
|
const pageComponents = [
|
||||||
<ManageMePage/>,
|
<ManageMePage />,
|
||||||
<StorageView/>,
|
<StorageView />,
|
||||||
<UserView/>,
|
<UserView />,
|
||||||
<ManageServerConfigPage/>,
|
<ManageServerConfigPage />,
|
||||||
]
|
]
|
||||||
|
|
||||||
return <div className="drawer lg:drawer-open">
|
return <div className="drawer lg:drawer-open">
|
||||||
<input id="my-drawer-2" type="checkbox" className="drawer-toggle"/>
|
<input id="my-drawer-2" type="checkbox" className="drawer-toggle" />
|
||||||
<div className="drawer-content" style={{
|
<div className="drawer-content" style={{
|
||||||
height: "calc(100vh - 64px)",
|
height: "calc(100vh - 64px)",
|
||||||
}}>
|
}}>
|
||||||
<div className={"flex w-full h-14 items-center gap-2 px-3"}>
|
<div className={"flex w-full h-14 items-center gap-2 px-3"}>
|
||||||
<label className={"btn btn-square btn-ghost lg:hidden"} htmlFor="my-drawer-2">
|
<label className={"btn btn-square btn-ghost lg:hidden"} htmlFor="my-drawer-2">
|
||||||
<MdMenu size={24}/>
|
<MdMenu size={24} />
|
||||||
</label>
|
</label>
|
||||||
<h1 className={"text-xl font-bold"}>
|
<h1 className={"text-xl font-bold"}>
|
||||||
{pageNames[page]}
|
{pageNames[page]}
|
||||||
@@ -80,10 +84,10 @@ export default function ManagePage() {
|
|||||||
<h2 className={"text-lg font-bold p-4"}>
|
<h2 className={"text-lg font-bold p-4"}>
|
||||||
{t("Manage")}
|
{t("Manage")}
|
||||||
</h2>
|
</h2>
|
||||||
{buildItem(t("My Info"), <MdOutlineBadge className={"text-xl"}/>, 0)}
|
{buildItem(t("My Info"), <MdOutlineBadge className={"text-xl"} />, 0)}
|
||||||
{buildItem(t("Storage"), <MdOutlineStorage className={"text-xl"}/>, 1)}
|
{buildItem(t("Storage"), <MdOutlineStorage className={"text-xl"} />, 1)}
|
||||||
{buildItem(t("Users"), <MdOutlinePerson className={"text-xl"}/>, 2)}
|
{buildItem(t("Users"), <MdOutlinePerson className={"text-xl"} />, 2)}
|
||||||
{buildItem(t("Server"), <MdOutlineStorage className={"text-xl"}/>, 3)}
|
{buildItem(t("Server"), <MdOutlineStorage className={"text-xl"} />, 3)}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -62,7 +62,7 @@ export default function ManageServerConfigPage() {
|
|||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
return <form className="px-4" onSubmit={handleSubmit}>
|
return <form className="px-4 pb-4" onSubmit={handleSubmit}>
|
||||||
<Input type="number" value={config.max_uploading_size_in_mb.toString()} label="Max uploading size (MB)" onChange={(e) => {
|
<Input type="number" value={config.max_uploading_size_in_mb.toString()} label="Max uploading size (MB)" onChange={(e) => {
|
||||||
setConfig({...config, max_uploading_size_in_mb: parseInt(e.target.value) })
|
setConfig({...config, max_uploading_size_in_mb: parseInt(e.target.value) })
|
||||||
}}></Input>
|
}}></Input>
|
||||||
@@ -78,6 +78,12 @@ export default function ManageServerConfigPage() {
|
|||||||
setConfig({ ...config, allow_register: e.target.checked })
|
setConfig({ ...config, allow_register: e.target.checked })
|
||||||
}} />
|
}} />
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
<Input type="text" value={config.server_name} label="Server name" onChange={(e) => {
|
||||||
|
setConfig({...config, server_name: e.target.value })
|
||||||
|
}}></Input>
|
||||||
|
<Input type="text" value={config.server_description} label="Server description" onChange={(e) => {
|
||||||
|
setConfig({...config, server_description: e.target.value })
|
||||||
|
}}></Input>
|
||||||
<Input type="text" value={config.cloudflare_turnstile_site_key} label="Cloudflare Turnstile Site Key" onChange={(e) => {
|
<Input type="text" value={config.cloudflare_turnstile_site_key} label="Cloudflare Turnstile Site Key" onChange={(e) => {
|
||||||
setConfig({...config, cloudflare_turnstile_site_key: e.target.value })
|
setConfig({...config, cloudflare_turnstile_site_key: e.target.value })
|
||||||
}}></Input>
|
}}></Input>
|
||||||
|
@@ -1,13 +1,13 @@
|
|||||||
import {useRef, useState} from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import {MdAdd, MdDelete, MdOutlineInfo} from "react-icons/md";
|
import { MdAdd, MdDelete, MdOutlineInfo } from "react-icons/md";
|
||||||
import {Tag} from "../network/models.ts";
|
import { Tag } from "../network/models.ts";
|
||||||
import {network} from "../network/network.ts";
|
import { network } from "../network/network.ts";
|
||||||
import {LuInfo} from "react-icons/lu";
|
import { LuInfo } from "react-icons/lu";
|
||||||
import {useNavigate} from "react-router";
|
import { useNavigate } from "react-router";
|
||||||
import showToast from "../components/toast.ts";
|
import showToast from "../components/toast.ts";
|
||||||
import {useTranslation} from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import {app} from "../app.ts";
|
import { app } from "../app.ts";
|
||||||
import {ErrorAlert} from "../components/alert.tsx";
|
import { ErrorAlert } from "../components/alert.tsx";
|
||||||
|
|
||||||
export default function PublishPage() {
|
export default function PublishPage() {
|
||||||
const [title, setTitle] = useState<string>("")
|
const [title, setTitle] = useState<string>("")
|
||||||
@@ -22,6 +22,10 @@ export default function PublishPage() {
|
|||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
document.title = t("Publish Resource");
|
||||||
|
}, [])
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
if (isSubmitting) {
|
if (isSubmitting) {
|
||||||
return
|
return
|
||||||
@@ -53,7 +57,7 @@ export default function PublishPage() {
|
|||||||
})
|
})
|
||||||
if (res.success) {
|
if (res.success) {
|
||||||
setSubmitting(false)
|
setSubmitting(false)
|
||||||
navigate("/resources/" + res.data!, {replace: true})
|
navigate("/resources/" + res.data!, { replace: true })
|
||||||
} else {
|
} else {
|
||||||
setSubmitting(false)
|
setSubmitting(false)
|
||||||
setError(res.message)
|
setError(res.message)
|
||||||
@@ -80,28 +84,28 @@ export default function PublishPage() {
|
|||||||
setImages([...images, res.data!])
|
setImages([...images, res.data!])
|
||||||
} else {
|
} else {
|
||||||
setUploading(false)
|
setUploading(false)
|
||||||
showToast({message: t("Failed to upload image"), type: "error"})
|
showToast({ message: t("Failed to upload image"), type: "error" })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
input.click()
|
input.click()
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!app.user) {
|
if (!app.user) {
|
||||||
return <ErrorAlert className={"m-4"} message={t("You are not logged in. Please log in to access this page.")}/>
|
return <ErrorAlert className={"m-4"} message={t("You are not logged in. Please log in to access this page.")} />
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!app.user?.is_admin) {
|
if (!app.user?.is_admin) {
|
||||||
return <ErrorAlert className={"m-4"} message={t("You are not authorized to access this page.")}/>
|
return <ErrorAlert className={"m-4"} message={t("You are not authorized to access this page.")} />
|
||||||
}
|
}
|
||||||
|
|
||||||
return <div className={"p-4"}>
|
return <div className={"p-4"}>
|
||||||
<h1 className={"text-2xl font-bold my-4"}>{t("Publish Resource")}</h1>
|
<h1 className={"text-2xl font-bold my-4"}>{t("Publish Resource")}</h1>
|
||||||
<div role="alert" className="alert alert-info mb-2 alert-dash">
|
<div role="alert" className="alert alert-info mb-2 alert-dash">
|
||||||
<MdOutlineInfo size={24}/>
|
<MdOutlineInfo size={24} />
|
||||||
<span>{t("All information, images, and files can be modified after publishing")}</span>
|
<span>{t("All information, images, and files can be modified after publishing")}</span>
|
||||||
</div>
|
</div>
|
||||||
<p className={"my-1"}>{t("Title")}</p>
|
<p className={"my-1"}>{t("Title")}</p>
|
||||||
<input type="text" className="input w-full" value={title} onChange={(e) => setTitle(e.target.value)}/>
|
<input type="text" className="input w-full" value={title} onChange={(e) => setTitle(e.target.value)} />
|
||||||
<div className={"h-4"}></div>
|
<div className={"h-4"}></div>
|
||||||
<p className={"my-1"}>{t("Alternative Titles")}</p>
|
<p className={"my-1"}>{t("Alternative Titles")}</p>
|
||||||
{
|
{
|
||||||
@@ -111,13 +115,13 @@ export default function PublishPage() {
|
|||||||
const newAltTitles = [...altTitles]
|
const newAltTitles = [...altTitles]
|
||||||
newAltTitles[index] = e.target.value
|
newAltTitles[index] = e.target.value
|
||||||
setAltTitles(newAltTitles)
|
setAltTitles(newAltTitles)
|
||||||
}}/>
|
}} />
|
||||||
<button className={"btn btn-square btn-error ml-2"} type={"button"} onClick={() => {
|
<button className={"btn btn-square btn-error ml-2"} type={"button"} onClick={() => {
|
||||||
const newAltTitles = [...altTitles]
|
const newAltTitles = [...altTitles]
|
||||||
newAltTitles.splice(index, 1)
|
newAltTitles.splice(index, 1)
|
||||||
setAltTitles(newAltTitles)
|
setAltTitles(newAltTitles)
|
||||||
}}>
|
}}>
|
||||||
<MdDelete size={24}/>
|
<MdDelete size={24} />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
})
|
})
|
||||||
@@ -125,7 +129,7 @@ export default function PublishPage() {
|
|||||||
<button className={"btn my-2"} type={"button"} onClick={() => {
|
<button className={"btn my-2"} type={"button"} onClick={() => {
|
||||||
setAltTitles([...altTitles, ""])
|
setAltTitles([...altTitles, ""])
|
||||||
}}>
|
}}>
|
||||||
<MdAdd/>
|
<MdAdd />
|
||||||
{t("Add Alternative Title")}
|
{t("Add Alternative Title")}
|
||||||
</button>
|
</button>
|
||||||
<div className={"h-2"}></div>
|
<div className={"h-2"}></div>
|
||||||
@@ -139,18 +143,18 @@ export default function PublishPage() {
|
|||||||
</p>
|
</p>
|
||||||
<TagInput onAdd={(tag) => {
|
<TagInput onAdd={(tag) => {
|
||||||
setTags([...tags, tag])
|
setTags([...tags, tag])
|
||||||
}}/>
|
}} />
|
||||||
<div className={"h-4"}></div>
|
<div className={"h-4"}></div>
|
||||||
<p className={"my-1"}>{t("Description")}</p>
|
<p className={"my-1"}>{t("Description")}</p>
|
||||||
<textarea className="textarea w-full min-h-80 p-4" value={article} onChange={(e) => setArticle(e.target.value)}/>
|
<textarea className="textarea w-full min-h-80 p-4" value={article} onChange={(e) => setArticle(e.target.value)} />
|
||||||
<div className={"flex items-center py-1 "}>
|
<div className={"flex items-center py-1 "}>
|
||||||
<MdOutlineInfo className={"inline mr-1"}/>
|
<MdOutlineInfo className={"inline mr-1"} />
|
||||||
<span className={"text-sm"}>{t("Use Markdown format")}</span>
|
<span className={"text-sm"}>{t("Use Markdown format")}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className={"h-4"}></div>
|
<div className={"h-4"}></div>
|
||||||
<p className={"my-1"}>{t("Images")}</p>
|
<p className={"my-1"}>{t("Images")}</p>
|
||||||
<div role="alert" className="alert alert-info alert-soft my-2">
|
<div role="alert" className="alert alert-info alert-soft my-2">
|
||||||
<MdOutlineInfo size={24}/>
|
<MdOutlineInfo size={24} />
|
||||||
<span>{t("Images will not be displayed automatically, you need to reference them in the description")}</span>
|
<span>{t("Images will not be displayed automatically, you need to reference them in the description")}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className={`rounded-box border border-base-content/5 bg-base-100 ${images.length === 0 ? "hidden" : ""}`}>
|
<div className={`rounded-box border border-base-content/5 bg-base-100 ${images.length === 0 ? "hidden" : ""}`}>
|
||||||
@@ -167,7 +171,7 @@ export default function PublishPage() {
|
|||||||
images.map((image, index) => {
|
images.map((image, index) => {
|
||||||
return <tr key={index} className={"hover"}>
|
return <tr key={index} className={"hover"}>
|
||||||
<td>
|
<td>
|
||||||
<img src={network.getImageUrl(image)} className={"w-16 h-16 object-cover card"} alt={"image"}/>
|
<img src={network.getImageUrl(image)} className={"w-16 h-16 object-cover card"} alt={"image"} />
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
{network.getImageUrl(image)}
|
{network.getImageUrl(image)}
|
||||||
@@ -180,7 +184,7 @@ export default function PublishPage() {
|
|||||||
setImages(newImages)
|
setImages(newImages)
|
||||||
network.deleteImage(id)
|
network.deleteImage(id)
|
||||||
}}>
|
}}>
|
||||||
<MdDelete size={24}/>
|
<MdDelete size={24} />
|
||||||
</button>
|
</button>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -190,7 +194,7 @@ export default function PublishPage() {
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
<button className={"btn my-2"} type={"button"} onClick={addImage}>
|
<button className={"btn my-2"} type={"button"} onClick={addImage}>
|
||||||
{isUploading ? <span className="loading loading-spinner"></span> : <MdAdd/>}
|
{isUploading ? <span className="loading loading-spinner"></span> : <MdAdd />}
|
||||||
{t("Upload Image")}
|
{t("Upload Image")}
|
||||||
</button>
|
</button>
|
||||||
<div className={"h-4"}></div>
|
<div className={"h-4"}></div>
|
||||||
@@ -199,7 +203,7 @@ export default function PublishPage() {
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" className="h-6 w-6 shrink-0 stroke-current" fill="none"
|
<svg xmlns="http://www.w3.org/2000/svg" className="h-6 w-6 shrink-0 stroke-current" fill="none"
|
||||||
viewBox="0 0 24 24">
|
viewBox="0 0 24 24">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||||
d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||||
</svg>
|
</svg>
|
||||||
<span>{t("Error")}: {error}</span>
|
<span>{t("Error")}: {error}</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -213,7 +217,7 @@ export default function PublishPage() {
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
function TagInput({onAdd}: { onAdd: (tag: Tag) => void }) {
|
function TagInput({ onAdd }: { onAdd: (tag: Tag) => void }) {
|
||||||
const [keyword, setKeyword] = useState<string>("")
|
const [keyword, setKeyword] = useState<string>("")
|
||||||
const [tags, setTags] = useState<Tag[]>([])
|
const [tags, setTags] = useState<Tag[]>([])
|
||||||
const [error, setError] = useState<string | null>(null)
|
const [error, setError] = useState<string | null>(null)
|
||||||
@@ -221,7 +225,7 @@ function TagInput({onAdd}: { onAdd: (tag: Tag) => void }) {
|
|||||||
|
|
||||||
const debounce = useRef(new Debounce(500))
|
const debounce = useRef(new Debounce(500))
|
||||||
|
|
||||||
const {t} = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const searchTags = async (keyword: string) => {
|
const searchTags = async (keyword: string) => {
|
||||||
if (keyword.length === 0) {
|
if (keyword.length === 0) {
|
||||||
@@ -275,20 +279,20 @@ function TagInput({onAdd}: { onAdd: (tag: Tag) => void }) {
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" className="h-6 w-6 shrink-0 stroke-current" fill="none"
|
<svg xmlns="http://www.w3.org/2000/svg" className="h-6 w-6 shrink-0 stroke-current" fill="none"
|
||||||
viewBox="0 0 24 24">
|
viewBox="0 0 24 24">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||||
d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||||
</svg>
|
</svg>
|
||||||
<span>{error}</span>
|
<span>{error}</span>
|
||||||
</div>
|
</div>
|
||||||
} else if (!keyword) {
|
} else if (!keyword) {
|
||||||
dropdownContent = <div className="flex flex-row py-2 px-4">
|
dropdownContent = <div className="flex flex-row py-2 px-4">
|
||||||
<LuInfo size={20}/>
|
<LuInfo size={20} />
|
||||||
<span className={"w-2"}/>
|
<span className={"w-2"} />
|
||||||
<span className={"flex-1"}>{t("Please enter a search keyword")}</span>
|
<span className={"flex-1"}>{t("Please enter a search keyword")}</span>
|
||||||
</div>
|
</div>
|
||||||
} else if(isLoading) {
|
} else if (isLoading) {
|
||||||
dropdownContent = <div className="flex flex-row py-2 px-4">
|
dropdownContent = <div className="flex flex-row py-2 px-4">
|
||||||
<span className={"loading loading-spinner loading-sm"}></span>
|
<span className={"loading loading-spinner loading-sm"}></span>
|
||||||
<span className={"w-2"}/>
|
<span className={"w-2"} />
|
||||||
<span className={"flex-1"}>{t("Searching...")}</span>
|
<span className={"flex-1"}>{t("Searching...")}</span>
|
||||||
</div>
|
</div>
|
||||||
} else {
|
} else {
|
||||||
@@ -327,7 +331,7 @@ function TagInput({onAdd}: { onAdd: (tag: Tag) => void }) {
|
|||||||
<path d="m21 21-4.3-4.3"></path>
|
<path d="m21 21-4.3-4.3"></path>
|
||||||
</g>
|
</g>
|
||||||
</svg>
|
</svg>
|
||||||
<input autoComplete={"off"} id={"search_tags_input"} tabIndex={0} type="text" className="grow" placeholder={t("Search Tags")} value={keyword} onChange={(e) => handleChange(e.target.value)}/>
|
<input autoComplete={"off"} id={"search_tags_input"} tabIndex={0} type="text" className="grow" placeholder={t("Search Tags")} value={keyword} onChange={(e) => handleChange(e.target.value)} />
|
||||||
</label>
|
</label>
|
||||||
<ul tabIndex={0} className="dropdown-content menu bg-base-100 rounded-box z-1 w-52 p-2 shadow mt-2 border border-base-300">
|
<ul tabIndex={0} className="dropdown-content menu bg-base-100 rounded-box z-1 w-52 p-2 shadow mt-2 border border-base-300">
|
||||||
{dropdownContent}
|
{dropdownContent}
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import {FormEvent, useState} from "react";
|
import {FormEvent, useEffect, useState} from "react";
|
||||||
import {network} from "../network/network.ts";
|
import {network} from "../network/network.ts";
|
||||||
import {app} from "../app.ts";
|
import {app} from "../app.ts";
|
||||||
import {useNavigate} from "react-router";
|
import {useNavigate} from "react-router";
|
||||||
@@ -37,6 +37,10 @@ export default function RegisterPage() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
document.title = t("Register");
|
||||||
|
}, [])
|
||||||
|
|
||||||
return <div className={"flex items-center justify-center w-full h-full bg-base-200"} id={"register-page"}>
|
return <div className={"flex items-center justify-center w-full h-full bg-base-200"} id={"register-page"}>
|
||||||
<div className={"w-96 card card-border bg-base-100 border-base-300"}>
|
<div className={"w-96 card card-border bg-base-100 border-base-300"}>
|
||||||
<form onSubmit={onSubmit}>
|
<form onSubmit={onSubmit}>
|
||||||
|
@@ -1,21 +1,21 @@
|
|||||||
import {useNavigate, useParams} from "react-router";
|
import { useNavigate, useParams } from "react-router";
|
||||||
import {createContext, useCallback, useContext, useEffect, useRef, useState} from "react";
|
import { createContext, useCallback, useContext, useEffect, useRef, useState } from "react";
|
||||||
import {ResourceDetails, RFile, Storage, Comment} from "../network/models.ts";
|
import { ResourceDetails, RFile, Storage, Comment } from "../network/models.ts";
|
||||||
import {network} from "../network/network.ts";
|
import { network } from "../network/network.ts";
|
||||||
import showToast from "../components/toast.ts";
|
import showToast from "../components/toast.ts";
|
||||||
import Markdown from "react-markdown";
|
import Markdown from "react-markdown";
|
||||||
import "../markdown.css";
|
import "../markdown.css";
|
||||||
import Loading from "../components/loading.tsx";
|
import Loading from "../components/loading.tsx";
|
||||||
import {MdAdd, MdOutlineArticle, MdOutlineComment, MdOutlineDataset, MdOutlineDownload} from "react-icons/md";
|
import { MdAdd, MdOutlineArticle, MdOutlineComment, MdOutlineDataset, MdOutlineDownload } from "react-icons/md";
|
||||||
import {app} from "../app.ts";
|
import { app } from "../app.ts";
|
||||||
import {uploadingManager} from "../network/uploading.ts";
|
import { uploadingManager } from "../network/uploading.ts";
|
||||||
import {ErrorAlert} from "../components/alert.tsx";
|
import { ErrorAlert } from "../components/alert.tsx";
|
||||||
import {useTranslation} from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import Pagination from "../components/pagination.tsx";
|
import Pagination from "../components/pagination.tsx";
|
||||||
|
|
||||||
export default function ResourcePage() {
|
export default function ResourcePage() {
|
||||||
const params = useParams()
|
const params = useParams()
|
||||||
const {t} = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const idStr = params.id
|
const idStr = params.id
|
||||||
|
|
||||||
@@ -32,18 +32,23 @@ export default function ResourcePage() {
|
|||||||
if (res.success) {
|
if (res.success) {
|
||||||
setResource(res.data!)
|
setResource(res.data!)
|
||||||
} else {
|
} else {
|
||||||
showToast({message: res.message, type: "error"})
|
showToast({ message: res.message, type: "error" })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [id])
|
}, [id])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
document.title = t("Resource Details");
|
||||||
|
}, [])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isNaN(id)) {
|
if (!isNaN(id)) {
|
||||||
network.getResourceDetails(id).then((res) => {
|
network.getResourceDetails(id).then((res) => {
|
||||||
if (res.success) {
|
if (res.success) {
|
||||||
setResource(res.data!)
|
setResource(res.data!)
|
||||||
|
document.title = res.data!.title
|
||||||
} else {
|
} else {
|
||||||
showToast({message: res.message, type: "error"})
|
showToast({ message: res.message, type: "error" })
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -60,7 +65,7 @@ export default function ResourcePage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!resource) {
|
if (!resource) {
|
||||||
return <Loading/>
|
return <Loading />
|
||||||
}
|
}
|
||||||
|
|
||||||
return <context.Provider value={reload}>
|
return <context.Provider value={reload}>
|
||||||
@@ -79,7 +84,7 @@ export default function ResourcePage() {
|
|||||||
<div className="flex items-center ">
|
<div className="flex items-center ">
|
||||||
<div className="avatar">
|
<div className="avatar">
|
||||||
<div className="w-6 rounded-full">
|
<div className="w-6 rounded-full">
|
||||||
<img src={network.getUserAvatar(resource.author)} alt={"avatar"}/>
|
<img src={network.getUserAvatar(resource.author)} alt={"avatar"} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-2"></div>
|
<div className="w-2"></div>
|
||||||
@@ -99,40 +104,40 @@ export default function ResourcePage() {
|
|||||||
<label className="tab transition-all">
|
<label className="tab transition-all">
|
||||||
<input type="radio" name="my_tabs" checked={page === 0} onChange={() => {
|
<input type="radio" name="my_tabs" checked={page === 0} onChange={() => {
|
||||||
setPage(0)
|
setPage(0)
|
||||||
}}/>
|
}} />
|
||||||
<MdOutlineArticle className="text-xl mr-2"/>
|
<MdOutlineArticle className="text-xl mr-2" />
|
||||||
<span className="text-sm">
|
<span className="text-sm">
|
||||||
{t("Description")}
|
{t("Description")}
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
<div key={"article"} className="tab-content p-2">
|
<div key={"article"} className="tab-content p-2">
|
||||||
<Article article={resource.article}/>
|
<Article article={resource.article} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<label className="tab transition-all">
|
<label className="tab transition-all">
|
||||||
<input type="radio" name="my_tabs" checked={page === 1} onChange={() => {
|
<input type="radio" name="my_tabs" checked={page === 1} onChange={() => {
|
||||||
setPage(1)
|
setPage(1)
|
||||||
}}/>
|
}} />
|
||||||
<MdOutlineDataset className="text-xl mr-2"/>
|
<MdOutlineDataset className="text-xl mr-2" />
|
||||||
<span className="text-sm">
|
<span className="text-sm">
|
||||||
{t("Files")}
|
{t("Files")}
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
<div key={"files"} className="tab-content p-2">
|
<div key={"files"} className="tab-content p-2">
|
||||||
<Files files={resource.files} resourceID={resource.id}/>
|
<Files files={resource.files} resourceID={resource.id} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<label className="tab transition-all">
|
<label className="tab transition-all">
|
||||||
<input type="radio" name="my_tabs" checked={page === 2} onChange={() => {
|
<input type="radio" name="my_tabs" checked={page === 2} onChange={() => {
|
||||||
setPage(2)
|
setPage(2)
|
||||||
}}/>
|
}} />
|
||||||
<MdOutlineComment className="text-xl mr-2"/>
|
<MdOutlineComment className="text-xl mr-2" />
|
||||||
<span className="text-sm">
|
<span className="text-sm">
|
||||||
{t("Comments")}
|
{t("Comments")}
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
<div key={"comments"} className="tab-content p-2">
|
<div key={"comments"} className="tab-content p-2">
|
||||||
<Comments resourceId={resource.id}/>
|
<Comments resourceId={resource.id} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="h-4"></div>
|
<div className="h-4"></div>
|
||||||
@@ -143,13 +148,13 @@ export default function ResourcePage() {
|
|||||||
const context = createContext<() => void>(() => {
|
const context = createContext<() => void>(() => {
|
||||||
})
|
})
|
||||||
|
|
||||||
function Article({article}: { article: string }) {
|
function Article({ article }: { article: string }) {
|
||||||
return <article>
|
return <article>
|
||||||
<Markdown>{article}</Markdown>
|
<Markdown>{article}</Markdown>
|
||||||
</article>
|
</article>
|
||||||
}
|
}
|
||||||
|
|
||||||
function FileTile({file}: { file: RFile }) {
|
function FileTile({ file }: { file: RFile }) {
|
||||||
return <div className={"card card-border border-base-300 my-2"}>
|
return <div className={"card card-border border-base-300 my-2"}>
|
||||||
<div className={"p-4 flex flex-row items-center"}>
|
<div className={"p-4 flex flex-row items-center"}>
|
||||||
<div className={"grow"}>
|
<div className={"grow"}>
|
||||||
@@ -161,14 +166,14 @@ function FileTile({file}: { file: RFile }) {
|
|||||||
const link = network.getFileDownloadLink(file.id);
|
const link = network.getFileDownloadLink(file.id);
|
||||||
window.open(link, "_blank");
|
window.open(link, "_blank");
|
||||||
}}>
|
}}>
|
||||||
<MdOutlineDownload size={24}/>
|
<MdOutlineDownload size={24} />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
function Files({files, resourceID}: { files: RFile[], resourceID: number }) {
|
function Files({ files, resourceID }: { files: RFile[], resourceID: number }) {
|
||||||
return <div>
|
return <div>
|
||||||
{
|
{
|
||||||
files.map((file) => {
|
files.map((file) => {
|
||||||
@@ -189,8 +194,8 @@ enum FileType {
|
|||||||
upload = "upload",
|
upload = "upload",
|
||||||
}
|
}
|
||||||
|
|
||||||
function CreateFileDialog({resourceId}: { resourceId: number }) {
|
function CreateFileDialog({ resourceId }: { resourceId: number }) {
|
||||||
const {t} = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [isLoading, setLoading] = useState(false)
|
const [isLoading, setLoading] = useState(false)
|
||||||
const storages = useRef<Storage[] | null>(null)
|
const storages = useRef<Storage[] | null>(null)
|
||||||
const mounted = useRef(true)
|
const mounted = useRef(true)
|
||||||
@@ -236,7 +241,7 @@ function CreateFileDialog({resourceId}: { resourceId: number }) {
|
|||||||
setSubmitting(false)
|
setSubmitting(false)
|
||||||
const dialog = document.getElementById("upload_dialog") as HTMLDialogElement
|
const dialog = document.getElementById("upload_dialog") as HTMLDialogElement
|
||||||
dialog.close()
|
dialog.close()
|
||||||
showToast({message: t("File created successfully"), type: "success"})
|
showToast({ message: t("File created successfully"), type: "success" })
|
||||||
reload()
|
reload()
|
||||||
} else {
|
} else {
|
||||||
setError(res.message)
|
setError(res.message)
|
||||||
@@ -257,7 +262,7 @@ function CreateFileDialog({resourceId}: { resourceId: number }) {
|
|||||||
setSubmitting(false)
|
setSubmitting(false)
|
||||||
const dialog = document.getElementById("upload_dialog") as HTMLDialogElement
|
const dialog = document.getElementById("upload_dialog") as HTMLDialogElement
|
||||||
dialog.close()
|
dialog.close()
|
||||||
showToast({message: t("Successfully create uploading task."), type: "success"})
|
showToast({ message: t("Successfully create uploading task."), type: "success" })
|
||||||
} else {
|
} else {
|
||||||
setError(res.message)
|
setError(res.message)
|
||||||
setSubmitting(false)
|
setSubmitting(false)
|
||||||
@@ -277,7 +282,7 @@ function CreateFileDialog({resourceId}: { resourceId: number }) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!res.success) {
|
if (!res.success) {
|
||||||
showToast({message: res.message, type: "error"})
|
showToast({ message: res.message, type: "error" })
|
||||||
} else {
|
} else {
|
||||||
storages.current = res.data!
|
storages.current = res.data!
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
@@ -291,7 +296,7 @@ function CreateFileDialog({resourceId}: { resourceId: number }) {
|
|||||||
dialog.showModal()
|
dialog.showModal()
|
||||||
}}>
|
}}>
|
||||||
{
|
{
|
||||||
isLoading ? <span className={"loading loading-spinner loading-sm"}></span> : <MdAdd size={24}/>
|
isLoading ? <span className={"loading loading-spinner loading-sm"}></span> : <MdAdd size={24} />
|
||||||
}
|
}
|
||||||
<span className={"text-sm"}>
|
<span className={"text-sm"}>
|
||||||
{t("Upload")}
|
{t("Upload")}
|
||||||
@@ -305,13 +310,13 @@ function CreateFileDialog({resourceId}: { resourceId: number }) {
|
|||||||
<form className="filter mb-2">
|
<form className="filter mb-2">
|
||||||
<input className="btn btn-square" type="reset" value="×" onClick={() => {
|
<input className="btn btn-square" type="reset" value="×" onClick={() => {
|
||||||
setFileType(null);
|
setFileType(null);
|
||||||
}}/>
|
}} />
|
||||||
<input className="btn text-sm" type="radio" name="type" aria-label={t("Redirect")} onInput={() => {
|
<input className="btn text-sm" type="radio" name="type" aria-label={t("Redirect")} onInput={() => {
|
||||||
setFileType(FileType.redirect);
|
setFileType(FileType.redirect);
|
||||||
}}/>
|
}} />
|
||||||
<input className="btn text-sm" type="radio" name="type" aria-label={t("Upload")} onInput={() => {
|
<input className="btn text-sm" type="radio" name="type" aria-label={t("Upload")} onInput={() => {
|
||||||
setFileType(FileType.upload);
|
setFileType(FileType.upload);
|
||||||
}}/>
|
}} />
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
{
|
{
|
||||||
@@ -319,13 +324,13 @@ function CreateFileDialog({resourceId}: { resourceId: number }) {
|
|||||||
<p className={"text-sm p-2"}>{t("User who click the file will be redirected to the URL")}</p>
|
<p className={"text-sm p-2"}>{t("User who click the file will be redirected to the URL")}</p>
|
||||||
<input type="text" className="input w-full my-2" placeholder={t("File Name")} onChange={(e) => {
|
<input type="text" className="input w-full my-2" placeholder={t("File Name")} onChange={(e) => {
|
||||||
setFilename(e.target.value)
|
setFilename(e.target.value)
|
||||||
}}/>
|
}} />
|
||||||
<input type="text" className="input w-full my-2" placeholder={t("URL")} onChange={(e) => {
|
<input type="text" className="input w-full my-2" placeholder={t("URL")} onChange={(e) => {
|
||||||
setRedirectUrl(e.target.value)
|
setRedirectUrl(e.target.value)
|
||||||
}}/>
|
}} />
|
||||||
<input type="text" className="input w-full my-2" placeholder={t("Description")} onChange={(e) => {
|
<input type="text" className="input w-full my-2" placeholder={t("Description")} onChange={(e) => {
|
||||||
setDescription(e.target.value)
|
setDescription(e.target.value)
|
||||||
}}/>
|
}} />
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -358,15 +363,15 @@ function CreateFileDialog({resourceId}: { resourceId: number }) {
|
|||||||
if (e.target.files) {
|
if (e.target.files) {
|
||||||
setFile(e.target.files[0])
|
setFile(e.target.files[0])
|
||||||
}
|
}
|
||||||
}}/>
|
}} />
|
||||||
|
|
||||||
<input type="text" className="input w-full my-2" placeholder={t("Description")} onChange={(e) => {
|
<input type="text" className="input w-full my-2" placeholder={t("Description")} onChange={(e) => {
|
||||||
setDescription(e.target.value)
|
setDescription(e.target.value)
|
||||||
}}/>
|
}} />
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
|
|
||||||
{error && <ErrorAlert className={"my-2"} message={error}/>}
|
{error && <ErrorAlert className={"my-2"} message={error} />}
|
||||||
|
|
||||||
<div className="modal-action">
|
<div className="modal-action">
|
||||||
<form method="dialog">
|
<form method="dialog">
|
||||||
@@ -382,7 +387,7 @@ function CreateFileDialog({resourceId}: { resourceId: number }) {
|
|||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
|
|
||||||
function Comments({resourceId}: { resourceId: number }) {
|
function Comments({ resourceId }: { resourceId: number }) {
|
||||||
const [page, setPage] = useState(1);
|
const [page, setPage] = useState(1);
|
||||||
|
|
||||||
const [maxPage, setMaxPage] = useState(0);
|
const [maxPage, setMaxPage] = useState(0);
|
||||||
@@ -404,17 +409,17 @@ function Comments({resourceId}: { resourceId: number }) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (commentContent === "") {
|
if (commentContent === "") {
|
||||||
showToast({message: "Comment content cannot be empty", type: "error"});
|
showToast({ message: "Comment content cannot be empty", type: "error" });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const res = await network.createComment(resourceId, commentContent);
|
const res = await network.createComment(resourceId, commentContent);
|
||||||
if (res.success) {
|
if (res.success) {
|
||||||
setCommentContent("");
|
setCommentContent("");
|
||||||
showToast({message: "Comment created successfully", type: "success"});
|
showToast({ message: "Comment created successfully", type: "success" });
|
||||||
reload();
|
reload();
|
||||||
} else {
|
} else {
|
||||||
showToast({message: res.message, type: "error"});
|
showToast({ message: res.message, type: "error" });
|
||||||
}
|
}
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
@@ -422,7 +427,7 @@ function Comments({resourceId}: { resourceId: number }) {
|
|||||||
return <div>
|
return <div>
|
||||||
<div className={"mt-4 mb-6 textarea w-full p-4 h-28 flex flex-col"}>
|
<div className={"mt-4 mb-6 textarea w-full p-4 h-28 flex flex-col"}>
|
||||||
<textarea placeholder={"Write down your comment"} className={"w-full resize-none grow"} value={commentContent}
|
<textarea placeholder={"Write down your comment"} className={"w-full resize-none grow"} value={commentContent}
|
||||||
onChange={(e) => setCommentContent(e.target.value)}/>
|
onChange={(e) => setCommentContent(e.target.value)} />
|
||||||
<div className={"flex flex-row-reverse"}>
|
<div className={"flex flex-row-reverse"}>
|
||||||
<button onClick={sendComment}
|
<button onClick={sendComment}
|
||||||
className={`btn btn-primary h-8 text-sm mx-2 ${commentContent === "" && "btn-disabled"}`}>
|
className={`btn btn-primary h-8 text-sm mx-2 ${commentContent === "" && "btn-disabled"}`}>
|
||||||
@@ -431,14 +436,14 @@ function Comments({resourceId}: { resourceId: number }) {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<CommentsList resourceId={resourceId} page={page} maxPageCallback={setMaxPage} key={listKey}/>
|
<CommentsList resourceId={resourceId} page={page} maxPageCallback={setMaxPage} key={listKey} />
|
||||||
{maxPage && <div className={"w-full flex justify-center"}>
|
{maxPage && <div className={"w-full flex justify-center"}>
|
||||||
<Pagination page={page} setPage={setPage} totalPages={maxPage}/>
|
<Pagination page={page} setPage={setPage} totalPages={maxPage} />
|
||||||
</div>}
|
</div>}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
function CommentsList({resourceId, page, maxPageCallback}: {
|
function CommentsList({ resourceId, page, maxPageCallback }: {
|
||||||
resourceId: number,
|
resourceId: number,
|
||||||
page: number,
|
page: number,
|
||||||
maxPageCallback: (maxPage: number) => void
|
maxPageCallback: (maxPage: number) => void
|
||||||
@@ -461,25 +466,25 @@ function CommentsList({resourceId, page, maxPageCallback}: {
|
|||||||
|
|
||||||
if (comments == null) {
|
if (comments == null) {
|
||||||
return <div className={"w-full"}>
|
return <div className={"w-full"}>
|
||||||
<Loading/>
|
<Loading />
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
return <>
|
return <>
|
||||||
{
|
{
|
||||||
comments.map((comment) => {
|
comments.map((comment) => {
|
||||||
return <CommentTile comment={comment} key={comment.id}/>
|
return <CommentTile comment={comment} key={comment.id} />
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
|
|
||||||
function CommentTile({comment}: { comment: Comment }) {
|
function CommentTile({ comment }: { comment: Comment }) {
|
||||||
return <div className={"card card-border border-base-300 p-2 my-3"}>
|
return <div className={"card card-border border-base-300 p-2 my-3"}>
|
||||||
<div className={"flex flex-row items-center my-1 mx-1"}>
|
<div className={"flex flex-row items-center my-1 mx-1"}>
|
||||||
<div className="avatar">
|
<div className="avatar">
|
||||||
<div className="w-8 rounded-full">
|
<div className="w-8 rounded-full">
|
||||||
<img src={network.getUserAvatar(comment.user)} alt={"avatar"}/>
|
<img src={network.getUserAvatar(comment.user)} alt={"avatar"} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className={"w-2"}></div>
|
<div className={"w-2"}></div>
|
||||||
|
@@ -10,7 +10,9 @@ export default function SearchPage() {
|
|||||||
|
|
||||||
const keyword = params.get("keyword")
|
const keyword = params.get("keyword")
|
||||||
|
|
||||||
useEffect(() => {}, [])
|
useEffect(() => {
|
||||||
|
document.title = t("Search") + ": " + (keyword || "");
|
||||||
|
}, [])
|
||||||
|
|
||||||
if (keyword === null || keyword === "") {
|
if (keyword === null || keyword === "") {
|
||||||
return <div role="alert" className="alert alert-info alert-dash">
|
return <div role="alert" className="alert alert-info alert-dash">
|
||||||
|
@@ -1,17 +1,25 @@
|
|||||||
import {useParams} from "react-router";
|
import { useParams } from "react-router";
|
||||||
import {ErrorAlert} from "../components/alert.tsx";
|
import { ErrorAlert } from "../components/alert.tsx";
|
||||||
import ResourcesView from "../components/resources_view.tsx";
|
import ResourcesView from "../components/resources_view.tsx";
|
||||||
import {network} from "../network/network.ts";
|
import { network } from "../network/network.ts";
|
||||||
|
import { useEffect } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
export default function TaggedResourcesPage() {
|
export default function TaggedResourcesPage() {
|
||||||
const {tag} = useParams()
|
const { tag } = useParams()
|
||||||
|
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
if (!tag) {
|
if (!tag) {
|
||||||
return <div>
|
return <div>
|
||||||
<ErrorAlert message={"Tag not found"}/>
|
<ErrorAlert message={"Tag not found"} />
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
document.title = t("Tag: " + tag);
|
||||||
|
}, [tag])
|
||||||
|
|
||||||
return <div>
|
return <div>
|
||||||
<h1 className={"text-2xl pt-6 pb-2 px-4 font-bold"}>
|
<h1 className={"text-2xl pt-6 pb-2 px-4 font-bold"}>
|
||||||
Tag: {tag}
|
Tag: {tag}
|
||||||
|
@@ -28,6 +28,10 @@ export default function UserPage() {
|
|||||||
});
|
});
|
||||||
}, [username]);
|
}, [username]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
document.title = username || "User";
|
||||||
|
}, [username]);
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return <div className="w-full">
|
return <div className="w-full">
|
||||||
<Loading />
|
<Loading />
|
||||||
|
2
go.mod
2
go.mod
@@ -28,9 +28,11 @@ require (
|
|||||||
github.com/fxamacker/cbor/v2 v2.8.0 // indirect
|
github.com/fxamacker/cbor/v2 v2.8.0 // indirect
|
||||||
github.com/gofiber/schema v1.3.0 // indirect
|
github.com/gofiber/schema v1.3.0 // indirect
|
||||||
github.com/gofiber/utils/v2 v2.0.0-beta.8 // indirect
|
github.com/gofiber/utils/v2 v2.0.0-beta.8 // indirect
|
||||||
|
github.com/gomarkdown/markdown v0.0.0-20250311123330-531bef5e742b
|
||||||
github.com/google/uuid v1.6.0
|
github.com/google/uuid v1.6.0
|
||||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||||
github.com/jinzhu/now v1.1.5 // indirect
|
github.com/jinzhu/now v1.1.5 // indirect
|
||||||
|
github.com/k3a/html2text v1.2.1
|
||||||
github.com/klauspost/compress v1.18.0 // indirect
|
github.com/klauspost/compress v1.18.0 // indirect
|
||||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
|
13
go.sum
13
go.sum
@@ -20,12 +20,18 @@ github.com/gofiber/utils/v2 v2.0.0-beta.8 h1:ZifwbHZqZO3YJsx1ZhDsWnPjaQ7C0YD20LH
|
|||||||
github.com/gofiber/utils/v2 v2.0.0-beta.8/go.mod h1:1lCBo9vEF4RFEtTgWntipnaScJZQiM8rrsYycLZ4n9c=
|
github.com/gofiber/utils/v2 v2.0.0-beta.8/go.mod h1:1lCBo9vEF4RFEtTgWntipnaScJZQiM8rrsYycLZ4n9c=
|
||||||
github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8=
|
github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8=
|
||||||
github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||||
|
github.com/gomarkdown/markdown v0.0.0-20250311123330-531bef5e742b h1:EY/KpStFl60qA17CptGXhwfZ+k1sFNJIUNR8DdbcuUk=
|
||||||
|
github.com/gomarkdown/markdown v0.0.0-20250311123330-531bef5e742b/go.mod h1:JDGcbDT52eL4fju3sZ4TeHGsQwhG9nbDV21aMyhwPoA=
|
||||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
|
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||||
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
||||||
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||||
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
||||||
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||||
|
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
||||||
|
github.com/k3a/html2text v1.2.1 h1:nvnKgBvBR/myqrwfLuiqecUtaK1lB9hGziIJKatNFVY=
|
||||||
|
github.com/k3a/html2text v1.2.1/go.mod h1:ieEXykM67iT8lTvEWBh6fhpH4B23kB9OMKPdIBmgUqA=
|
||||||
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
|
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
|
||||||
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
|
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
|
||||||
github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||||
@@ -49,6 +55,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
|
|||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU=
|
github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU=
|
||||||
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
|
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
|
||||||
|
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
|
||||||
|
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
|
||||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
github.com/tinylib/msgp v1.2.5 h1:WeQg1whrXRFiZusidTQqzETkRpGjFjcIhW6uqWH09po=
|
github.com/tinylib/msgp v1.2.5 h1:WeQg1whrXRFiZusidTQqzETkRpGjFjcIhW6uqWH09po=
|
||||||
@@ -61,17 +69,22 @@ github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
|
|||||||
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
|
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
|
||||||
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
|
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
|
||||||
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
||||||
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE=
|
golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE=
|
||||||
golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc=
|
golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc=
|
||||||
golang.org/x/image v0.27.0 h1:C8gA4oWU/tKkdCfYT6T2u4faJu3MeNS5O8UPWlPF61w=
|
golang.org/x/image v0.27.0 h1:C8gA4oWU/tKkdCfYT6T2u4faJu3MeNS5O8UPWlPF61w=
|
||||||
golang.org/x/image v0.27.0/go.mod h1:xbdrClrAUway1MUTEZDq9mz/UpRwYAkFFNUslZtcB+g=
|
golang.org/x/image v0.27.0/go.mod h1:xbdrClrAUway1MUTEZDq9mz/UpRwYAkFFNUslZtcB+g=
|
||||||
|
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY=
|
golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY=
|
||||||
golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E=
|
golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E=
|
||||||
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
|
golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
|
||||||
golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||||
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
|
golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
|
||||||
golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
|
golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
|
||||||
|
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
gorm.io/driver/sqlite v1.5.7 h1:8NvsrhP0ifM7LX9G4zPB97NwovUakUxc+2V2uuf3Z1I=
|
gorm.io/driver/sqlite v1.5.7 h1:8NvsrhP0ifM7LX9G4zPB97NwovUakUxc+2V2uuf3Z1I=
|
||||||
|
2
main.go
2
main.go
@@ -28,6 +28,8 @@ func main() {
|
|||||||
|
|
||||||
app.Use(middleware.JwtMiddleware)
|
app.Use(middleware.JwtMiddleware)
|
||||||
|
|
||||||
|
app.Use(middleware.FrontendMiddleware)
|
||||||
|
|
||||||
if debugMode {
|
if debugMode {
|
||||||
app.Use(cors.New(cors.ConfigDefault))
|
app.Use(cors.New(cors.ConfigDefault))
|
||||||
}
|
}
|
||||||
|
@@ -22,6 +22,10 @@ type ServerConfig struct {
|
|||||||
CloudflareTurnstileSiteKey string `json:"cloudflare_turnstile_site_key"`
|
CloudflareTurnstileSiteKey string `json:"cloudflare_turnstile_site_key"`
|
||||||
// CloudflareTurnstileSecretKey is the secret key for Cloudflare Turnstile.
|
// CloudflareTurnstileSecretKey is the secret key for Cloudflare Turnstile.
|
||||||
CloudflareTurnstileSecretKey string `json:"cloudflare_turnstile_secret_key"`
|
CloudflareTurnstileSecretKey string `json:"cloudflare_turnstile_secret_key"`
|
||||||
|
|
||||||
|
ServerName string `json:"server_name"`
|
||||||
|
|
||||||
|
ServerDescription string `json:"server_description"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
@@ -34,6 +38,8 @@ func init() {
|
|||||||
AllowRegister: true,
|
AllowRegister: true,
|
||||||
CloudflareTurnstileSiteKey: "",
|
CloudflareTurnstileSiteKey: "",
|
||||||
CloudflareTurnstileSecretKey: "",
|
CloudflareTurnstileSecretKey: "",
|
||||||
|
ServerName: "Nysoure",
|
||||||
|
ServerDescription: "Nysoure is a file sharing service.",
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
data, err := os.ReadFile(filepath)
|
data, err := os.ReadFile(filepath)
|
||||||
@@ -78,3 +84,15 @@ func AllowRegister() bool {
|
|||||||
func MaxDownloadsPerDayForSingleIP() int {
|
func MaxDownloadsPerDayForSingleIP() int {
|
||||||
return config.MaxDownloadsPerDayForSingleIP
|
return config.MaxDownloadsPerDayForSingleIP
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func CloudflareTurnstileSiteKey() string {
|
||||||
|
return config.CloudflareTurnstileSiteKey
|
||||||
|
}
|
||||||
|
|
||||||
|
func ServerName() string {
|
||||||
|
return config.ServerName
|
||||||
|
}
|
||||||
|
|
||||||
|
func ServerDescription() string {
|
||||||
|
return config.ServerDescription
|
||||||
|
}
|
||||||
|
106
server/middleware/frontend_middleware.go
Normal file
106
server/middleware/frontend_middleware.go
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
package middleware
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"nysoure/server/config"
|
||||||
|
"nysoure/server/service"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/gofiber/fiber/v3"
|
||||||
|
"github.com/gomarkdown/markdown"
|
||||||
|
"github.com/gomarkdown/markdown/html"
|
||||||
|
"github.com/gomarkdown/markdown/parser"
|
||||||
|
"github.com/k3a/html2text"
|
||||||
|
)
|
||||||
|
|
||||||
|
func FrontendMiddleware(c fiber.Ctx) error {
|
||||||
|
if strings.HasPrefix(c.Path(), "/api") {
|
||||||
|
return c.Next()
|
||||||
|
}
|
||||||
|
|
||||||
|
path := c.Path()
|
||||||
|
file := "static" + path
|
||||||
|
|
||||||
|
if _, err := os.Stat(file); path == "/" || os.IsNotExist(err) {
|
||||||
|
return serveIndexHtml(c)
|
||||||
|
} else {
|
||||||
|
return c.SendFile(file)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func serveIndexHtml(c fiber.Ctx) error {
|
||||||
|
data, err := os.ReadFile("static/index.html")
|
||||||
|
if err != nil {
|
||||||
|
return c.Status(fiber.StatusInternalServerError).SendString("Internal Server Error")
|
||||||
|
}
|
||||||
|
content := string(data)
|
||||||
|
|
||||||
|
siteName := config.ServerName()
|
||||||
|
description := config.ServerDescription()
|
||||||
|
preview := "/icon-192.png"
|
||||||
|
title := siteName
|
||||||
|
url := c.OriginalURL()
|
||||||
|
cfTurnstileSiteKey := config.CloudflareTurnstileSiteKey()
|
||||||
|
|
||||||
|
if strings.HasPrefix(url, "/resources/") {
|
||||||
|
idStr := strings.TrimPrefix(url, "/resources/")
|
||||||
|
id, err := strconv.Atoi(idStr)
|
||||||
|
if err == nil {
|
||||||
|
r, err := service.GetResource(uint(id))
|
||||||
|
if err == nil {
|
||||||
|
if len(r.Images) > 0 {
|
||||||
|
preview = fmt.Sprintf("/images/%d", r.Images[0].ID)
|
||||||
|
}
|
||||||
|
title = r.Title
|
||||||
|
description = getResourceDescription(r.Article)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if strings.HasPrefix(url, "/user/") {
|
||||||
|
username := strings.TrimPrefix(url, "/user/")
|
||||||
|
u, err := service.GetUserByUsername(username)
|
||||||
|
if err == nil {
|
||||||
|
preview = fmt.Sprintf("/avatar/%d", u.ID)
|
||||||
|
title = u.Username
|
||||||
|
description = "User " + u.Username + "'s profile"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
content = strings.ReplaceAll(content, "{{SiteName}}", siteName)
|
||||||
|
content = strings.ReplaceAll(content, "{{Description}}", description)
|
||||||
|
content = strings.ReplaceAll(content, "{{Preview}}", preview)
|
||||||
|
content = strings.ReplaceAll(content, "{{Title}}", title)
|
||||||
|
content = strings.ReplaceAll(content, "{{Url}}", url)
|
||||||
|
content = strings.ReplaceAll(content, "{{CFTurnstileSiteKey}}", cfTurnstileSiteKey)
|
||||||
|
|
||||||
|
c.Set("Content-Type", "text/html; charset=utf-8")
|
||||||
|
return c.SendString(content)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getResourceDescription(article string) string {
|
||||||
|
htmlContent := mdToHTML([]byte(article))
|
||||||
|
plain := html2text.HTML2Text(string(htmlContent))
|
||||||
|
if len([]rune(plain)) > 100 {
|
||||||
|
plain = string([]rune(plain)[:100])
|
||||||
|
}
|
||||||
|
plain = strings.ReplaceAll(plain, "\n", " ")
|
||||||
|
plain = strings.ReplaceAll(plain, "\r", "")
|
||||||
|
plain = strings.ReplaceAll(plain, "\t", "")
|
||||||
|
plain = strings.TrimSpace(plain)
|
||||||
|
return plain
|
||||||
|
}
|
||||||
|
|
||||||
|
func mdToHTML(md []byte) []byte {
|
||||||
|
// create Markdown parser with extensions
|
||||||
|
extensions := parser.CommonExtensions | parser.NoEmptyLineBeforeBlock | parser.MathJax
|
||||||
|
p := parser.NewWithExtensions(extensions)
|
||||||
|
doc := p.Parse(md)
|
||||||
|
|
||||||
|
// create HTML renderer with extensions
|
||||||
|
htmlFlags := html.CommonFlags | html.HrefTargetBlank
|
||||||
|
opts := html.RendererOptions{Flags: htmlFlags}
|
||||||
|
renderer := html.NewRenderer(opts)
|
||||||
|
|
||||||
|
return markdown.Render(doc, renderer)
|
||||||
|
}
|
Reference in New Issue
Block a user