Implemented Image Upload Modal in Article Editor
This commit is contained in:
parent
f61188784d
commit
1750542182
|
@ -1,11 +1,15 @@
|
||||||
import React, { useState, useEffect, useRef } from "react";
|
import React, { useState, useEffect, useRef } from "react";
|
||||||
import { updateCharactersLeft, insertBeforeSelection, insertBeforeAndAfterSelection } from "../utilities/md_functions";
|
import { updateCharactersLeft, insertBeforeSelection, insertBeforeAndAfterSelection } from "../utilities/md_functions";
|
||||||
import { LabelInput, ToolBarButton } from "./Forms";
|
import { LabelInput, ToolBarButton } from "./Forms";
|
||||||
|
import ImageEditor from "./ImageEditor";
|
||||||
import { CategoryColor, Category, ArticleStatus, ArticleView, ArticleDto } from "../model/Models";
|
import { CategoryColor, Category, ArticleStatus, ArticleView, ArticleDto } from "../model/Models";
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
// @ts-ignore
|
||||||
import markdownit from "markdown-it";
|
import markdownit from "markdown-it";
|
||||||
|
// @ts-ignore
|
||||||
import markdownitmark from "markdown-it-mark";
|
import markdownitmark from "markdown-it-mark";
|
||||||
import "groupby-polyfill/lib/polyfill.js";
|
import "groupby-polyfill/lib/polyfill.js";
|
||||||
|
import TextareaMarkdownEditor from 'react-textarea-markdown-editor';
|
||||||
|
|
||||||
const nameof = function<T>(name: keyof T) { return name; }
|
const nameof = function<T>(name: keyof T) { return name; }
|
||||||
|
|
||||||
|
@ -62,6 +66,7 @@ export default function Editor() {
|
||||||
const [isPublished, setIsPublished] = useState(false);
|
const [isPublished, setIsPublished] = useState(false);
|
||||||
const [article, setArticle] = useState<ArticleView|null>(null);
|
const [article, setArticle] = useState<ArticleView|null>(null);
|
||||||
const [categories, setCategories] = useState<Category[]>([]);
|
const [categories, setCategories] = useState<Category[]>([]);
|
||||||
|
const [imageDialog, setImageDialog] = useState(false);
|
||||||
const [model, setModel] = useState<ArticleDto>({
|
const [model, setModel] = useState<ArticleDto>({
|
||||||
body: "",
|
body: "",
|
||||||
categories: [],
|
categories: [],
|
||||||
|
@ -155,6 +160,7 @@ export default function Editor() {
|
||||||
categories: result.categories.map(c => c.id),
|
categories: result.categories.map(c => c.id),
|
||||||
}));
|
}));
|
||||||
setArticle(result);
|
setArticle(result);
|
||||||
|
// setMarkdown(result.body)
|
||||||
console.log("Article loaded");
|
console.log("Article loaded");
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
|
@ -165,16 +171,27 @@ export default function Editor() {
|
||||||
}
|
}
|
||||||
}, ([setArticle, setNotice, console, location]) as any[]);
|
}, ([setArticle, setNotice, console, location]) as any[]);
|
||||||
|
|
||||||
const markdownArea = useRef<HTMLTextAreaElement>(null);
|
const textAreaMarkdown = useRef<TextareaMarkdownEditor>(null);
|
||||||
|
const markdownOnChange = function(v: string) {
|
||||||
|
onChangeModel({
|
||||||
|
target: {
|
||||||
|
// @ts-ignore
|
||||||
|
value: v,
|
||||||
|
name: nameof<ArticleDto>("body")
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
// @ts-ignore
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{
|
{
|
||||||
dirty &&
|
dirty &&
|
||||||
<div role="alert" className="alert alert-warning sticky left-4 right-4 top-4 mb-4 z-50 rounded-sm">
|
<div role="alert" className="alert alert-warning sticky left-4 right-4 top-4 mb-4 z-50 rounded-sm">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" className="size-6">
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" className="size-6 hidden md:inline">
|
||||||
<path fillRule="evenodd" clipRule="evenodd" d="M9.401 3.003c1.155-2 4.043-2 5.197 0l7.355 12.748c1.154 2-.29 4.5-2.599 4.5H4.645c-2.309 0-3.752-2.5-2.598-4.5L9.4 3.003ZM12 8.25a.75.75 0 0 1 .75.75v3.75a.75.75 0 0 1-1.5 0V9a.75.75 0 0 1 .75-.75Zm0 8.25a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5Z" />
|
<path fillRule="evenodd" clipRule="evenodd" d="M9.401 3.003c1.155-2 4.043-2 5.197 0l7.355 12.748c1.154 2-.29 4.5-2.599 4.5H4.645c-2.309 0-3.752-2.5-2.598-4.5L9.4 3.003ZM12 8.25a.75.75 0 0 1 .75.75v3.75a.75.75 0 0 1-1.5 0V9a.75.75 0 0 1 .75-.75Zm0 8.25a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5Z" />
|
||||||
</svg>
|
</svg>
|
||||||
<p>{t("editor.unsaved_changes_notice")}</p>
|
<p className="line-clamp-1">{t("editor.unsaved_changes_notice")}</p>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
|
@ -198,6 +215,11 @@ export default function Editor() {
|
||||||
<li className={`step w-24 ${article.status === ArticleStatus.Published ? "step-primary" : ""}`}>{t("Published")}</li>
|
<li className={`step w-24 ${article.status === ArticleStatus.Published ? "step-primary" : ""}`}>{t("Published")}</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
|
<ImageEditor open={imageDialog} onClose={() => setImageDialog(false)} callback={(location) => {
|
||||||
|
textAreaMarkdown.current?.append(`\n![](${location})\n`)
|
||||||
|
setImageDialog(false)
|
||||||
|
}} />
|
||||||
|
|
||||||
<form method="post" onSubmit={onSubmit}>
|
<form method="post" onSubmit={onSubmit}>
|
||||||
<fieldset className="grid grid-cols-1 lg:grid-cols-2 gap-x-8">
|
<fieldset className="grid grid-cols-1 lg:grid-cols-2 gap-x-8">
|
||||||
<LabelInput label={t("Title_Label")}>
|
<LabelInput label={t("Title_Label")}>
|
||||||
|
@ -213,6 +235,7 @@ export default function Editor() {
|
||||||
onChange={onChangeModel} name={nameof<ArticleDto>("categories")}
|
onChange={onChangeModel} name={nameof<ArticleDto>("categories")}
|
||||||
defaultValue={article.categories.map(c => c.id)}>
|
defaultValue={article.categories.map(c => c.id)}>
|
||||||
{
|
{
|
||||||
|
// @ts-ignore
|
||||||
Array.from(Map.groupBy(categories, (c: Category) => c.color) as Map<CategoryColor, Category[]>)
|
Array.from(Map.groupBy(categories, (c: Category) => c.color) as Map<CategoryColor, Category[]>)
|
||||||
.map((value, _) =>
|
.map((value, _) =>
|
||||||
<optgroup className="font-bold not-italic my-3"
|
<optgroup className="font-bold not-italic my-3"
|
||||||
|
@ -242,77 +265,77 @@ export default function Editor() {
|
||||||
|
|
||||||
<fieldset className="my-6 grid grid-cols-1 lg:grid-cols-2 gap-x-8 gap-y-4">
|
<fieldset className="my-6 grid grid-cols-1 lg:grid-cols-2 gap-x-8 gap-y-4">
|
||||||
<div className="join join-vertical min-h-96 h-full w-full">
|
<div className="join join-vertical min-h-96 h-full w-full">
|
||||||
<div className="flex flex-wrap gap-1 p-2 z-50 bg-base-200 sticky top-0"
|
<div className="flex flex-wrap gap-1 p-2 z-50 bg-base-200 sticky top-20 rounded-b-none rounded-t-sm"
|
||||||
role="toolbar">
|
role="toolbar">
|
||||||
<div className="join join-horizontal">
|
<div className="join join-horizontal">
|
||||||
<ToolBarButton title={t("Tools.H1_Tooltip")}
|
<ToolBarButton title={t("Tools.H1_Tooltip")}
|
||||||
onClick={() => insertBeforeSelection(markdownArea.current, "# ", true)}>
|
onClick={() => textAreaMarkdown.current?.markLine("# ")}>
|
||||||
<strong>{t("Tools.H1_Label")}</strong>
|
<strong>{t("Tools.H1_Label")}</strong>
|
||||||
</ToolBarButton>
|
</ToolBarButton>
|
||||||
<ToolBarButton title={t("Tools.H2_Tooltip")}
|
<ToolBarButton title={t("Tools.H2_Tooltip")}
|
||||||
onClick={() => insertBeforeSelection(markdownArea.current, "## ", true)}>
|
onClick={() => textAreaMarkdown.current?.markLine("## ")}>
|
||||||
<strong>{t("Tools.H2_Label")}</strong>
|
<strong>{t("Tools.H2_Label")}</strong>
|
||||||
</ToolBarButton>
|
</ToolBarButton>
|
||||||
<ToolBarButton title={t("Tools.H3_Tooltip")}
|
<ToolBarButton title={t("Tools.H3_Tooltip")}
|
||||||
onClick={() => insertBeforeSelection(markdownArea.current, "### ", true)}>
|
onClick={() => textAreaMarkdown.current?.markLine("### ")}>
|
||||||
<strong>{t("Tools.H3_Label")}</strong>
|
<strong>{t("Tools.H3_Label")}</strong>
|
||||||
</ToolBarButton>
|
</ToolBarButton>
|
||||||
<ToolBarButton title={t("Tools.H4_Tooltip")}
|
<ToolBarButton title={t("Tools.H4_Tooltip")}
|
||||||
onClick={() => insertBeforeSelection(markdownArea.current, "#### ", true)}>
|
onClick={() => textAreaMarkdown.current?.markLine("#### ")}>
|
||||||
<strong>{t("Tools.H4_Label")}</strong>
|
<strong>{t("Tools.H4_Label")}</strong>
|
||||||
</ToolBarButton>
|
</ToolBarButton>
|
||||||
</div>
|
</div>
|
||||||
<div className="join join-horizontal">
|
<div className="join join-horizontal">
|
||||||
<ToolBarButton title={t("Tools.Bold_Tooltip")}
|
<ToolBarButton title={t("Tools.Bold_Tooltip")}
|
||||||
onClick={() => insertBeforeAndAfterSelection(markdownArea.current, "**")}>
|
onClick={() => textAreaMarkdown.current?.mark('**', '**',t("Tools.Bold_Tooltip"))}>
|
||||||
<strong>B</strong>
|
<strong>B</strong>
|
||||||
</ToolBarButton>
|
</ToolBarButton>
|
||||||
<ToolBarButton title={t("Tools.Italic_Tooltip")}
|
<ToolBarButton title={t("Tools.Italic_Tooltip")}
|
||||||
onClick={() => insertBeforeAndAfterSelection(markdownArea.current, "*")}>
|
onClick={() => textAreaMarkdown.current?.mark('*', '*',t("Tools.Italic_Tooltip"))}>
|
||||||
<em>I</em>
|
<em>I</em>
|
||||||
</ToolBarButton>
|
</ToolBarButton>
|
||||||
<ToolBarButton title={t("Tools.Underline_Tooltip")}
|
<ToolBarButton title={t("Tools.Underline_Tooltip")}
|
||||||
onClick={() => insertBeforeAndAfterSelection(markdownArea.current, "++")}>
|
onClick={() => textAreaMarkdown.current?.mark('+', '+',t("Tools.Underline_Tooltip"))}>
|
||||||
<span className="underline">U</span>
|
<span className="underline">U</span>
|
||||||
</ToolBarButton>
|
</ToolBarButton>
|
||||||
<ToolBarButton title={t("Tools.StrikeThrough_Tooltip")}
|
<ToolBarButton title={t("Tools.StrikeThrough_Tooltip")}
|
||||||
onClick={() => insertBeforeAndAfterSelection(markdownArea.current, "~~")}>
|
onClick={() => textAreaMarkdown.current?.mark('~~', '~~',t("Tools.StrikeThrough_Tooltip"))}>
|
||||||
<del>{t("Tools.StrikeThrough_Label")}</del>
|
<del>{t("Tools.StrikeThrough_Label")}</del>
|
||||||
</ToolBarButton>
|
</ToolBarButton>
|
||||||
<ToolBarButton title={t("Tools.Mark_Tooltip")}
|
<ToolBarButton title={t("Tools.Mark_Tooltip")}
|
||||||
onClick={() => insertBeforeAndAfterSelection(markdownArea.current, "==")}>
|
onClick={() => textAreaMarkdown.current?.mark('==', '==',t("Tools.Mark_Tooltip"))}>
|
||||||
<mark>{t("Tools.Mark_Label")}</mark>
|
<mark>{t("Tools.Mark_Label")}</mark>
|
||||||
</ToolBarButton>
|
</ToolBarButton>
|
||||||
<ToolBarButton title={t("Tools.Mark_Tooltip")}
|
<ToolBarButton title={t("Tools.Mark_Tooltip")}
|
||||||
onClick={() => insertBeforeSelection(markdownArea.current, "> ", true)}>
|
onClick={() => textAreaMarkdown.current?.markLine("> ")}>
|
||||||
| <em>{t("Tools.Cite_Label")}</em>
|
| <em>{t("Tools.Cite_Label")}</em>
|
||||||
</ToolBarButton>
|
</ToolBarButton>
|
||||||
</div>
|
</div>
|
||||||
<div className="join join-horizontal">
|
<div className="join join-horizontal">
|
||||||
<ToolBarButton
|
<ToolBarButton
|
||||||
onClick={() => insertBeforeSelection(markdownArea.current, "1. ", true)}>
|
onClick={() => textAreaMarkdown.current?.markLine("1. ")}>
|
||||||
1.
|
1.
|
||||||
</ToolBarButton>
|
</ToolBarButton>
|
||||||
<ToolBarButton
|
<ToolBarButton
|
||||||
onClick={() => insertBeforeSelection(markdownArea.current, "a. ", true)}>
|
onClick={() => textAreaMarkdown.current?.markLine("a. ")}>
|
||||||
a.
|
a.
|
||||||
</ToolBarButton>
|
</ToolBarButton>
|
||||||
<ToolBarButton
|
<ToolBarButton
|
||||||
onClick={() => insertBeforeSelection(markdownArea.current, "A. ", true)}>
|
onClick={() => textAreaMarkdown.current?.markLine("A. ")}>
|
||||||
A.
|
A.
|
||||||
</ToolBarButton>
|
</ToolBarButton>
|
||||||
<ToolBarButton
|
<ToolBarButton
|
||||||
onClick={() => insertBeforeSelection(markdownArea.current, "i. ", true)}>
|
onClick={() => textAreaMarkdown.current?.markLine("i. ")}>
|
||||||
i.
|
i.
|
||||||
</ToolBarButton>
|
</ToolBarButton>
|
||||||
<ToolBarButton
|
<ToolBarButton
|
||||||
onClick={() => insertBeforeSelection(markdownArea.current, "I. ", true)}>
|
onClick={() => textAreaMarkdown.current?.markLine("I. ")}>
|
||||||
I.
|
I.
|
||||||
</ToolBarButton>
|
</ToolBarButton>
|
||||||
</div>
|
</div>
|
||||||
<div className="join join-horizontal">
|
<div className="join join-horizontal">
|
||||||
<ToolBarButton title={t("Tools.CodeLine_Tooltip")}
|
<ToolBarButton title={t("Tools.CodeLine_Tooltip")}
|
||||||
onClick={() => insertBeforeAndAfterSelection(markdownArea.current, "`")}>
|
onClick={() => textAreaMarkdown.current?.mark('`', '`',t("Tools.CodeLine_Tooltip"))}>
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"
|
||||||
fill="currentColor"
|
fill="currentColor"
|
||||||
className="w-4 h-4">
|
className="w-4 h-4">
|
||||||
|
@ -322,7 +345,7 @@ export default function Editor() {
|
||||||
</svg>
|
</svg>
|
||||||
</ToolBarButton>
|
</ToolBarButton>
|
||||||
<ToolBarButton title={t("Tools.CodeBlock_Tooltip")}
|
<ToolBarButton title={t("Tools.CodeBlock_Tooltip")}
|
||||||
onClick={() => insertBeforeAndAfterSelection(markdownArea.current, "```")}>
|
onClick={() => textAreaMarkdown.current?.mark('```\n', '\n```\n',t("Tools.CodeBlock_Tooltip"))}>
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"
|
||||||
fill="currentColor"
|
fill="currentColor"
|
||||||
className="w-4 h-4">
|
className="w-4 h-4">
|
||||||
|
@ -332,12 +355,25 @@ export default function Editor() {
|
||||||
</svg>
|
</svg>
|
||||||
</ToolBarButton>
|
</ToolBarButton>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="join join-horizontal">
|
||||||
|
<ToolBarButton title={t("Tools.ImageAdd_Tooltip")}
|
||||||
|
onClick={() => setImageDialog(true)}>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"
|
||||||
|
fill="currentColor" className="w-4 h-4">
|
||||||
|
<path fill-rule="evenodd"
|
||||||
|
d="M1.5 6a2.25 2.25 0 0 1 2.25-2.25h16.5A2.25 2.25 0 0 1 22.5 6v12a2.25 2.25 0 0 1-2.25 2.25H3.75A2.25 2.25 0 0 1 1.5 18V6ZM3 16.06V18c0 .414.336.75.75.75h16.5A.75.75 0 0 0 21 18v-1.94l-2.69-2.689a1.5 1.5 0 0 0-2.12 0l-.88.879.97.97a.75.75 0 1 1-1.06 1.06l-5.16-5.159a1.5 1.5 0 0 0-2.12 0L3 16.061Zm10.125-7.81a1.125 1.125 0 1 1 2.25 0 1.125 1.125 0 0 1-2.25 0Z"
|
||||||
|
clip-rule="evenodd"/>
|
||||||
|
</svg>
|
||||||
|
<span>{t("Tools.ImageAdd_Label")}</span>
|
||||||
|
</ToolBarButton>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<textarea ref={markdownArea} id="tool-target"
|
<TextareaMarkdownEditor ref={textAreaMarkdown} markers={[]}
|
||||||
className="resize-none textarea textarea-bordered outline-none w-full flex-1 join-item"
|
textareaId="tool-target"
|
||||||
required aria-required placeholder={t("Body_Placeholder")}
|
value={model.body} onChange={markdownOnChange}
|
||||||
autoComplete="off"
|
className="first:*:hidden flex-1 flex join-item *:rounded-t-none *:rounded-b-sm w-full *:w-full *:h-full *:flex-1 *:resize-none *:outline-none *:textarea *:textarea-bordered"
|
||||||
name={nameof<ArticleDto>("body")} value={model.body} onChange={onChangeModel}/>
|
placeholder={t("Body_Placeholder")}
|
||||||
|
doParse={md.render}/>
|
||||||
</div>
|
</div>
|
||||||
<div className="bg-base-200 p-2">
|
<div className="bg-base-200 p-2">
|
||||||
<h2 className="text-2xl lg:text-4xl font-bold mb-6 hyphens-auto">
|
<h2 className="text-2xl lg:text-4xl font-bold mb-6 hyphens-auto">
|
||||||
|
|
|
@ -14,7 +14,7 @@ export function LabelInput({label, className, children} : ILabelProperties) : Re
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ToolBarButton({title, onClick, children}: {title?: string, onClick:React.MouseEventHandler<HTMLButtonElement>, children:any}) {
|
export function ToolBarButton({title, onClick, children}: {title?: string, onClick:React.MouseEventHandler<HTMLButtonElement>, children:any}) {
|
||||||
return <button type="button" className="btn btn-accent btn-sm outline-none font-normal join-item"
|
return <button type="button" className="btn btn-accent btn-sm text-sm justify-center items-center outline-none font-normal join-item"
|
||||||
title={title}
|
title={title}
|
||||||
onClick={onClick}>
|
onClick={onClick}>
|
||||||
{children ?? "err"}
|
{children ?? "err"}
|
||||||
|
|
35
Wave/Assets/React/ImageEditor.tsx
Normal file
35
Wave/Assets/React/ImageEditor.tsx
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
import React, {useEffect} from "react";
|
||||||
|
import Modal from "./Modal";
|
||||||
|
|
||||||
|
const ImageEditor = function({open = false, onClose, callback}: {open: boolean, onClose: () => void, callback: (location: string) => void}){
|
||||||
|
async function onSubmit(event: React.FormEvent) {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
const formData = new FormData(event.target as HTMLFormElement);
|
||||||
|
let response = await fetch("/images/create", {
|
||||||
|
method: "PUT",
|
||||||
|
body: formData
|
||||||
|
})
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(response.statusText);
|
||||||
|
}
|
||||||
|
|
||||||
|
(event.target as HTMLFormElement)?.reset()
|
||||||
|
|
||||||
|
const loc = response.headers.get("Location") as string;
|
||||||
|
callback(loc);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal open={open} onClose={onClose}>
|
||||||
|
<form onSubmit={onSubmit} className="flex gap-2 p-2">
|
||||||
|
<input id="file" name="file" type="file" alt="Image"
|
||||||
|
className="file-input file-input-bordered w-full max-w-xs" autoFocus={true}/>
|
||||||
|
|
||||||
|
<button type="submit" className="btn btn-primary w-full sm:btn-wide">Upload</button>
|
||||||
|
</form>
|
||||||
|
</Modal>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ImageEditor;
|
29
Wave/Assets/React/Modal.tsx
Normal file
29
Wave/Assets/React/Modal.tsx
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
import React, {useEffect, useRef} from "react";
|
||||||
|
|
||||||
|
interface ModalProperties {
|
||||||
|
open: boolean,
|
||||||
|
onClose: () => void,
|
||||||
|
className?: string,
|
||||||
|
children: React.ReactNode,
|
||||||
|
}
|
||||||
|
|
||||||
|
const Modal = function({open, onClose, children}: ModalProperties) {
|
||||||
|
const ref = useRef<HTMLDialogElement>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (open && ref.current) ref.current.showModal();
|
||||||
|
else if (ref.current) ref.current.close()
|
||||||
|
}, [open])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<dialog ref={ref} onCancel={onClose} className="p-4 rounded-lg bg-base-200 border border-base-300 shadow z-[100] backdrop:bg-base-100 backdrop:bg-opacity-50">
|
||||||
|
{children}
|
||||||
|
|
||||||
|
<button type="button" className="btn btn-primary w-full sm:btn-wide" onClick={onClose}>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
</dialog>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Modal;
|
|
@ -78,6 +78,9 @@ if (domNode) {
|
||||||
|
|
||||||
CodeLine_Tooltip: "Mark selected text as programming code",
|
CodeLine_Tooltip: "Mark selected text as programming code",
|
||||||
CodeBlock_Tooltip: "Insert program code block",
|
CodeBlock_Tooltip: "Insert program code block",
|
||||||
|
|
||||||
|
ImageAdd_Label: "Image",
|
||||||
|
ImageAdd_Tooltip: "Upload an Image",
|
||||||
},
|
},
|
||||||
Category: {
|
Category: {
|
||||||
Primary: "Primary Category",
|
Primary: "Primary Category",
|
||||||
|
@ -134,6 +137,9 @@ if (domNode) {
|
||||||
|
|
||||||
CodeLine_Tooltip: "Selektierten text als programmcode markieren",
|
CodeLine_Tooltip: "Selektierten text als programmcode markieren",
|
||||||
CodeBlock_Tooltip: "Programmierblock einfügen",
|
CodeBlock_Tooltip: "Programmierblock einfügen",
|
||||||
|
|
||||||
|
ImageAdd_Label: "Bild",
|
||||||
|
ImageAdd_Tooltip: "Bild Hochladen",
|
||||||
},
|
},
|
||||||
Category: {
|
Category: {
|
||||||
Primary: "Hauptkategorie",
|
Primary: "Hauptkategorie",
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
using Microsoft.AspNetCore.Http.HttpResults;
|
using Microsoft.AspNetCore.Http.HttpResults;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Wave.Services;
|
using Wave.Services;
|
||||||
|
|
||||||
namespace Wave.Controllers;
|
namespace Wave.Controllers;
|
||||||
|
@ -19,4 +20,27 @@ public class ImageController(ImageService imageService) : ControllerBase {
|
||||||
if (size < 800) return File(await ImageService.GetResized(path, size), ImageService.ImageMimeType);
|
if (size < 800) return File(await ImageService.GetResized(path, size), ImageService.ImageMimeType);
|
||||||
return File(System.IO.File.OpenRead(path), ImageService.ImageMimeType);
|
return File(System.IO.File.OpenRead(path), ImageService.ImageMimeType);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HttpPut("create")]
|
||||||
|
[Authorize(Policy = "ArticleEditPermissions")]
|
||||||
|
[Consumes("multipart/form-data")]
|
||||||
|
public async Task<IActionResult> CreateImageAsync(
|
||||||
|
[FromForm] IFormFile file,
|
||||||
|
ImageService.ImageQuality quality = ImageService.ImageQuality.Normal) {
|
||||||
|
try {
|
||||||
|
string tempFile = Path.GetTempFileName();
|
||||||
|
{
|
||||||
|
await using var stream = System.IO.File.OpenWrite(tempFile);
|
||||||
|
await file.CopyToAsync(stream);
|
||||||
|
stream.Close();
|
||||||
|
}
|
||||||
|
var id = await ImageService.StoreImageAsync(tempFile);
|
||||||
|
if (id is null) throw new ApplicationException("Saving image failed unexpectedly.");
|
||||||
|
return Created($"/images/{id}", new CreateResponse(id.Value));
|
||||||
|
} catch (Exception ex) {
|
||||||
|
return BadRequest($"Failed to process image: {ex.Message}.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
record CreateResponse(Guid Id);
|
||||||
}
|
}
|
|
@ -3,6 +3,10 @@
|
||||||
namespace Wave.Services;
|
namespace Wave.Services;
|
||||||
|
|
||||||
public class ImageService(ILogger<ImageService> logger) {
|
public class ImageService(ILogger<ImageService> logger) {
|
||||||
|
public enum ImageQuality {
|
||||||
|
Normal, High, Source
|
||||||
|
}
|
||||||
|
|
||||||
private ILogger<ImageService> Logger { get; } = logger;
|
private ILogger<ImageService> Logger { get; } = logger;
|
||||||
private const string BasePath = "./files/images";
|
private const string BasePath = "./files/images";
|
||||||
private const string ImageExtension = ".jpg";
|
private const string ImageExtension = ".jpg";
|
||||||
|
@ -52,7 +56,7 @@ public class ImageService(ILogger<ImageService> logger) {
|
||||||
await image.WriteAsync(path, cancellation);
|
await image.WriteAsync(path, cancellation);
|
||||||
return guid;
|
return guid;
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
Logger.LogInformation(ex, "Failed to process uploaded image.");
|
Logger.LogWarning(ex, "Failed to process uploaded image.");
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
BIN
Wave/package-lock.json
generated
BIN
Wave/package-lock.json
generated
Binary file not shown.
|
@ -33,6 +33,7 @@
|
||||||
"vite": "^5.2.13"
|
"vite": "^5.2.13"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"groupby-polyfill": "^1.0.0"
|
"groupby-polyfill": "^1.0.0",
|
||||||
|
"react-textarea-markdown-editor": "^2.3.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue