mirror of
https://github.com/wgh136/nysoure.git
synced 2025-09-27 04:17:23 +00:00
frontend for pinned resources
This commit is contained in:
@@ -162,6 +162,7 @@ export interface ServerConfig {
|
||||
allow_normal_user_upload: boolean;
|
||||
max_normal_user_upload_size_in_mb: number;
|
||||
upload_prompt: string;
|
||||
pinned_resources: number[];
|
||||
}
|
||||
|
||||
export enum RSort {
|
||||
|
@@ -415,6 +415,10 @@ class Network {
|
||||
);
|
||||
}
|
||||
|
||||
async getPinnedResources(): Promise<Response<Resource[]>> {
|
||||
return this._callApi(() => axios.get(`${this.apiBaseUrl}/resource/pinned`));
|
||||
}
|
||||
|
||||
async createS3Storage(
|
||||
name: string,
|
||||
endPoint: string,
|
||||
|
@@ -95,7 +95,9 @@ function ActivityCard({ activity }: { activity: Activity }) {
|
||||
) {
|
||||
content = (
|
||||
<div className={"mx-1"}>
|
||||
<div className={"font-bold my-4 break-all"}>{activity.resource?.title}</div>
|
||||
<div className={"font-bold my-4 break-all"}>
|
||||
{activity.resource?.title}
|
||||
</div>
|
||||
{activity.resource?.image && (
|
||||
<div>
|
||||
<img
|
||||
@@ -116,7 +118,9 @@ function ActivityCard({ activity }: { activity: Activity }) {
|
||||
} else if (activity.type === ActivityType.NewFile) {
|
||||
content = (
|
||||
<div>
|
||||
<h4 className={"font-bold py-2 break-all"}>{activity.file!.filename}</h4>
|
||||
<h4 className={"font-bold py-2 break-all"}>
|
||||
{activity.file!.filename}
|
||||
</h4>
|
||||
<div className={"text-sm my-1 comment_tile"}>
|
||||
<Markdown>
|
||||
{activity.file!.description.replaceAll("\n", " \n")}
|
||||
@@ -170,8 +174,12 @@ function ActivityCard({ activity }: { activity: Activity }) {
|
||||
src={network.getUserAvatar(activity.user!)}
|
||||
/>
|
||||
</div>
|
||||
<span className={"mx-2 font-bold text-sm"}>{activity.user?.username}</span>
|
||||
<span className={"ml-2 badge-sm sm:badge-md badge badge-primary badge-soft"}>
|
||||
<span className={"mx-2 font-bold text-sm"}>
|
||||
{activity.user?.username}
|
||||
</span>
|
||||
<span
|
||||
className={"ml-2 badge-sm sm:badge-md badge badge-primary badge-soft"}
|
||||
>
|
||||
{messages[activity.type]}
|
||||
</span>
|
||||
</div>
|
||||
|
@@ -164,7 +164,8 @@ export default function CollectionPage() {
|
||||
<span className="flex-1" />
|
||||
{!collection.isPublic && (
|
||||
<Badge className="badge-soft badge-error text-xs mr-2 shadow-xs">
|
||||
<MdOutlineLock size={16} className="inline-block" /> {t("Private")}
|
||||
<MdOutlineLock size={16} className="inline-block" />{" "}
|
||||
{t("Private")}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
|
@@ -2,10 +2,11 @@ import { useEffect, useState } from "react";
|
||||
import ResourcesView from "../components/resources_view.tsx";
|
||||
import { network } from "../network/network.ts";
|
||||
import { app } from "../app.ts";
|
||||
import { RSort } from "../network/models.ts";
|
||||
import { Resource, RSort } from "../network/models.ts";
|
||||
import { useTranslation } from "../utils/i18n";
|
||||
import { useAppContext } from "../components/AppContext.tsx";
|
||||
import Select from "../components/select.tsx";
|
||||
import { useNavigate } from "react-router";
|
||||
|
||||
export default function HomePage() {
|
||||
useEffect(() => {
|
||||
@@ -31,6 +32,7 @@ export default function HomePage() {
|
||||
|
||||
return (
|
||||
<>
|
||||
<PinnedResources />
|
||||
<div className={"flex pt-4 px-4 items-center"}>
|
||||
<Select
|
||||
values={[
|
||||
@@ -58,3 +60,76 @@ export default function HomePage() {
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
let cachedPinnedResources: Resource[] | null = null;
|
||||
|
||||
function PinnedResources() {
|
||||
const [pinnedResources, setPinnedResources] = useState<Resource[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
if (cachedPinnedResources != null) {
|
||||
setPinnedResources(cachedPinnedResources);
|
||||
return;
|
||||
}
|
||||
const prefetchData = app.getPreFetchData();
|
||||
if (prefetchData && prefetchData.pinned) {
|
||||
cachedPinnedResources = prefetchData.pinned;
|
||||
setPinnedResources(cachedPinnedResources!);
|
||||
return;
|
||||
}
|
||||
const fetchPinnedResources = async () => {
|
||||
const res = await network.getPinnedResources();
|
||||
if (res.success) {
|
||||
cachedPinnedResources = res.data ?? [];
|
||||
setPinnedResources(res.data ?? []);
|
||||
}
|
||||
};
|
||||
fetchPinnedResources();
|
||||
}, []);
|
||||
|
||||
if (pinnedResources.length == 0) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 p-4">
|
||||
{pinnedResources.map((resource) => (
|
||||
<PinnedResourceItem key={resource.id} resource={resource} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function PinnedResourceItem({ resource }: { resource: Resource }) {
|
||||
const navigate = useNavigate();
|
||||
|
||||
return (
|
||||
<a
|
||||
href={`/resources/${resource.id}`}
|
||||
className={"p-2 cursor-pointer block"}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
navigate(`/resources/${resource.id}`);
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className={
|
||||
"card shadow hover:shadow-md transition-shadow bg-base-100-tr82"
|
||||
}
|
||||
>
|
||||
{resource.image != null && (
|
||||
<figure>
|
||||
<img
|
||||
src={network.getResampledImageUrl(resource.image.id)}
|
||||
alt="cover"
|
||||
className="w-full aspect-[5/2] object-cover"
|
||||
/>
|
||||
</figure>
|
||||
)}
|
||||
<div className="p-4">
|
||||
<h2 className="card-title break-all">{resource.title}</h2>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
@@ -14,12 +14,15 @@ export default function ManageServerConfigPage() {
|
||||
|
||||
const [config, setConfig] = useState<ServerConfig | null>(null);
|
||||
|
||||
const [pinnedResources, setPinnedResources] = useState("");
|
||||
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
network.getServerConfig().then((res) => {
|
||||
if (res.success) {
|
||||
setConfig(res.data!);
|
||||
setPinnedResources(res.data!.pinned_resources.join(","));
|
||||
} else {
|
||||
showToast({
|
||||
message: res.message,
|
||||
@@ -56,8 +59,25 @@ export default function ManageServerConfigPage() {
|
||||
if (isLoading) {
|
||||
return;
|
||||
}
|
||||
function isPositiveInteger(str: string) {
|
||||
return /^[1-9]\d*$/.test(str);
|
||||
}
|
||||
for (const e of pinnedResources.split(",")) {
|
||||
if (!isPositiveInteger(e)) {
|
||||
showToast({
|
||||
message: "Pinned resources must be a comma separated list of numbers",
|
||||
type: "error",
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
let pinned = pinnedResources.split(",").map((id) => parseInt(id));
|
||||
setConfig({ ...config, pinned_resources: pinned });
|
||||
setIsLoading(true);
|
||||
const res = await network.setServerConfig(config);
|
||||
const res = await network.setServerConfig({
|
||||
...config,
|
||||
pinned_resources: pinned,
|
||||
});
|
||||
if (res.success) {
|
||||
showToast({
|
||||
message: t("Update server config successfully"),
|
||||
@@ -197,6 +217,14 @@ export default function ManageServerConfigPage() {
|
||||
setConfig({ ...config, upload_prompt: e.target.value });
|
||||
}}
|
||||
></Input>
|
||||
<Input
|
||||
type="text"
|
||||
value={pinnedResources}
|
||||
label="Pinned resources"
|
||||
onChange={(e) => {
|
||||
setPinnedResources(e.target.value);
|
||||
}}
|
||||
></Input>
|
||||
<InfoAlert
|
||||
className="my-2"
|
||||
message="If the cloudflare turnstile keys are not empty, the turnstile will be used for register and download."
|
||||
|
@@ -217,6 +217,7 @@ export default function ResourcePage() {
|
||||
</div>
|
||||
</button>
|
||||
<Tags tags={resource.tags} />
|
||||
<CollectionDialog rid={resource.id} />
|
||||
<p className={"px-3 mt-2"}>
|
||||
{resource.links &&
|
||||
resource.links.map((l) => {
|
||||
@@ -237,7 +238,6 @@ export default function ResourcePage() {
|
||||
</a>
|
||||
);
|
||||
})}
|
||||
<CollectionDialog rid={resource.id} />
|
||||
</p>
|
||||
|
||||
<div
|
||||
|
@@ -155,6 +155,14 @@ func serveIndexHtml(c fiber.Ctx) error {
|
||||
preFetchData = url.PathEscape(string(preFetchDataJson))
|
||||
}
|
||||
}
|
||||
} else if path == "/" || path == "" {
|
||||
pinned, err := service.GetPinnedResources()
|
||||
if err == nil {
|
||||
preFetchDataJson, _ := json.Marshal(map[string]interface{}{
|
||||
"pinned": pinned,
|
||||
})
|
||||
preFetchData = url.PathEscape(string(preFetchDataJson))
|
||||
}
|
||||
}
|
||||
|
||||
content = strings.ReplaceAll(content, "{{SiteName}}", siteName)
|
||||
|
Reference in New Issue
Block a user