feat: add collection

This commit is contained in:
2025-07-31 15:41:15 +08:00
parent 1e5b12f531
commit 08c70a0b52
38 changed files with 1079 additions and 418 deletions

View File

@@ -1,5 +1,5 @@
import { useState, useRef, useEffect } from "react";
import { useTranslation } from "react-i18next";
import { useTranslation } from "../utils/i18n";
import showToast from "./toast";
import { network } from "../network/network";
import { InfoAlert } from "./alert";

View File

@@ -1,4 +1,4 @@
import { useTranslation } from "react-i18next";
import { useTranslation } from "../utils/i18n";
import { useNavigate } from "react-router";
import { MdOutlineComment } from "react-icons/md";
import { Comment } from "../network/models";

View File

@@ -1,5 +1,5 @@
import { MdAdd } from "react-icons/md";
import { useTranslation } from "react-i18next";
import { useTranslation } from "../utils/i18n";
import { network } from "../network/network.ts";
import showToast from "./toast.ts";
import { useState } from "react";

View File

@@ -1,4 +1,4 @@
import { useTranslation } from "react-i18next";
import { useTranslation } from "../utils/i18n";
export default function Loading() {
const { t } = useTranslation();

View File

@@ -3,7 +3,7 @@ import { network } from "../network/network.ts";
import { useNavigate, useOutlet } from "react-router";
import { createContext, useContext, useEffect, useState } from "react";
import { MdArrowUpward, MdOutlinePerson, MdSearch } from "react-icons/md";
import { useTranslation } from "react-i18next";
import { useTranslation } from "../utils/i18n";
import UploadingSideBar from "./uploading_side_bar.tsx";
import { ThemeSwitcher } from "./theme_switcher.tsx";
import { IoLogoGithub } from "react-icons/io";

View File

@@ -2,8 +2,15 @@ import { Resource } from "../network/models.ts";
import { network } from "../network/network.ts";
import { useNavigate } from "react-router";
import Badge from "./badge.tsx";
import React from "react";
export default function ResourceCard({ resource }: { resource: Resource }) {
export default function ResourceCard({
resource,
action,
}: {
resource: Resource;
action?: React.ReactNode;
}) {
const navigate = useNavigate();
let tags = resource.tags;
@@ -58,6 +65,8 @@ export default function ResourceCard({ resource }: { resource: Resource }) {
</div>
<div className="w-2"></div>
<div className="text-sm">{resource.author.username}</div>
<div className="flex-1"></div>
{action}
</div>
</div>
</div>

View File

@@ -9,9 +9,11 @@ import { useAppContext } from "./AppContext.tsx";
export default function ResourcesView({
loader,
storageKey,
actionBuilder,
}: {
loader: (page: number) => Promise<PageResponse<Resource>>;
storageKey?: string;
actionBuilder?: (resource: Resource) => React.ReactNode;
}) {
const [data, setData] = useState<Resource[]>([]);
const pageRef = useRef(1);
@@ -54,7 +56,8 @@ export default function ResourcesView({
isLoadingRef.current = false;
pageRef.current = pageRef.current + 1;
totalPagesRef.current = res.totalPages ?? 1;
setData((prev) => [...prev, ...res.data!]);
let data = res.data ?? [];
setData((prev) => [...prev, ...data]);
}
}, [loader]);
@@ -71,7 +74,13 @@ export default function ResourcesView({
columnWidth={300}
items={data}
render={(e) => {
return <ResourceCard resource={e.data} key={e.data.id} />;
return (
<ResourceCard
resource={e.data}
key={e.data.id}
action={actionBuilder?.(e.data)}
/>
);
}}
></Masonry>
{pageRef.current <= totalPagesRef.current && <Loading />}

View File

@@ -1,12 +1,13 @@
import { Tag } from "../network/models.ts";
import { useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import { useTranslation } from "../utils/i18n";
import { network } from "../network/network.ts";
import { LuInfo } from "react-icons/lu";
import { MdSearch } from "react-icons/md";
import Button from "./button.tsx";
import Input, { TextArea } from "./input.tsx";
import { ErrorAlert } from "./alert.tsx";
import { Debounce } from "../utils/debounce.ts";
export default function TagInput({
onAdd,
@@ -177,31 +178,6 @@ export default function TagInput({
);
}
class Debounce {
private timer: number | null = null;
private readonly delay: number;
constructor(delay: number) {
this.delay = delay;
}
run(callback: () => void) {
if (this.timer) {
clearTimeout(this.timer);
}
this.timer = setTimeout(() => {
callback();
}, this.delay);
}
cancel() {
if (this.timer) {
clearTimeout(this.timer);
this.timer = null;
}
}
}
export function QuickAddTagDialog({
onAdded,
}: {

View File

@@ -1,5 +1,5 @@
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { useTranslation } from "../utils/i18n";
import { MdPalette } from "react-icons/md";
interface ThemeOption {

View File

@@ -10,7 +10,7 @@ export default function showToast({
type = type || "info";
const div = document.createElement("div");
div.innerHTML = `
<div class="toast toast-center">
<div class="toast toast-center z-10">
<div class="alert shadow ${type === "success" && "alert-success"} ${type === "error" && "alert-error"} ${type === "warning" && "alert-warning"} ${type === "info" && "alert-info"}">
<span>${message}</span>
</div>