Add image upload functionality with drag-and-drop and clipboard support

This commit is contained in:
2025-05-31 18:08:24 +08:00
parent d274735b2d
commit 789fb86109
4 changed files with 440 additions and 309 deletions

View File

@@ -0,0 +1,163 @@
import {MdAdd} from "react-icons/md";
import {useTranslation} from "react-i18next";
import {network} from "../network/network.ts";
import showToast from "./toast.ts";
import {useState} from "react";
async function uploadImages(files: File[]): Promise<number[]> {
const images: number[] = [];
for (const file of files) {
const res = await network.uploadImage(file);
if (res.success) {
images.push(res.data!);
} else {
showToast({
type: "error",
message: `Failed to upload image: ${res.message}`,
})
}
}
return images;
}
export function SelectAndUploadImageButton({onUploaded}: {onUploaded: (image: number[]) => void}) {
const [isUploading, setUploading] = useState(false)
const { t } = useTranslation();
const addImage = () => {
if (isUploading) {
return
}
const input = document.createElement("input")
input.type = "file"
input.accept = "image/*"
input.multiple = true
input.onchange = async () => {
if (!input.files || input.files.length === 0) {
return
}
setUploading(true)
const files = Array.from(input.files);
const uploadedImages = await uploadImages(files);
setUploading(false);
if (uploadedImages.length > 0) {
onUploaded(uploadedImages);
}
}
input.click()
}
return <button className={"btn my-2"} type={"button"} onClick={addImage}>
{isUploading ? <span className="loading loading-spinner"></span> : <MdAdd />}
{t("Upload Image")}
</button>
}
export function UploadClipboardImageButton({onUploaded}: {onUploaded: (image: number[]) => void}) {
const [isUploading, setUploading] = useState(false)
const { t } = useTranslation();
const addClipboardImage = async () => {
if (isUploading) {
return
}
try {
const clipboardItems = await navigator.clipboard.read();
const files: File[] = [];
for (const item of clipboardItems) {
console.log(item)
for (const type of item.types) {
if (type.startsWith("image/")) {
const blob = await item.getType(type);
files.push(new File([blob], `clipboard-image.${type.split("/")[1]}`, { type }));
}
}
}
if (files.length > 0) {
setUploading(true);
const uploadedImages = await uploadImages(files);
setUploading(false);
if (uploadedImages.length > 0) {
onUploaded(uploadedImages);
}
} else {
showToast({
type: "error",
message: t("No image found in clipboard"),
});
}
} catch (error) {
showToast({
type: "error",
message: t("Failed to read clipboard image"),
});
}
}
return <button className={"btn my-2"} type={"button"} onClick={addClipboardImage}>
{isUploading ? <span className="loading loading-spinner"></span> : <MdAdd />}
{t("Upload Clipboard Image")}
</button>
}
export function ImageDrapArea({children, onUploaded}: {children: React.ReactNode, onUploaded: (image: number[]) => void}) {
const [isUploading, setUploading] = useState(false);
const handleDragOver = (e: React.DragEvent<HTMLDivElement>) => {
e.preventDefault();
e.stopPropagation();
};
const handleDragLeave = (e: React.DragEvent<HTMLDivElement>) => {
e.preventDefault();
e.stopPropagation();
};
const handleDrop = async (e: React.DragEvent<HTMLDivElement>) => {
e.preventDefault();
e.stopPropagation();
if (isUploading) {
return;
}
if (e.dataTransfer.files.length > 0) {
setUploading(true);
let files = Array.from(e.dataTransfer.files);
files = files.filter(file => file.type.startsWith("image/"));
if (files.length === 0) {
setUploading(false);
return;
}
const uploadedImages = await uploadImages(files);
if (uploadedImages.length > 0) {
onUploaded(uploadedImages);
}
setUploading(false);
}
};
return (
<>
<dialog id="uploading_image_dialog" className="modal">
<div className="modal-box">
<h3 className="font-bold text-lg">Uploading Image</h3>
<div className={"flex items-center justify-center w-full h-40"}>
<span className="loading loading-spinner progress-primary loading-lg mr-2"></span>
</div>
</div>
</dialog>
<div
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
onDrop={handleDrop}
>
{children}
</div>
</>
);
}

View File

@@ -177,6 +177,7 @@ export const i18nData = {
"Input tags separated by separator.": "Input tags separated by separator.", "Input tags separated by separator.": "Input tags separated by separator.",
"If the tag does not exist, it will be created automatically.": "If the tag does not exist, it will be created automatically.", "If the tag does not exist, it will be created automatically.": "If the tag does not exist, it will be created automatically.",
"Optionally, you can specify a type for the new tags.": "Optionally, you can specify a type for the new tags.", "Optionally, you can specify a type for the new tags.": "Optionally, you can specify a type for the new tags.",
"Upload Clipboard Image": "Upload Clipboard Image",
} }
}, },
"zh-CN": { "zh-CN": {
@@ -357,6 +358,7 @@ export const i18nData = {
"Input tags separated by separator.": "输入标签, 用分隔符分隔。", "Input tags separated by separator.": "输入标签, 用分隔符分隔。",
"If the tag does not exist, it will be created automatically.": "如果标签不存在, 将自动创建。", "If the tag does not exist, it will be created automatically.": "如果标签不存在, 将自动创建。",
"Optionally, you can specify a type for the new tags.": "您可以选择为新标签指定一个类型。", "Optionally, you can specify a type for the new tags.": "您可以选择为新标签指定一个类型。",
"Upload Clipboard Image": "上传剪贴板图片",
} }
}, },
"zh-TW": { "zh-TW": {
@@ -537,6 +539,7 @@ export const i18nData = {
"Input tags separated by separator.": "輸入標籤, 用分隔符分隔。", "Input tags separated by separator.": "輸入標籤, 用分隔符分隔。",
"If the tag does not exist, it will be created automatically.": "如果標籤不存在, 將自動創建。", "If the tag does not exist, it will be created automatically.": "如果標籤不存在, 將自動創建。",
"Optionally, you can specify a type for the new tags.": "您可以選擇為新標籤指定一個類型。", "Optionally, you can specify a type for the new tags.": "您可以選擇為新標籤指定一個類型。",
"Upload Clipboard Image": "上傳剪貼板圖片",
} }
} }
} }

View File

@@ -9,6 +9,7 @@ import { app } from "../app.ts";
import { ErrorAlert } from "../components/alert.tsx"; import { ErrorAlert } from "../components/alert.tsx";
import Loading from "../components/loading.tsx"; import Loading from "../components/loading.tsx";
import TagInput, {QuickAddTagDialog} from "../components/tag_input.tsx"; import TagInput, {QuickAddTagDialog} from "../components/tag_input.tsx";
import {ImageDrapArea, SelectAndUploadImageButton, UploadClipboardImageButton} from "../components/image_selector.tsx";
export default function EditResourcePage() { export default function EditResourcePage() {
const [title, setTitle] = useState<string>("") const [title, setTitle] = useState<string>("")
@@ -16,7 +17,6 @@ export default function EditResourcePage() {
const [tags, setTags] = useState<Tag[]>([]) const [tags, setTags] = useState<Tag[]>([])
const [article, setArticle] = useState<string>("") const [article, setArticle] = useState<string>("")
const [images, setImages] = useState<number[]>([]) const [images, setImages] = useState<number[]>([])
const [isUploading, setUploading] = useState(false)
const [error, setError] = useState<string | null>(null) const [error, setError] = useState<string | null>(null)
const [isSubmitting, setSubmitting] = useState(false) const [isSubmitting, setSubmitting] = useState(false)
const [isLoading, setLoading] = useState(true) const [isLoading, setLoading] = useState(true)
@@ -87,32 +87,6 @@ export default function EditResourcePage() {
setError(res.message) setError(res.message)
} }
} }
const addImage = () => {
if (isUploading) {
return
}
const input = document.createElement("input")
input.type = "file"
input.accept = "image/*"
input.onchange = async () => {
const files = input.files
if (!files || files.length === 0) {
return
}
const image = files[0]
setUploading(true)
const res = await network.uploadImage(image)
if (res.success) {
setUploading(false)
setImages([...images, res.data!])
} else {
setUploading(false)
showToast({ message: t("Failed to upload image"), type: "error" })
}
}
input.click()
}
if (isNaN(id)) { if (isNaN(id)) {
return <ErrorAlert className={"m-4"} message={t("Invalid resource ID")} /> return <ErrorAlert className={"m-4"} message={t("Invalid resource ID")} />
@@ -126,154 +100,163 @@ export default function EditResourcePage() {
return <Loading/> return <Loading/>
} }
return <div className={"p-4"}> return <ImageDrapArea onUploaded={(images) => {
<h1 className={"text-2xl font-bold my-4"}>{t("Edit Resource")}</h1> setImages((prev) => ([...prev, ...images]));
<div role="alert" className="alert alert-info mb-2 alert-dash"> }}>
<MdOutlineInfo size={24} /> <div className={"p-4"}>
<span>{t("All information can be modified after publishing")}</span> <h1 className={"text-2xl font-bold my-4"}>{t("Edit Resource")}</h1>
</div> <div role="alert" className="alert alert-info mb-2 alert-dash">
<p className={"my-1"}>{t("Title")}</p> <MdOutlineInfo size={24} />
<input type="text" className="input w-full" value={title} onChange={(e) => setTitle(e.target.value)} /> <span>{t("All information can be modified after publishing")}</span>
<div className={"h-4"}></div> </div>
<p className={"my-1"}>{t("Alternative Titles")}</p> <p className={"my-1"}>{t("Title")}</p>
{ <input type="text" className="input w-full" value={title} onChange={(e) => setTitle(e.target.value)} />
altTitles.map((title, index) => { <div className={"h-4"}></div>
return <div key={index} className={"flex items-center my-2"}> <p className={"my-1"}>{t("Alternative Titles")}</p>
<input type="text" className="input w-full" value={title} onChange={(e) => {
const newAltTitles = [...altTitles]
newAltTitles[index] = e.target.value
setAltTitles(newAltTitles)
}} />
<button className={"btn btn-square btn-error ml-2"} type={"button"} onClick={() => {
const newAltTitles = [...altTitles]
newAltTitles.splice(index, 1)
setAltTitles(newAltTitles)
}}>
<MdDelete size={24} />
</button>
</div>
})
}
<button className={"btn my-2"} type={"button"} onClick={() => {
setAltTitles([...altTitles, ""])
}}>
<MdAdd />
{t("Add Alternative Title")}
</button>
<div className={"h-2"}></div>
<p className={"my-1"}>{t("Tags")}</p>
<p className={"my-1 pb-1"}>
{ {
tags.map((tag, index) => { altTitles.map((title, index) => {
return <span key={index} className={"badge badge-primary mr-2 text-sm"}> return <div key={index} className={"flex items-center my-2"}>
{tag.name} <input type="text" className="input w-full" value={title} onChange={(e) => {
<span onClick={() => { const newAltTitles = [...altTitles]
const newTags = [...tags] newAltTitles[index] = e.target.value
newTags.splice(index, 1) setAltTitles(newAltTitles)
setTags(newTags) }} />
<button className={"btn btn-square btn-error ml-2"} type={"button"} onClick={() => {
const newAltTitles = [...altTitles]
newAltTitles.splice(index, 1)
setAltTitles(newAltTitles)
}}> }}>
<MdDelete size={24} />
</button>
</div>
})
}
<button className={"btn my-2"} type={"button"} onClick={() => {
setAltTitles([...altTitles, ""])
}}>
<MdAdd />
{t("Add Alternative Title")}
</button>
<div className={"h-2"}></div>
<p className={"my-1"}>{t("Tags")}</p>
<p className={"my-1 pb-1"}>
{
tags.map((tag, index) => {
return <span key={index} className={"badge badge-primary mr-2 text-sm"}>
{tag.name}
<span onClick={() => {
const newTags = [...tags]
newTags.splice(index, 1)
setTags(newTags)
}}>
<MdClose size={18}/> <MdClose size={18}/>
</span> </span>
</span> </span>
})
}
</p>
<div className={"flex items-center"}>
<TagInput onAdd={(tag) => {
setTags((prev) => {
const existingTag = prev.find(t => t.id === tag.id);
if (existingTag) {
return prev; // If the tag already exists, do not add it again
}
return [...prev, tag];
})
}} />
<span className={"w-4"}/>
<QuickAddTagDialog onAdded={(tags) => {
setTags((prev) => {
const newTags = [...prev];
for (const tag of tags) {
const existingTag = newTags.find(t => t.id === tag.id);
if (!existingTag) {
newTags.push(tag);
}
}
return newTags;
})
}}/>
</div>
<div className={"h-4"}></div>
<p className={"my-1"}>{t("Description")}</p>
<textarea className="textarea w-full min-h-80 p-4" value={article} onChange={(e) => setArticle(e.target.value)} />
<div className={"flex items-center py-1 "}>
<MdOutlineInfo className={"inline mr-1"} />
<span className={"text-sm"}>{t("Use Markdown format")}</span>
</div>
<div className={"h-4"}></div>
<p className={"my-1"}>{t("Images")}</p>
<div role="alert" className="alert alert-info alert-soft my-2">
<MdOutlineInfo size={24} />
<div>
<p>{t("Images will not be displayed automatically, you need to reference them in the description")}</p>
<p>{t("The first image will be used as the cover image")}</p>
</div>
</div>
<div className={`rounded-box border border-base-content/5 bg-base-100 ${images.length === 0 ? "hidden" : ""}`}>
<table className={"table"}>
<thead>
<tr>
<td>{t("Preview")}</td>
<td>{t("Link")}</td>
<td>{t("Action")}</td>
</tr>
</thead>
<tbody>
{
images.map((image, index) => {
return <tr key={index} className={"hover"}>
<td>
<img src={network.getImageUrl(image)} className={"w-16 h-16 object-cover card"} alt={"image"} />
</td>
<td>
{network.getImageUrl(image)}
</td>
<td>
<button className={"btn btn-square"} type={"button"} onClick={() => {
const id = images[index]
const newImages = [...images]
newImages.splice(index, 1)
setImages(newImages)
network.deleteImage(id)
}}>
<MdDelete size={24} />
</button>
</td>
</tr>
}) })
} }
</tbody> </p>
</table> <div className={"flex items-center"}>
</div> <TagInput onAdd={(tag) => {
<button className={"btn my-2"} type={"button"} onClick={addImage}> setTags((prev) => {
{isUploading ? <span className="loading loading-spinner"></span> : <MdAdd />} const existingTag = prev.find(t => t.id === tag.id);
{t("Upload Image")} if (existingTag) {
</button> return prev; // If the tag already exists, do not add it again
<div className={"h-4"}></div> }
{ return [...prev, tag];
error && <div role="alert" className="alert alert-error my-2 shadow"> })
<svg xmlns="http://www.w3.org/2000/svg" className="h-6 w-6 shrink-0 stroke-current" fill="none" }} />
viewBox="0 0 24 24"> <span className={"w-4"}/>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" <QuickAddTagDialog onAdded={(tags) => {
d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" /> setTags((prev) => {
</svg> const newTags = [...prev];
<span>{t("Error")}: {error}</span> for (const tag of tags) {
const existingTag = newTags.find(t => t.id === tag.id);
if (!existingTag) {
newTags.push(tag);
}
}
return newTags;
})
}}/>
</div>
<div className={"h-4"}></div>
<p className={"my-1"}>{t("Description")}</p>
<textarea className="textarea w-full min-h-80 p-4" value={article} onChange={(e) => setArticle(e.target.value)} />
<div className={"flex items-center py-1 "}>
<MdOutlineInfo className={"inline mr-1"} />
<span className={"text-sm"}>{t("Use Markdown format")}</span>
</div>
<div className={"h-4"}></div>
<p className={"my-1"}>{t("Images")}</p>
<div role="alert" className="alert alert-info alert-soft my-2">
<MdOutlineInfo size={24} />
<div>
<p>{t("Images will not be displayed automatically, you need to reference them in the description")}</p>
<p>{t("The first image will be used as the cover image")}</p>
</div>
</div>
<div className={`rounded-box border border-base-content/5 bg-base-100 ${images.length === 0 ? "hidden" : ""}`}>
<table className={"table"}>
<thead>
<tr>
<td>{t("Preview")}</td>
<td>{t("Link")}</td>
<td>{t("Action")}</td>
</tr>
</thead>
<tbody>
{
images.map((image, index) => {
return <tr key={index} className={"hover"}>
<td>
<img src={network.getImageUrl(image)} className={"w-16 h-16 object-cover card"} alt={"image"} />
</td>
<td>
{network.getImageUrl(image)}
</td>
<td>
<button className={"btn btn-square"} type={"button"} onClick={() => {
const id = images[index]
const newImages = [...images]
newImages.splice(index, 1)
setImages(newImages)
network.deleteImage(id)
}}>
<MdDelete size={24} />
</button>
</td>
</tr>
})
}
</tbody>
</table>
</div>
<div className={"flex"}>
<SelectAndUploadImageButton onUploaded={(images) => {
setImages((prev) => ([...prev, ...images]));
}}/>
<span className={"w-4"}></span>
<UploadClipboardImageButton onUploaded={(images) => {
setImages((prev) => ([...prev, ...images]));
}}/>
</div>
<div className={"h-4"}></div>
{
error && <div role="alert" className="alert alert-error my-2 shadow">
<svg xmlns="http://www.w3.org/2000/svg" className="h-6 w-6 shrink-0 stroke-current" fill="none"
viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<span>{t("Error")}: {error}</span>
</div>
}
<div className={"flex flex-row-reverse mt-4"}>
<button className={"btn btn-accent shadow"} onClick={handleSubmit}>
{isSubmitting && <span className="loading loading-spinner"></span>}
{t("Publish")}
</button>
</div> </div>
}
<div className={"flex flex-row-reverse mt-4"}>
<button className={"btn btn-accent shadow"} onClick={handleSubmit}>
{isSubmitting && <span className="loading loading-spinner"></span>}
{t("Publish")}
</button>
</div> </div>
</div> </ImageDrapArea>
} }

View File

@@ -3,12 +3,12 @@ import {MdAdd, MdClose, MdDelete, MdOutlineInfo} from "react-icons/md";
import { Tag } from "../network/models.ts"; import { Tag } from "../network/models.ts";
import { network } from "../network/network.ts"; import { network } from "../network/network.ts";
import { useNavigate } from "react-router"; import { useNavigate } from "react-router";
import showToast from "../components/toast.ts";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { app } from "../app.ts"; import { app } from "../app.ts";
import { ErrorAlert } from "../components/alert.tsx"; import { ErrorAlert } from "../components/alert.tsx";
import {useAppContext} from "../components/AppContext.tsx"; import {useAppContext} from "../components/AppContext.tsx";
import TagInput, {QuickAddTagDialog} from "../components/tag_input.tsx"; import TagInput, {QuickAddTagDialog} from "../components/tag_input.tsx";
import {ImageDrapArea, SelectAndUploadImageButton, UploadClipboardImageButton} from "../components/image_selector.tsx";
export default function PublishPage() { export default function PublishPage() {
const [title, setTitle] = useState<string>("") const [title, setTitle] = useState<string>("")
@@ -16,7 +16,6 @@ export default function PublishPage() {
const [tags, setTags] = useState<Tag[]>([]) const [tags, setTags] = useState<Tag[]>([])
const [article, setArticle] = useState<string>("") const [article, setArticle] = useState<string>("")
const [images, setImages] = useState<number[]>([]) const [images, setImages] = useState<number[]>([])
const [isUploading, setUploading] = useState(false)
const [error, setError] = useState<string | null>(null) const [error, setError] = useState<string | null>(null)
const [isSubmitting, setSubmitting] = useState(false) const [isSubmitting, setSubmitting] = useState(false)
@@ -68,32 +67,6 @@ export default function PublishPage() {
} }
} }
const addImage = () => {
if (isUploading) {
return
}
const input = document.createElement("input")
input.type = "file"
input.accept = "image/*"
input.onchange = async () => {
const files = input.files
if (!files || files.length === 0) {
return
}
const image = files[0]
setUploading(true)
const res = await network.uploadImage(image)
if (res.success) {
setUploading(false)
setImages([...images, res.data!])
} else {
setUploading(false)
showToast({ message: t("Failed to upload image"), type: "error" })
}
}
input.click()
}
if (!app.user) { if (!app.user) {
return <ErrorAlert className={"m-4"} message={t("You are not logged in. Please log in to access this page.")} /> return <ErrorAlert className={"m-4"} message={t("You are not logged in. Please log in to access this page.")} />
} }
@@ -102,108 +75,111 @@ export default function PublishPage() {
return <ErrorAlert className={"m-4"} message={t("You are not authorized to access this page.")} /> return <ErrorAlert className={"m-4"} message={t("You are not authorized to access this page.")} />
} }
return <div className={"p-4"}> return <ImageDrapArea onUploaded={(images) => {
<h1 className={"text-2xl font-bold my-4"}>{t("Publish Resource")}</h1> setImages((prev) => ([...prev, ...images]));
<div role="alert" className="alert alert-info mb-2 alert-dash"> }}>
<MdOutlineInfo size={24} /> <div className={"p-4"}>
<span>{t("All information can be modified after publishing")}</span> <h1 className={"text-2xl font-bold my-4"}>{t("Publish Resource")}</h1>
</div> <div role="alert" className="alert alert-info mb-2 alert-dash">
<p className={"my-1"}>{t("Title")}</p> <MdOutlineInfo size={24} />
<input type="text" className="input w-full" value={title} onChange={(e) => setTitle(e.target.value)} /> <span>{t("All information can be modified after publishing")}</span>
<div className={"h-4"}></div> </div>
<p className={"my-1"}>{t("Alternative Titles")}</p> <p className={"my-1"}>{t("Title")}</p>
{ <input type="text" className="input w-full" value={title} onChange={(e) => setTitle(e.target.value)} />
altTitles.map((title, index) => { <div className={"h-4"}></div>
return <div key={index} className={"flex items-center my-2"}> <p className={"my-1"}>{t("Alternative Titles")}</p>
<input type="text" className="input w-full" value={title} onChange={(e) => {
const newAltTitles = [...altTitles]
newAltTitles[index] = e.target.value
setAltTitles(newAltTitles)
}} />
<button className={"btn btn-square btn-error ml-2"} type={"button"} onClick={() => {
const newAltTitles = [...altTitles]
newAltTitles.splice(index, 1)
setAltTitles(newAltTitles)
}}>
<MdDelete size={24} />
</button>
</div>
})
}
<button className={"btn my-2"} type={"button"} onClick={() => {
setAltTitles([...altTitles, ""])
}}>
<MdAdd />
{t("Add Alternative Title")}
</button>
<div className={"h-2"}></div>
<p className={"my-1"}>{t("Tags")}</p>
<p className={"my-1 pb-1"}>
{ {
tags.map((tag, index) => { altTitles.map((title, index) => {
return <span key={index} className={"badge badge-primary mr-2 text-sm"}> return <div key={index} className={"flex items-center my-2"}>
{tag.name} <input type="text" className="input w-full" value={title} onChange={(e) => {
<span onClick={() => { const newAltTitles = [...altTitles]
const newTags = [...tags] newAltTitles[index] = e.target.value
newTags.splice(index, 1) setAltTitles(newAltTitles)
setTags(newTags) }} />
<button className={"btn btn-square btn-error ml-2"} type={"button"} onClick={() => {
const newAltTitles = [...altTitles]
newAltTitles.splice(index, 1)
setAltTitles(newAltTitles)
}}> }}>
<MdDelete size={24} />
</button>
</div>
})
}
<button className={"btn my-2"} type={"button"} onClick={() => {
setAltTitles([...altTitles, ""])
}}>
<MdAdd />
{t("Add Alternative Title")}
</button>
<div className={"h-2"}></div>
<p className={"my-1"}>{t("Tags")}</p>
<p className={"my-1 pb-1"}>
{
tags.map((tag, index) => {
return <span key={index} className={"badge badge-primary mr-2 text-sm"}>
{tag.name}
<span onClick={() => {
const newTags = [...tags]
newTags.splice(index, 1)
setTags(newTags)
}}>
<MdClose size={18}/> <MdClose size={18}/>
</span> </span>
</span> </span>
}) })
} }
</p> </p>
<div className={"flex items-center"}> <div className={"flex items-center"}>
<TagInput onAdd={(tag) => { <TagInput onAdd={(tag) => {
setTags((prev) => { setTags((prev) => {
const existingTag = prev.find(t => t.id === tag.id); const existingTag = prev.find(t => t.id === tag.id);
if (existingTag) { if (existingTag) {
return prev; // If the tag already exists, do not add it again return prev; // If the tag already exists, do not add it again
}
return [...prev, tag];
})
}} />
<span className={"w-4"}/>
<QuickAddTagDialog onAdded={(tags) => {
setTags((prev) => {
const newTags = [...prev];
for (const tag of tags) {
const existingTag = newTags.find(t => t.id === tag.id);
if (!existingTag) {
newTags.push(tag);
} }
} return [...prev, tag];
return newTags; })
}) }} />
}}/> <span className={"w-4"}/>
</div> <QuickAddTagDialog onAdded={(tags) => {
<div className={"h-4"}></div> setTags((prev) => {
<p className={"my-1"}>{t("Description")}</p> const newTags = [...prev];
<textarea className="textarea w-full min-h-80 p-4" value={article} onChange={(e) => setArticle(e.target.value)} /> for (const tag of tags) {
<div className={"flex items-center py-1 "}> const existingTag = newTags.find(t => t.id === tag.id);
<MdOutlineInfo className={"inline mr-1"} /> if (!existingTag) {
<span className={"text-sm"}>{t("Use Markdown format")}</span> newTags.push(tag);
</div> }
<div className={"h-4"}></div> }
<p className={"my-1"}>{t("Images")}</p> return newTags;
<div role="alert" className="alert alert-info alert-soft my-2"> })
<MdOutlineInfo size={24} /> }}/>
<div>
<p>{t("Images will not be displayed automatically, you need to reference them in the description")}</p>
<p>{t("The first image will be used as the cover image")}</p>
</div> </div>
</div> <div className={"h-4"}></div>
<div className={`rounded-box border border-base-content/5 bg-base-100 ${images.length === 0 ? "hidden" : ""}`}> <p className={"my-1"}>{t("Description")}</p>
<table className={"table"}> <textarea className="textarea w-full min-h-80 p-4" value={article} onChange={(e) => setArticle(e.target.value)} />
<thead> <div className={"flex items-center py-1 "}>
<MdOutlineInfo className={"inline mr-1"} />
<span className={"text-sm"}>{t("Use Markdown format")}</span>
</div>
<div className={"h-4"}></div>
<p className={"my-1"}>{t("Images")}</p>
<div role="alert" className="alert alert-info alert-soft my-2">
<MdOutlineInfo size={24} />
<div>
<p>{t("Images will not be displayed automatically, you need to reference them in the description")}</p>
<p>{t("The first image will be used as the cover image")}</p>
</div>
</div>
<div className={`rounded-box border border-base-content/5 bg-base-100 ${images.length === 0 ? "hidden" : ""}`}>
<table className={"table"}>
<thead>
<tr> <tr>
<td>{t("Preview")}</td> <td>{t("Preview")}</td>
<td>{t("Link")}</td> <td>{t("Link")}</td>
<td>{t("Action")}</td> <td>{t("Action")}</td>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{ {
images.map((image, index) => { images.map((image, index) => {
return <tr key={index} className={"hover"}> return <tr key={index} className={"hover"}>
@@ -227,29 +203,35 @@ export default function PublishPage() {
</tr> </tr>
}) })
} }
</tbody> </tbody>
</table> </table>
</div> </div>
<button className={"btn my-2"} type={"button"} onClick={addImage}> <div className={"flex"}>
{isUploading ? <span className="loading loading-spinner"></span> : <MdAdd />} <SelectAndUploadImageButton onUploaded={(images) => {
{t("Upload Image")} setImages((prev) => ([...prev, ...images]));
</button> }}/>
<div className={"h-4"}></div> <span className={"w-4"}></span>
{ <UploadClipboardImageButton onUploaded={(images) => {
error && <div role="alert" className="alert alert-error my-2 shadow"> setImages((prev) => ([...prev, ...images]));
<svg xmlns="http://www.w3.org/2000/svg" className="h-6 w-6 shrink-0 stroke-current" fill="none" }}/>
viewBox="0 0 24 24"> </div>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" <div className={"h-4"}></div>
d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" /> {
</svg> error && <div role="alert" className="alert alert-error my-2 shadow">
<span>{t("Error")}: {error}</span> <svg xmlns="http://www.w3.org/2000/svg" className="h-6 w-6 shrink-0 stroke-current" fill="none"
viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<span>{t("Error")}: {error}</span>
</div>
}
<div className={"flex flex-row-reverse mt-4"}>
<button className={"btn btn-accent shadow"} onClick={handleSubmit}>
{isSubmitting && <span className="loading loading-spinner"></span>}
{t("Publish")}
</button>
</div> </div>
}
<div className={"flex flex-row-reverse mt-4"}>
<button className={"btn btn-accent shadow"} onClick={handleSubmit}>
{isSubmitting && <span className="loading loading-spinner"></span>}
{t("Publish")}
</button>
</div> </div>
</div> </ImageDrapArea>
} }