diff --git a/frontend/src/components/image_selector.tsx b/frontend/src/components/image_selector.tsx index 764e159..895bc49 100644 --- a/frontend/src/components/image_selector.tsx +++ b/frontend/src/components/image_selector.tsx @@ -128,7 +128,7 @@ export function UploadClipboardImageButton({ ); } -export function ImageDrapArea({ +export function ImageDropArea({ children, onUploaded, }: { diff --git a/frontend/src/network/models.ts b/frontend/src/network/models.ts index 43b71b4..eaab55d 100644 --- a/frontend/src/network/models.ts +++ b/frontend/src/network/models.ts @@ -42,6 +42,7 @@ export interface TagWithCount extends Tag { export interface CreateResourceParams { title: string; alternative_titles: string[]; + links: RLink[]; tags: number[]; article: string; images: number[]; @@ -53,6 +54,11 @@ export interface Image { height: number; } +export interface RLink { + label: string; + url: string; +} + export interface Resource { id: number; title: string; @@ -66,6 +72,7 @@ export interface ResourceDetails { id: number; title: string; alternativeTitles: string[]; + links: RLink[]; article: string; createdAt: string; tags: Tag[]; diff --git a/frontend/src/pages/edit_resource_page.tsx b/frontend/src/pages/edit_resource_page.tsx index 1c6488f..c45dce6 100644 --- a/frontend/src/pages/edit_resource_page.tsx +++ b/frontend/src/pages/edit_resource_page.tsx @@ -16,7 +16,7 @@ import { ErrorAlert } from "../components/alert.tsx"; import Loading from "../components/loading.tsx"; import TagInput, { QuickAddTagDialog } from "../components/tag_input.tsx"; import { - ImageDrapArea, + ImageDropArea, SelectAndUploadImageButton, UploadClipboardImageButton, } from "../components/image_selector.tsx"; @@ -27,6 +27,7 @@ export default function EditResourcePage() { const [tags, setTags] = useState([]); const [article, setArticle] = useState(""); const [images, setImages] = useState([]); + const [links, setLinks] = useState<{ label: string; url: string }[]>([]); const [error, setError] = useState(null); const [isSubmitting, setSubmitting] = useState(false); const [isLoading, setLoading] = useState(true); @@ -53,6 +54,7 @@ export default function EditResourcePage() { setTags(data.tags); setArticle(data.article); setImages(data.images.map((i) => i.id)); + setLinks(data.links); setLoading(false); } else { showToast({ message: t("Failed to load resource"), type: "error" }); @@ -74,6 +76,12 @@ export default function EditResourcePage() { return; } } + for (let i = 0; i < links.length; i++) { + if (!links[i].label || !links[i].url) { + setError(t("Link cannot be empty")); + return; + } + } if (!tags || tags.length === 0) { setError(t("At least one tag required")); return; @@ -89,6 +97,7 @@ export default function EditResourcePage() { tags: tags.map((tag) => tag.id), article: article, images: images, + links: links, }); if (res.success) { setSubmitting(false); @@ -117,7 +126,7 @@ export default function EditResourcePage() { } return ( - { setImages((prev) => [...prev, ...images]); }} @@ -175,6 +184,61 @@ export default function EditResourcePage() { {t("Add Alternative Title")}
+

{t("Links")}

+
+ {links.map((link, index) => { + return ( +
+ { + const newLinks = [...links]; + newLinks[index].label = e.target.value; + setLinks(newLinks); + }} + /> + { + const newLinks = [...links]; + newLinks[index].url = e.target.value; + setLinks(newLinks); + }} + /> + +
+ ); + })} +
+ +
+
+

{t("Tags")}

{tags.map((tag, index) => { @@ -343,6 +407,6 @@ export default function EditResourcePage() { - + ); } diff --git a/frontend/src/pages/publish_page.tsx b/frontend/src/pages/publish_page.tsx index bee87a0..fcea95c 100644 --- a/frontend/src/pages/publish_page.tsx +++ b/frontend/src/pages/publish_page.tsx @@ -15,7 +15,7 @@ import { ErrorAlert } from "../components/alert.tsx"; import { useAppContext } from "../components/AppContext.tsx"; import TagInput, { QuickAddTagDialog } from "../components/tag_input.tsx"; import { - ImageDrapArea, + ImageDropArea, SelectAndUploadImageButton, UploadClipboardImageButton, } from "../components/image_selector.tsx"; @@ -26,6 +26,7 @@ export default function PublishPage() { const [tags, setTags] = useState([]); const [article, setArticle] = useState(""); const [images, setImages] = useState([]); + const [links, setLinks] = useState<{ label: string; url: string }[]>([]); const [error, setError] = useState(null); const [isSubmitting, setSubmitting] = useState(false); @@ -83,6 +84,12 @@ export default function PublishPage() { return; } } + for (let i = 0; i < links.length; i++) { + if (!links[i].label || !links[i].url) { + setError(t("Link cannot be empty")); + return; + } + } if (!tags || tags.length === 0) { setError(t("At least one tag required")); return; @@ -98,6 +105,7 @@ export default function PublishPage() { tags: tags.map((tag) => tag.id), article: article, images: images, + links: links, }); if (res.success) { localStorage.removeItem("publish_data"); @@ -129,7 +137,7 @@ export default function PublishPage() { } return ( - { setImages((prev) => [...prev, ...images]); }} @@ -187,6 +195,61 @@ export default function PublishPage() { {t("Add Alternative Title")}

+

{t("Links")}

+
+ {links.map((link, index) => { + return ( +
+ { + const newLinks = [...links]; + newLinks[index].label = e.target.value; + setLinks(newLinks); + }} + /> + { + const newLinks = [...links]; + newLinks[index].url = e.target.value; + setLinks(newLinks); + }} + /> + +
+ ); + })} +
+ +
+
+

{t("Tags")}

{tags.map((tag, index) => { @@ -355,6 +418,6 @@ export default function PublishPage() { - + ); } diff --git a/frontend/src/pages/resource_details_page.tsx b/frontend/src/pages/resource_details_page.tsx index 964bf5a..9e4b703 100644 --- a/frontend/src/pages/resource_details_page.tsx +++ b/frontend/src/pages/resource_details_page.tsx @@ -34,6 +34,7 @@ import { MdOutlineDownload, MdOutlineEdit, MdOutlineImage, + MdOutlineLink, MdOutlineOpenInNew, } from "react-icons/md"; import { app } from "../app.ts"; @@ -48,6 +49,7 @@ import Badge, { BadgeAccent } from "../components/badge.tsx"; import Input, { TextArea } from "../components/input.tsx"; import { useAppContext } from "../components/AppContext.tsx"; import { ImageGrid, SquareImage } from "../components/image.tsx"; +import { BiLogoSteam } from "react-icons/bi"; export default function ResourcePage() { const params = useParams(); @@ -182,6 +184,27 @@ export default function ResourcePage() { + {resource.links && ( +

+ {resource.links.map((l) => { + return ( + + {l.url.includes("steampowered.com") ? ( + + ) : ( + + )} + {l.label} + + ); + })} +

+ )} +