Add site information management with Markdown support

This commit is contained in:
2025-05-18 11:04:59 +08:00
parent 1b5eb23a65
commit be09b55765
9 changed files with 67 additions and 8 deletions

View File

@@ -35,6 +35,7 @@
<script> <script>
window.serverName = "{{SiteName}}"; window.serverName = "{{SiteName}}";
window.cloudflareTurnstileSiteKey = "{{CFTurnstileSiteKey}}"; window.cloudflareTurnstileSiteKey = "{{CFTurnstileSiteKey}}";
window.siteInfo = `{{SiteInfo}}`;
</script> </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>

View File

@@ -3,6 +3,7 @@ import {User} from "./network/models.ts";
interface MyWindow extends Window { interface MyWindow extends Window {
serverName?: string; serverName?: string;
cloudflareTurnstileSiteKey?: string; cloudflareTurnstileSiteKey?: string;
siteInfo?: string;
} }
class App { class App {
@@ -14,6 +15,8 @@ class App {
cloudflareTurnstileSiteKey: string | null = null; cloudflareTurnstileSiteKey: string | null = null;
siteInfo = ""
constructor() { constructor() {
this.init(); this.init();
} }
@@ -29,6 +32,7 @@ class App {
} }
this.appName = (window as MyWindow).serverName || this.appName; this.appName = (window as MyWindow).serverName || this.appName;
this.cloudflareTurnstileSiteKey = (window as MyWindow).cloudflareTurnstileSiteKey || null; this.cloudflareTurnstileSiteKey = (window as MyWindow).cloudflareTurnstileSiteKey || null;
this.siteInfo = (window as MyWindow).siteInfo || "";
} }
saveData() { saveData() {

View File

@@ -1,3 +1,5 @@
import React from "react";
interface InputProps { interface InputProps {
type?: string; type?: string;
placeholder?: string; placeholder?: string;
@@ -20,3 +22,19 @@ export default function Input(props: InputProps) {
</fieldset> </fieldset>
} }
} }
interface TextAreaProps {
value: string;
onChange: (e: React.ChangeEvent<HTMLTextAreaElement>) => void;
label: string;
height?: string | number;
}
export function TextArea(props: TextAreaProps) {
return <fieldset className="fieldset w-full">
<legend className="fieldset-legend">{props.label}</legend>
<textarea className={`textarea w-full ${props.height != undefined ? "resize-none" : ""}`} value={props.value} onChange={props.onChange} style={{
height: props.height,
}} />
</fieldset>
}

View File

@@ -149,6 +149,7 @@ export const i18nData = {
"Search Tags": "Search Tags", "Search Tags": "Search Tags",
"Edit Resource": "Edit Resource", "Edit Resource": "Edit Resource",
"Change Bio": "Change Bio", "Change Bio": "Change Bio",
"About this site": "About this site",
} }
}, },
"zh-CN": { "zh-CN": {
@@ -301,6 +302,7 @@ export const i18nData = {
"Search Tags": "搜索标签", "Search Tags": "搜索标签",
"Edit Resource": "编辑资源", "Edit Resource": "编辑资源",
"Change Bio": "更改个人简介", "Change Bio": "更改个人简介",
"About this site": "关于此网站",
} }
}, },
"zh-TW": { "zh-TW": {
@@ -453,6 +455,7 @@ export const i18nData = {
"Search Tags": "搜尋標籤", "Search Tags": "搜尋標籤",
"Edit Resource": "編輯資源", "Edit Resource": "編輯資源",
"Change Bio": "更改個人簡介", "Change Bio": "更改個人簡介",
"About this site": "關於此網站",
} }
} }
} }

View File

@@ -122,4 +122,5 @@ export interface ServerConfig {
cloudflare_turnstile_secret_key: string; cloudflare_turnstile_secret_key: string;
server_name: string; server_name: string;
server_description: string; server_description: string;
site_info: string;
} }

View File

@@ -1,12 +1,33 @@
import { useEffect } from "react"; import {useEffect, useState} 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"; import { app } from "../app.ts";
import Markdown from "react-markdown";
import {useTranslation} from "react-i18next";
export default function HomePage() { export default function HomePage() {
useEffect(() => { useEffect(() => {
document.title = app.appName; document.title = app.appName;
}, []) }, [])
return <ResourcesView loader={(page) => network.getResources(page)}></ResourcesView> const [isCollapsed, setIsCollapsed] = useState(false);
const {t} = useTranslation()
return <>
{
app.siteInfo && <div className={"mt-4 px-4"}>
<div className="collapse collapse-arrow bg-base-100 border border-base-300 cursor-pointer" onClick={() => setIsCollapsed(!isCollapsed)}>
<input type="radio" name="my-accordion-2" checked={isCollapsed}/>
<div className="collapse-title font-semibold">{t("About this site")}</div>
<article className="collapse-content text-sm">
<Markdown>
{app.siteInfo}
</Markdown>
</article>
</div>
</div>
}
<ResourcesView loader={(page) => network.getResources(page)}></ResourcesView>
</>
} }

View File

@@ -4,7 +4,7 @@ import { ErrorAlert, InfoAlert } from "../components/alert"
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { ServerConfig } from "../network/models"; import { ServerConfig } from "../network/models";
import Loading from "../components/loading"; import Loading from "../components/loading";
import Input from "../components/input"; import Input, {TextArea} from "../components/input";
import { network } from "../network/network"; import { network } from "../network/network";
import showToast from "../components/toast"; import showToast from "../components/toast";
import Button from "../components/button"; import Button from "../components/button";
@@ -90,6 +90,9 @@ export default function ManageServerConfigPage() {
<Input type="text" value={config.cloudflare_turnstile_secret_key} label="Cloudflare Turnstile Secret Key" onChange={(e) => { <Input type="text" value={config.cloudflare_turnstile_secret_key} label="Cloudflare Turnstile Secret Key" onChange={(e) => {
setConfig({...config, cloudflare_turnstile_secret_key: e.target.value }) setConfig({...config, cloudflare_turnstile_secret_key: e.target.value })
}}></Input> }}></Input>
<TextArea value={config.site_info} onChange={(e) => {
setConfig({...config, site_info: e.target.value })
}} label="Site info (Markdown)" height={180} />
<InfoAlert className="my-2" message="If the cloudflare turnstile keys are not empty, the turnstile will be used for register and download." /> <InfoAlert className="my-2" message="If the cloudflare turnstile keys are not empty, the turnstile will be used for register and download." />
<div className="flex justify-end"> <div className="flex justify-end">
<Button className="btn-accent shadow" isLoading={isLoading}>{t("Submit")}</Button> <Button className="btn-accent shadow" isLoading={isLoading}>{t("Submit")}</Button>

View File

@@ -22,10 +22,12 @@ 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 is the name of the server. It will be used as the title of the web page.
ServerName string `json:"server_name"` ServerName string `json:"server_name"`
// ServerDescription is the description of the server. It will be used as the description of html meta tag.
ServerDescription string `json:"server_description"` ServerDescription string `json:"server_description"`
// SiteInfo is an article that describes the site. It will be displayed on the home page. Markdown format.
SiteInfo string `json:"site_info"`
} }
func init() { func init() {
@@ -63,8 +65,8 @@ func SetConfig(newConfig ServerConfig) {
if err != nil { if err != nil {
panic(err) panic(err)
} }
filepath := filepath.Join(utils.GetStoragePath(), "config.json") p := filepath.Join(utils.GetStoragePath(), "config.json")
if err := os.WriteFile(filepath, data, 0644); err != nil { if err := os.WriteFile(p, data, 0644); err != nil {
panic(err) panic(err)
} }
} }
@@ -100,3 +102,7 @@ func ServerDescription() string {
func CloudflareTurnstileSecretKey() string { func CloudflareTurnstileSecretKey() string {
return config.CloudflareTurnstileSecretKey return config.CloudflareTurnstileSecretKey
} }
func SiteInfo() string {
return config.SiteInfo
}

View File

@@ -43,8 +43,9 @@ func serveIndexHtml(c fiber.Ctx) error {
description := config.ServerDescription() description := config.ServerDescription()
preview := serverBaseURL + "/icon-192.png" preview := serverBaseURL + "/icon-192.png"
title := siteName title := siteName
url := c.OriginalURL() url := serverBaseURL + c.Path()
cfTurnstileSiteKey := config.CloudflareTurnstileSiteKey() cfTurnstileSiteKey := config.CloudflareTurnstileSiteKey()
siteInfo := config.SiteInfo()
if strings.HasPrefix(url, "/resources/") { if strings.HasPrefix(url, "/resources/") {
idStr := strings.TrimPrefix(url, "/resources/") idStr := strings.TrimPrefix(url, "/resources/")
@@ -75,6 +76,7 @@ func serveIndexHtml(c fiber.Ctx) error {
content = strings.ReplaceAll(content, "{{Title}}", title) content = strings.ReplaceAll(content, "{{Title}}", title)
content = strings.ReplaceAll(content, "{{Url}}", url) content = strings.ReplaceAll(content, "{{Url}}", url)
content = strings.ReplaceAll(content, "{{CFTurnstileSiteKey}}", cfTurnstileSiteKey) content = strings.ReplaceAll(content, "{{CFTurnstileSiteKey}}", cfTurnstileSiteKey)
content = strings.ReplaceAll(content, "{{SiteInfo}}", siteInfo)
c.Set("Content-Type", "text/html; charset=utf-8") c.Set("Content-Type", "text/html; charset=utf-8")
return c.SendString(content) return c.SendString(content)