mirror of
https://github.com/wgh136/nysoure.git
synced 2025-09-27 12:17:24 +00:00
Add site information management with Markdown support
This commit is contained in:
@@ -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>
|
||||||
|
@@ -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() {
|
||||||
|
@@ -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>
|
||||||
|
}
|
@@ -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": "關於此網站",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -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;
|
||||||
}
|
}
|
||||||
|
@@ -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>
|
||||||
|
</>
|
||||||
}
|
}
|
@@ -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>
|
||||||
|
@@ -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
|
||||||
|
}
|
||||||
|
@@ -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)
|
||||||
|
Reference in New Issue
Block a user