-
-
{t("Search")}
-
- {searchField}
-
-
-
+ return (
+ <>
+
+
-
- >
+
+ >
+ );
}
- return searchField
+ return searchField;
}
function FloatingToTopButton() {
@@ -293,9 +430,14 @@ function FloatingToTopButton() {
};
}, []);
- return
;
-}
\ No newline at end of file
+ return (
+
+ );
+}
diff --git a/frontend/src/components/pagination.tsx b/frontend/src/components/pagination.tsx
index 7fbb8a1..e4ba76e 100644
--- a/frontend/src/components/pagination.tsx
+++ b/frontend/src/components/pagination.tsx
@@ -1,48 +1,106 @@
-import {ReactNode} from "react";
-import {MdChevronLeft, MdChevronRight} from "react-icons/md";
+import { ReactNode } from "react";
+import { MdChevronLeft, MdChevronRight } from "react-icons/md";
-export default function Pagination({page, setPage, totalPages}: {
- page: number,
- setPage: (page: number) => void,
- totalPages: number
+export default function Pagination({
+ page,
+ setPage,
+ totalPages,
+}: {
+ page: number;
+ setPage: (page: number) => void;
+ totalPages: number;
}) {
const items: ReactNode[] = [];
if (page > 1) {
- items.push(
);
+ items.push(
+
,
+ );
}
if (page - 2 > 1) {
- items.push(
);
+ items.push(
+
,
+ );
}
if (page - 1 > 1) {
- items.push(
);
+ items.push(
+
,
+ );
}
- items.push(
);
+ items.push(
+
,
+ );
if (page + 1 < totalPages) {
- items.push(
);
+ items.push(
+
,
+ );
}
if (page + 2 < totalPages) {
- items.push(
);
+ items.push(
+
,
+ );
}
if (page < totalPages) {
- items.push(
);
+ items.push(
+
,
+ );
}
- return
-
- {items}
-
-
-}
\ No newline at end of file
+ return (
+
+
+ {items}
+
+
+ );
+}
diff --git a/frontend/src/components/popup.tsx b/frontend/src/components/popup.tsx
index cdd30a9..248cc2a 100644
--- a/frontend/src/components/popup.tsx
+++ b/frontend/src/components/popup.tsx
@@ -1,7 +1,10 @@
import React from "react";
import { createRoot } from "react-dom/client";
-export default function showPopup(content: React.ReactNode, element: HTMLElement) {
+export default function showPopup(
+ content: React.ReactNode,
+ element: HTMLElement,
+) {
const eRect = element.getBoundingClientRect();
const div = document.createElement("div");
@@ -39,23 +42,33 @@ export default function showPopup(content: React.ReactNode, element: HTMLElement
mask.onclick = close;
document.body.appendChild(mask);
- createRoot(div).render(
- {content}
- )
+ createRoot(div).render(
+
{content},
+ );
}
-const context = React.createContext<() => void>(() => { });
+const context = React.createContext<() => void>(() => {});
export function useClosePopup() {
return React.useContext(context);
}
-export function PopupMenuItem({ children, onClick }: { children: React.ReactNode, onClick: () => void }) {
+export function PopupMenuItem({
+ children,
+ onClick,
+}: {
+ children: React.ReactNode;
+ onClick: () => void;
+}) {
const close = useClosePopup();
- return
{
- close();
- onClick();
- }}>
- {children}
-
-}
\ No newline at end of file
+ return (
+
{
+ close();
+ onClick();
+ }}
+ >
+ {children}
+
+ );
+}
diff --git a/frontend/src/components/resource_card.tsx b/frontend/src/components/resource_card.tsx
index f85c1fa..a317af6 100644
--- a/frontend/src/components/resource_card.tsx
+++ b/frontend/src/components/resource_card.tsx
@@ -4,48 +4,57 @@ import { useNavigate } from "react-router";
import Badge from "./badge.tsx";
export default function ResourceCard({ resource }: { resource: Resource }) {
- const navigate = useNavigate()
+ const navigate = useNavigate();
- let tags = resource.tags
+ let tags = resource.tags;
if (tags.length > 10) {
- tags = tags.slice(0, 10)
+ tags = tags.slice(0, 10);
}
- return
{
- navigate(`/resources/${resource.id}`)
- }}>
-
- {
- resource.image != null &&
-
-
- }
-
-
{resource.title}
-
-
- {
- tags.map((tag) => {
- return {tag.name}
- })
- }
-
-
-
-
-
-
})
+ return (
+
{
+ navigate(`/resources/${resource.id}`);
+ }}
+ >
+
+ {resource.image != null && (
+
+
+
+ )}
+
+
{resource.title}
+
+
+ {tags.map((tag) => {
+ return (
+
+ {tag.name}
+
+ );
+ })}
+
+
+
+
+
+
})
+
+
+
{resource.author.username}
-
-
{resource.author.username}
-
-}
\ No newline at end of file
+ );
+}
diff --git a/frontend/src/components/resources_view.tsx b/frontend/src/components/resources_view.tsx
index 0ca007a..b3d102b 100644
--- a/frontend/src/components/resources_view.tsx
+++ b/frontend/src/components/resources_view.tsx
@@ -1,69 +1,80 @@
-import {PageResponse, Resource} from "../network/models.ts";
-import {useCallback, useEffect, useRef, useState} from "react";
+import { PageResponse, Resource } from "../network/models.ts";
+import { useCallback, useEffect, useRef, useState } from "react";
import showToast from "./toast.ts";
import ResourceCard from "./resource_card.tsx";
-import {Masonry, useInfiniteLoader} from "masonic";
+import { Masonry, useInfiniteLoader } from "masonic";
import Loading from "./loading.tsx";
-import {useAppContext} from "./AppContext.tsx";
+import { useAppContext } from "./AppContext.tsx";
-export default function ResourcesView({loader, storageKey}: {loader: (page: number) => Promise
>, storageKey?: string}) {
- const [data, setData] = useState([])
- const pageRef = useRef(1)
- const totalPagesRef = useRef(1)
- const isLoadingRef = useRef(false)
-
- const appContext = useAppContext()
+export default function ResourcesView({
+ loader,
+ storageKey,
+}: {
+ loader: (page: number) => Promise>;
+ storageKey?: string;
+}) {
+ const [data, setData] = useState([]);
+ const pageRef = useRef(1);
+ const totalPagesRef = useRef(1);
+ const isLoadingRef = useRef(false);
+
+ const appContext = useAppContext();
useEffect(() => {
if (storageKey) {
- const data = appContext.get(storageKey + "/data")
- const page = appContext.get(storageKey + "/page")
- const totalPages = appContext.get(storageKey + "/totalPages")
- console.log("loading data", data, page, totalPages)
+ const data = appContext.get(storageKey + "/data");
+ const page = appContext.get(storageKey + "/page");
+ const totalPages = appContext.get(storageKey + "/totalPages");
+ console.log("loading data", data, page, totalPages);
if (data) {
- setData(data)
- pageRef.current = page
- totalPagesRef.current = totalPages
+ setData(data);
+ pageRef.current = page;
+ totalPagesRef.current = totalPages;
}
}
}, [appContext, storageKey]);
useEffect(() => {
if (storageKey && data.length > 0) {
- console.log("storing data", data)
- appContext.set(storageKey + "/data", data)
- appContext.set(storageKey + "/page", pageRef.current)
- appContext.set(storageKey + "/totalPages", totalPagesRef.current)
+ console.log("storing data", data);
+ appContext.set(storageKey + "/data", data);
+ appContext.set(storageKey + "/page", pageRef.current);
+ appContext.set(storageKey + "/totalPages", totalPagesRef.current);
}
}, [appContext, data, storageKey]);
const loadPage = useCallback(async () => {
- if (pageRef.current > totalPagesRef.current) return
- if (isLoadingRef.current) return
- isLoadingRef.current = true
- const res = await loader(pageRef.current)
+ if (pageRef.current > totalPagesRef.current) return;
+ if (isLoadingRef.current) return;
+ isLoadingRef.current = true;
+ const res = await loader(pageRef.current);
if (!res.success) {
- showToast({message: res.message, type: "error"})
+ showToast({ message: res.message, type: "error" });
} else {
- isLoadingRef.current = false
- pageRef.current = pageRef.current + 1
- totalPagesRef.current = res.totalPages ?? 1
- setData((prev) => [...prev, ...res.data!])
+ isLoadingRef.current = false;
+ pageRef.current = pageRef.current + 1;
+ totalPagesRef.current = res.totalPages ?? 1;
+ setData((prev) => [...prev, ...res.data!]);
}
- }, [loader])
+ }, [loader]);
useEffect(() => {
- loadPage()
+ loadPage();
}, [loadPage]);
- const maybeLoadMore = useInfiniteLoader(loadPage)
+ const maybeLoadMore = useInfiniteLoader(loadPage);
- return
- {
- return
- } }>
- {
- pageRef.current <= totalPagesRef.current &&
- }
-
-}
\ No newline at end of file
+ return (
+
+ {
+ return ;
+ }}
+ >
+ {pageRef.current <= totalPagesRef.current && }
+
+ );
+}
diff --git a/frontend/src/components/tag_input.tsx b/frontend/src/components/tag_input.tsx
index 4c98c11..0290f66 100644
--- a/frontend/src/components/tag_input.tsx
+++ b/frontend/src/components/tag_input.tsx
@@ -1,229 +1,332 @@
-import {Tag} from "../network/models.ts";
-import {useRef, useState} from "react";
-import {useTranslation} from "react-i18next";
-import {network} from "../network/network.ts";
-import {LuInfo} from "react-icons/lu";
-import {MdSearch} from "react-icons/md";
+import { Tag } from "../network/models.ts";
+import { useRef, useState } from "react";
+import { useTranslation } from "react-i18next";
+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 Input, { TextArea } from "./input.tsx";
+import { ErrorAlert } from "./alert.tsx";
-export default function TagInput({ onAdd, mainTag }: { onAdd: (tag: Tag) => void, mainTag?: boolean }) {
- const [keyword, setKeyword] = useState("")
- const [tags, setTags] = useState([])
- const [error, setError] = useState(null)
- const [isLoading, setLoading] = useState(false)
+export default function TagInput({
+ onAdd,
+ mainTag,
+}: {
+ onAdd: (tag: Tag) => void;
+ mainTag?: boolean;
+}) {
+ const [keyword, setKeyword] = useState("");
+ const [tags, setTags] = useState([]);
+ const [error, setError] = useState(null);
+ const [isLoading, setLoading] = useState(false);
- const debounce = useRef(new Debounce(500))
+ const debounce = useRef(new Debounce(500));
const { t } = useTranslation();
const searchTags = async (keyword: string) => {
if (keyword.length === 0) {
- return
+ return;
}
- setLoading(true)
- setTags([])
- setError(null)
- const res = await network.searchTags(keyword, mainTag)
+ setLoading(true);
+ setTags([]);
+ setError(null);
+ const res = await network.searchTags(keyword, mainTag);
if (!res.success) {
- setError(res.message)
- setLoading(false)
- return
+ setError(res.message);
+ setLoading(false);
+ return;
}
- setTags(res.data!)
- setLoading(false)
- }
+ setTags(res.data!);
+ setLoading(false);
+ };
const handleChange = async (v: string) => {
- setKeyword(v)
- setTags([])
- setError(null)
+ setKeyword(v);
+ setTags([]);
+ setError(null);
if (v.length !== 0) {
- setLoading(true)
- debounce.current.run(() => searchTags(v))
+ setLoading(true);
+ debounce.current.run(() => searchTags(v));
} else {
- setLoading(false)
- debounce.current.cancel()
+ setLoading(false);
+ debounce.current.cancel();
}
- }
+ };
const handleCreateTag = async (name: string) => {
- setLoading(true)
- const res = await network.createTag(name)
+ setLoading(true);
+ const res = await network.createTag(name);
if (!res.success) {
- setError(res.message)
- setLoading(false)
- return
+ setError(res.message);
+ setLoading(false);
+ return;
}
- onAdd(res.data!)
- setKeyword("")
- setTags([])
- setLoading(false)
- const input = document.getElementById("search_tags_input") as HTMLInputElement
- input.blur()
- }
+ onAdd(res.data!);
+ setKeyword("");
+ setTags([]);
+ setLoading(false);
+ const input = document.getElementById(
+ "search_tags_input",
+ ) as HTMLInputElement;
+ input.blur();
+ };
- let dropdownContent
+ let dropdownContent;
if (error) {
- dropdownContent =
+ dropdownContent = (
+
+ );
} else if (!keyword) {
- dropdownContent =
-
-
- {t("Please enter a search keyword")}
-
+ dropdownContent = (
+
+
+
+ {t("Please enter a search keyword")}
+
+ );
} else if (isLoading) {
- dropdownContent =
-
-
- {t("Searching...")}
-
+ dropdownContent = (
+
+
+
+ {t("Searching...")}
+
+ );
} else {
- const haveExactMatch = tags.find((t) => t.name === keyword) !== undefined
- dropdownContent = <>
- {
- tags.map((t) => {
- return {
- onAdd(t);
- setKeyword("")
- setTags([])
- const input = document.getElementById("search_tags_input") as HTMLInputElement
- input.blur()
- }}>
- {t.name}
- {t.type && {t.type}}
-
- })
- }
- {
- !haveExactMatch && {
- handleCreateTag(keyword)
- }}>{t("Create Tag")}: {keyword}
- }
- >
+ const haveExactMatch = tags.find((t) => t.name === keyword) !== undefined;
+ dropdownContent = (
+ <>
+ {tags.map((t) => {
+ return (
+ {
+ onAdd(t);
+ setKeyword("");
+ setTags([]);
+ const input = document.getElementById(
+ "search_tags_input",
+ ) as HTMLInputElement;
+ input.blur();
+ }}
+ >
+
+ {t.name}
+ {t.type && (
+
+ {t.type}
+
+ )}
+
+
+ );
+ })}
+ {!haveExactMatch && (
+ {
+ handleCreateTag(keyword);
+ }}
+ >
+
+ {t("Create Tag")}: {keyword}
+
+
+ )}
+ >
+ );
}
- return
-
-
-
+ return (
+
+
+
+
+ );
}
class Debounce {
- private timer: number | null = null
- private readonly delay: number
+ private timer: number | null = null;
+ private readonly delay: number;
constructor(delay: number) {
- this.delay = delay
+ this.delay = delay;
}
run(callback: () => void) {
if (this.timer) {
- clearTimeout(this.timer)
+ clearTimeout(this.timer);
}
this.timer = setTimeout(() => {
- callback()
- }, this.delay)
+ callback();
+ }, this.delay);
}
cancel() {
if (this.timer) {
- clearTimeout(this.timer)
- this.timer = null
+ clearTimeout(this.timer);
+ this.timer = null;
}
}
}
-export function QuickAddTagDialog({ onAdded }: { onAdded: (tags: Tag[]) => void }) {
- const {t} = useTranslation();
+export function QuickAddTagDialog({
+ onAdded,
+}: {
+ onAdded: (tags: Tag[]) => void;
+}) {
+ const { t } = useTranslation();
- const [text, setText] = useState("")
+ const [text, setText] = useState("");
- const [type, setType] = useState("")
+ const [type, setType] = useState("");
- const [error, setError] = useState(null)
+ const [error, setError] = useState(null);
- const [separator, setSeparator] = useState(",")
+ const [separator, setSeparator] = useState(",");
- const [isLoading, setLoading] = useState(false)
+ const [isLoading, setLoading] = useState(false);
const handleSubmit = async () => {
if (isLoading) {
- return
+ return;
}
if (text.trim().length === 0) {
- return
+ return;
}
- setError(null)
- const names = text.split(separator).filter((n) => n.length > 0)
- setLoading(true)
- const res = await network.getOrCreateTags(names, type)
- setLoading(false)
+ setError(null);
+ const names = text.split(separator).filter((n) => n.length > 0);
+ setLoading(true);
+ const res = await network.getOrCreateTags(names, type);
+ setLoading(false);
if (!res.success) {
- setError(res.message)
- return
+ setError(res.message);
+ return;
}
- const tags = res.data!
- onAdded(tags)
- setText("")
- setType("")
- const dialog = document.getElementById("quick_add_tag_dialog") as HTMLDialogElement
- dialog.close()
- }
+ const tags = res.data!;
+ onAdded(tags);
+ setText("");
+ setType("");
+ const dialog = document.getElementById(
+ "quick_add_tag_dialog",
+ ) as HTMLDialogElement;
+ dialog.close();
+ };
- return <>
-
-