mirror of
https://github.com/wgh136/nysoure.git
synced 2025-09-27 12:17:24 +00:00
Add link support to resource creation and editing, including validation
This commit is contained in:
@@ -128,7 +128,7 @@ export function UploadClipboardImageButton({
|
||||
);
|
||||
}
|
||||
|
||||
export function ImageDrapArea({
|
||||
export function ImageDropArea({
|
||||
children,
|
||||
onUploaded,
|
||||
}: {
|
||||
|
@@ -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[];
|
||||
|
@@ -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<Tag[]>([]);
|
||||
const [article, setArticle] = useState<string>("");
|
||||
const [images, setImages] = useState<number[]>([]);
|
||||
const [links, setLinks] = useState<{ label: string; url: string }[]>([]);
|
||||
const [error, setError] = useState<string | null>(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 (
|
||||
<ImageDrapArea
|
||||
<ImageDropArea
|
||||
onUploaded={(images) => {
|
||||
setImages((prev) => [...prev, ...images]);
|
||||
}}
|
||||
@@ -175,6 +184,61 @@ export default function EditResourcePage() {
|
||||
{t("Add Alternative Title")}
|
||||
</button>
|
||||
<div className={"h-2"}></div>
|
||||
<p className={"my-1"}>{t("Links")}</p>
|
||||
<div className={"flex flex-col"}>
|
||||
{links.map((link, index) => {
|
||||
return (
|
||||
<div key={index} className={"flex items-center my-2"}>
|
||||
<input
|
||||
type="text"
|
||||
className="input"
|
||||
placeholder={t("Label")}
|
||||
value={link.label}
|
||||
onChange={(e) => {
|
||||
const newLinks = [...links];
|
||||
newLinks[index].label = e.target.value;
|
||||
setLinks(newLinks);
|
||||
}}
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
className="input w-full ml-2"
|
||||
placeholder={t("URL")}
|
||||
value={link.url}
|
||||
onChange={(e) => {
|
||||
const newLinks = [...links];
|
||||
newLinks[index].url = e.target.value;
|
||||
setLinks(newLinks);
|
||||
}}
|
||||
/>
|
||||
<button
|
||||
className={"btn btn-square btn-error ml-2"}
|
||||
type={"button"}
|
||||
onClick={() => {
|
||||
const newLinks = [...links];
|
||||
newLinks.splice(index, 1);
|
||||
setLinks(newLinks);
|
||||
}}
|
||||
>
|
||||
<MdDelete size={24} />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
<div className={"flex"}>
|
||||
<button
|
||||
className={"btn my-2"}
|
||||
type={"button"}
|
||||
onClick={() => {
|
||||
setLinks([...links, { label: "", url: "" }]);
|
||||
}}
|
||||
>
|
||||
<MdAdd />
|
||||
{t("Add Link")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className={"h-2"}></div>
|
||||
<p className={"my-1"}>{t("Tags")}</p>
|
||||
<p className={"my-1 pb-1"}>
|
||||
{tags.map((tag, index) => {
|
||||
@@ -343,6 +407,6 @@ export default function EditResourcePage() {
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</ImageDrapArea>
|
||||
</ImageDropArea>
|
||||
);
|
||||
}
|
||||
|
@@ -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<Tag[]>([]);
|
||||
const [article, setArticle] = useState<string>("");
|
||||
const [images, setImages] = useState<number[]>([]);
|
||||
const [links, setLinks] = useState<{ label: string; url: string }[]>([]);
|
||||
const [error, setError] = useState<string | null>(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 (
|
||||
<ImageDrapArea
|
||||
<ImageDropArea
|
||||
onUploaded={(images) => {
|
||||
setImages((prev) => [...prev, ...images]);
|
||||
}}
|
||||
@@ -187,6 +195,61 @@ export default function PublishPage() {
|
||||
{t("Add Alternative Title")}
|
||||
</button>
|
||||
<div className={"h-2"}></div>
|
||||
<p className={"my-1"}>{t("Links")}</p>
|
||||
<div className={"flex flex-col"}>
|
||||
{links.map((link, index) => {
|
||||
return (
|
||||
<div key={index} className={"flex items-center my-2"}>
|
||||
<input
|
||||
type="text"
|
||||
className="input"
|
||||
placeholder={t("Label")}
|
||||
value={link.label}
|
||||
onChange={(e) => {
|
||||
const newLinks = [...links];
|
||||
newLinks[index].label = e.target.value;
|
||||
setLinks(newLinks);
|
||||
}}
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
className="input w-full ml-2"
|
||||
placeholder={t("URL")}
|
||||
value={link.url}
|
||||
onChange={(e) => {
|
||||
const newLinks = [...links];
|
||||
newLinks[index].url = e.target.value;
|
||||
setLinks(newLinks);
|
||||
}}
|
||||
/>
|
||||
<button
|
||||
className={"btn btn-square btn-error ml-2"}
|
||||
type={"button"}
|
||||
onClick={() => {
|
||||
const newLinks = [...links];
|
||||
newLinks.splice(index, 1);
|
||||
setLinks(newLinks);
|
||||
}}
|
||||
>
|
||||
<MdDelete size={24} />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
<div className={"flex"}>
|
||||
<button
|
||||
className={"btn my-2"}
|
||||
type={"button"}
|
||||
onClick={() => {
|
||||
setLinks([...links, { label: "", url: "" }]);
|
||||
}}
|
||||
>
|
||||
<MdAdd />
|
||||
{t("Add Link")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className={"h-2"}></div>
|
||||
<p className={"my-1"}>{t("Tags")}</p>
|
||||
<p className={"my-1 pb-1"}>
|
||||
{tags.map((tag, index) => {
|
||||
@@ -355,6 +418,6 @@ export default function PublishPage() {
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</ImageDrapArea>
|
||||
</ImageDropArea>
|
||||
);
|
||||
}
|
||||
|
@@ -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() {
|
||||
</div>
|
||||
</button>
|
||||
<Tags tags={resource.tags} />
|
||||
{resource.links && (
|
||||
<p className={"px-3 mt-2"}>
|
||||
{resource.links.map((l) => {
|
||||
return (
|
||||
<span
|
||||
className={
|
||||
"py-1 px-3 inline-flex items-center m-1 border border-base-300 rounded-2xl hover:bg-base-200 transition-colors cursor-pointer select-none"
|
||||
}
|
||||
>
|
||||
{l.url.includes("steampowered.com") ? (
|
||||
<BiLogoSteam size={20} />
|
||||
) : (
|
||||
<MdOutlineLink size={20} />
|
||||
)}
|
||||
<span className={"ml-2 text-sm"}>{l.label}</span>
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
</p>
|
||||
)}
|
||||
|
||||
<div className="tabs tabs-box my-4 mx-2 p-4">
|
||||
<label className="tab transition-all">
|
||||
<input
|
||||
|
Reference in New Issue
Block a user