migrated editing client
This commit is contained in:
@@ -1,10 +1,16 @@
|
||||
import { Show, createSignal } from "solid-js";
|
||||
import { useSearchParams, useNavigate, query } from "@solidjs/router";
|
||||
import { Show, createSignal, onCleanup } from "solid-js";
|
||||
import { useNavigate, query } from "@solidjs/router";
|
||||
import { Title } from "@solidjs/meta";
|
||||
import { createAsync } from "@solidjs/router";
|
||||
import { getRequestEvent } from "solid-js/web";
|
||||
import { getPrivilegeLevel, getUserID } from "~/server/utils";
|
||||
import { api } from "~/lib/api";
|
||||
import Dropzone from "~/components/blog/Dropzone";
|
||||
import TextEditor from "~/components/blog/TextEditor";
|
||||
import TagMaker from "~/components/blog/TagMaker";
|
||||
import AddAttachmentSection from "~/components/blog/AddAttachmentSection";
|
||||
import XCircle from "~/components/icons/XCircle";
|
||||
import AddImageToS3 from "~/lib/s3upload";
|
||||
|
||||
const getAuthState = query(async () => {
|
||||
"use server";
|
||||
@@ -17,19 +23,124 @@ const getAuthState = query(async () => {
|
||||
}, "auth-state");
|
||||
|
||||
export default function CreatePost() {
|
||||
const [searchParams] = useSearchParams();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const authState = createAsync(() => getAuthState());
|
||||
|
||||
const [title, setTitle] = createSignal("");
|
||||
const [subtitle, setSubtitle] = createSignal("");
|
||||
const [body, setBody] = createSignal("");
|
||||
const [bannerPhoto, setBannerPhoto] = createSignal("");
|
||||
const [bannerPhoto, setBannerPhoto] = createSignal<string>("");
|
||||
const [bannerImageFile, setBannerImageFile] = createSignal<File>();
|
||||
const [bannerImageHolder, setBannerImageHolder] = createSignal<
|
||||
string | ArrayBuffer | null
|
||||
>(null);
|
||||
const [published, setPublished] = createSignal(false);
|
||||
const [tags, setTags] = createSignal<string[]>([]);
|
||||
const [tagInputValue, setTagInputValue] = createSignal("");
|
||||
const [loading, setLoading] = createSignal(false);
|
||||
const [error, setError] = createSignal("");
|
||||
const [showAutoSaveMessage, setShowAutoSaveMessage] = createSignal(false);
|
||||
const [hasSaved, setHasSaved] = createSignal(false);
|
||||
|
||||
let autosaveInterval: number | undefined;
|
||||
|
||||
const autoSave = async () => {
|
||||
const titleVal = title();
|
||||
const bodyVal = body();
|
||||
|
||||
if (titleVal && bodyVal !== "") {
|
||||
try {
|
||||
let bannerImageKey = "";
|
||||
const bannerFile = bannerImageFile();
|
||||
if (bannerFile) {
|
||||
bannerImageKey = (await AddImageToS3(
|
||||
bannerFile,
|
||||
titleVal,
|
||||
"blog"
|
||||
)) as string;
|
||||
}
|
||||
|
||||
const method = hasSaved() ? "PATCH" : "POST";
|
||||
|
||||
if (method === "POST") {
|
||||
await api.database.createPost.mutate({
|
||||
category: "blog",
|
||||
title: titleVal.replaceAll(" ", "_"),
|
||||
subtitle: subtitle() || null,
|
||||
body: bodyVal || null,
|
||||
banner_photo: bannerImageKey !== "" ? bannerImageKey : null,
|
||||
published: published(),
|
||||
tags: tags().length > 0 ? tags() : null,
|
||||
author_id: authState()!.userID
|
||||
});
|
||||
}
|
||||
|
||||
setHasSaved(true);
|
||||
showAutoSaveTrigger();
|
||||
} catch (err) {
|
||||
console.error("Autosave failed:", err);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const showAutoSaveTrigger = () => {
|
||||
setShowAutoSaveMessage(true);
|
||||
setTimeout(() => {
|
||||
setShowAutoSaveMessage(false);
|
||||
}, 5000);
|
||||
};
|
||||
|
||||
// Set up autosave interval (2 minutes)
|
||||
autosaveInterval = setInterval(
|
||||
() => {
|
||||
autoSave();
|
||||
},
|
||||
2 * 60 * 1000
|
||||
) as unknown as number;
|
||||
|
||||
onCleanup(() => {
|
||||
if (autosaveInterval) {
|
||||
clearInterval(autosaveInterval);
|
||||
}
|
||||
});
|
||||
|
||||
const handleBannerImageDrop = (acceptedFiles: File[]) => {
|
||||
if (acceptedFiles.length > 0) {
|
||||
const file = acceptedFiles[0];
|
||||
setBannerImageFile(file);
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => {
|
||||
setBannerImageHolder(reader.result);
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
}
|
||||
};
|
||||
|
||||
const removeBannerImage = () => {
|
||||
setBannerImageFile(undefined);
|
||||
setBannerImageHolder(null);
|
||||
};
|
||||
|
||||
const tagHandler = (input: string) => {
|
||||
const split = input.split(" ");
|
||||
if (split.length > 1) {
|
||||
const newSplit: string[] = [];
|
||||
split.forEach((word) => {
|
||||
if (word[0] === "#" && word.length > 1) {
|
||||
setTags((prevTags) => [...prevTags, word]);
|
||||
} else {
|
||||
newSplit.push(word);
|
||||
}
|
||||
});
|
||||
setTagInputValue(newSplit.join(" "));
|
||||
} else {
|
||||
setTagInputValue(input);
|
||||
}
|
||||
};
|
||||
|
||||
const deleteTag = (idx: number) => {
|
||||
setTags((prevTags) => prevTags.filter((_, index) => index !== idx));
|
||||
};
|
||||
|
||||
const handleSubmit = async (e: Event) => {
|
||||
e.preventDefault();
|
||||
@@ -43,20 +154,29 @@ export default function CreatePost() {
|
||||
setError("");
|
||||
|
||||
try {
|
||||
let bannerImageKey = "";
|
||||
const bannerFile = bannerImageFile();
|
||||
if (bannerFile) {
|
||||
bannerImageKey = (await AddImageToS3(
|
||||
bannerFile,
|
||||
title(),
|
||||
"blog"
|
||||
)) as string;
|
||||
}
|
||||
|
||||
const result = await api.database.createPost.mutate({
|
||||
category: "blog",
|
||||
title: title(),
|
||||
title: title().replaceAll(" ", "_"),
|
||||
subtitle: subtitle() || null,
|
||||
body: body() || null,
|
||||
banner_photo: bannerPhoto() || null,
|
||||
banner_photo: bannerImageKey !== "" ? bannerImageKey : null,
|
||||
published: published(),
|
||||
tags: tags().length > 0 ? tags() : null,
|
||||
author_id: authState()!.userID
|
||||
});
|
||||
|
||||
if (result.data) {
|
||||
// Redirect to the new post
|
||||
navigate(`/blog/${encodeURIComponent(title())}`);
|
||||
navigate(`/blog/${encodeURIComponent(title().replaceAll(" ", "_"))}`);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Error creating post:", err);
|
||||
@@ -81,108 +201,100 @@ export default function CreatePost() {
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div class="bg-base min-h-screen px-4 py-12">
|
||||
<div class="mx-auto max-w-4xl">
|
||||
<h1 class="mb-8 text-center text-4xl font-bold">
|
||||
Create Blog Post
|
||||
</h1>
|
||||
|
||||
<form onSubmit={handleSubmit} class="space-y-6">
|
||||
<div class="bg-base text-text min-h-screen px-8 py-32">
|
||||
<div class="text-center text-2xl tracking-wide">Create a Blog</div>
|
||||
<div class="flex h-full w-full justify-center">
|
||||
<form
|
||||
onSubmit={handleSubmit}
|
||||
class="w-full md:w-3/4 lg:w-1/3 xl:w-1/2"
|
||||
>
|
||||
{/* Title */}
|
||||
<div>
|
||||
<label for="title" class="mb-2 block text-sm font-medium">
|
||||
Title *
|
||||
</label>
|
||||
<div class="input-group mx-4">
|
||||
<input
|
||||
id="title"
|
||||
type="text"
|
||||
required
|
||||
value={title()}
|
||||
onInput={(e) => setTitle(e.currentTarget.value)}
|
||||
class="border-surface2 bg-surface0 w-full rounded-md border px-4 py-2"
|
||||
placeholder="Enter post title"
|
||||
required
|
||||
name="title"
|
||||
placeholder=" "
|
||||
class="underlinedInput w-full bg-transparent"
|
||||
/>
|
||||
<span class="bar"></span>
|
||||
<label class="underlinedInputLabel">Title</label>
|
||||
</div>
|
||||
|
||||
{/* Subtitle */}
|
||||
<div>
|
||||
<label for="subtitle" class="mb-2 block text-sm font-medium">
|
||||
Subtitle
|
||||
</label>
|
||||
<div class="input-group mx-4">
|
||||
<input
|
||||
id="subtitle"
|
||||
type="text"
|
||||
value={subtitle()}
|
||||
onInput={(e) => setSubtitle(e.currentTarget.value)}
|
||||
class="border-surface2 bg-surface0 w-full rounded-md border px-4 py-2"
|
||||
placeholder="Enter post subtitle"
|
||||
name="subtitle"
|
||||
placeholder=" "
|
||||
class="underlinedInput w-full bg-transparent"
|
||||
/>
|
||||
<span class="bar"></span>
|
||||
<label class="underlinedInputLabel">Subtitle</label>
|
||||
</div>
|
||||
|
||||
{/* Body */}
|
||||
<div>
|
||||
<label for="body" class="mb-2 block text-sm font-medium">
|
||||
Body (HTML)
|
||||
</label>
|
||||
<textarea
|
||||
id="body"
|
||||
rows={15}
|
||||
value={body()}
|
||||
onInput={(e) => setBody(e.currentTarget.value)}
|
||||
class="border-surface2 bg-surface0 w-full rounded-md border px-4 py-2 font-mono text-sm"
|
||||
placeholder="Enter post content (HTML)"
|
||||
{/* Banner */}
|
||||
<div class="pt-8 text-center text-xl">Banner</div>
|
||||
<div class="flex justify-center pb-8">
|
||||
<Dropzone
|
||||
onDrop={handleBannerImageDrop}
|
||||
accept="image/jpg, image/jpeg, image/png"
|
||||
fileHolder={bannerImageHolder()}
|
||||
preSet={null}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
class="z-50 -ml-6 h-fit rounded-full"
|
||||
onClick={removeBannerImage}
|
||||
>
|
||||
<XCircle
|
||||
height={36}
|
||||
width={36}
|
||||
stroke={"currentColor"}
|
||||
strokeWidth={1}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Banner Photo URL */}
|
||||
<div>
|
||||
<label for="banner" class="mb-2 block text-sm font-medium">
|
||||
Banner Photo URL
|
||||
</label>
|
||||
<input
|
||||
id="banner"
|
||||
type="text"
|
||||
value={bannerPhoto()}
|
||||
onInput={(e) => setBannerPhoto(e.currentTarget.value)}
|
||||
class="border-surface2 bg-surface0 w-full rounded-md border px-4 py-2"
|
||||
placeholder="Enter banner photo URL"
|
||||
/>
|
||||
{/* Attachments */}
|
||||
<AddAttachmentSection type="blog" postTitle={title()} />
|
||||
|
||||
{/* Text Editor */}
|
||||
<div class="md:-mx-36">
|
||||
<TextEditor updateContent={setBody} preSet={undefined} />
|
||||
</div>
|
||||
|
||||
{/* Tags */}
|
||||
<div>
|
||||
<label for="tags" class="mb-2 block text-sm font-medium">
|
||||
Tags (comma-separated)
|
||||
</label>
|
||||
<input
|
||||
id="tags"
|
||||
type="text"
|
||||
value={tags().join(", ")}
|
||||
onInput={(e) =>
|
||||
setTags(
|
||||
e.currentTarget.value
|
||||
.split(",")
|
||||
.map((t) => t.trim())
|
||||
.filter(Boolean)
|
||||
)
|
||||
}
|
||||
class="border-surface2 bg-surface0 w-full rounded-md border px-4 py-2"
|
||||
placeholder="tag1, tag2, tag3"
|
||||
/>
|
||||
<TagMaker
|
||||
tagInputValue={tagInputValue()}
|
||||
tagHandler={tagHandler}
|
||||
tags={tags()}
|
||||
deleteTag={deleteTag}
|
||||
/>
|
||||
|
||||
{/* Auto-save message */}
|
||||
<div
|
||||
class={`${
|
||||
showAutoSaveMessage() ? "" : "user-select opacity-0"
|
||||
} text-green flex min-h-[16px] justify-center text-center italic transition-opacity duration-500 ease-in-out`}
|
||||
>
|
||||
{showAutoSaveMessage() ? "Auto save success!" : ""}
|
||||
</div>
|
||||
|
||||
{/* Published */}
|
||||
<div class="flex items-center gap-2">
|
||||
{/* Publish checkbox */}
|
||||
<div class="flex justify-end pt-4 pb-2">
|
||||
<input
|
||||
id="published"
|
||||
type="checkbox"
|
||||
class="my-auto"
|
||||
name="publish"
|
||||
checked={published()}
|
||||
onChange={(e) => setPublished(e.currentTarget.checked)}
|
||||
class="h-4 w-4"
|
||||
/>
|
||||
<label for="published" class="text-sm font-medium">
|
||||
Publish immediately
|
||||
</label>
|
||||
<div class="my-auto px-2 text-sm font-normal">Publish</div>
|
||||
</div>
|
||||
|
||||
{/* Error message */}
|
||||
@@ -191,25 +303,23 @@ export default function CreatePost() {
|
||||
</Show>
|
||||
|
||||
{/* Submit button */}
|
||||
<div class="flex gap-4">
|
||||
<div class="flex justify-end">
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading()}
|
||||
class={`flex-1 rounded-md px-6 py-3 text-base transition-all ${
|
||||
class={`${
|
||||
loading()
|
||||
? "bg-blue cursor-not-allowed brightness-50"
|
||||
: "bg-blue hover:brightness-125 active:scale-95"
|
||||
}`}
|
||||
? "bg-surface2 cursor-not-allowed"
|
||||
: published()
|
||||
? "bg-peach hover:brightness-125"
|
||||
: "bg-green hover:brightness-125"
|
||||
} text-crust flex w-36 justify-center rounded py-3 transition-all duration-300 ease-out active:scale-90`}
|
||||
>
|
||||
{loading() ? "Creating..." : "Create Post"}
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => navigate("/blog")}
|
||||
class="border-surface2 rounded-md border px-6 py-3 transition-all hover:brightness-125"
|
||||
>
|
||||
Cancel
|
||||
{loading()
|
||||
? "Loading..."
|
||||
: published()
|
||||
? "Publish!"
|
||||
: "Save as Draft"}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Show, createSignal, createEffect } from "solid-js";
|
||||
import { Show, createSignal, createEffect, onCleanup } from "solid-js";
|
||||
import { useParams, useNavigate, query } from "@solidjs/router";
|
||||
import { Title } from "@solidjs/meta";
|
||||
import { createAsync } from "@solidjs/router";
|
||||
@@ -6,8 +6,13 @@ import { getRequestEvent } from "solid-js/web";
|
||||
import { getPrivilegeLevel, getUserID } from "~/server/utils";
|
||||
import { api } from "~/lib/api";
|
||||
import { ConnectionFactory } from "~/server/utils";
|
||||
import Dropzone from "~/components/blog/Dropzone";
|
||||
import TextEditor from "~/components/blog/TextEditor";
|
||||
import TagMaker from "~/components/blog/TagMaker";
|
||||
import AddAttachmentSection from "~/components/blog/AddAttachmentSection";
|
||||
import XCircle from "~/components/icons/XCircle";
|
||||
import AddImageToS3 from "~/lib/s3upload";
|
||||
|
||||
// Server function to fetch post for editing
|
||||
const getPostForEdit = query(async (id: string) => {
|
||||
"use server";
|
||||
|
||||
@@ -44,17 +49,26 @@ export default function EditPost() {
|
||||
const [subtitle, setSubtitle] = createSignal("");
|
||||
const [body, setBody] = createSignal("");
|
||||
const [bannerPhoto, setBannerPhoto] = createSignal("");
|
||||
const [bannerImageFile, setBannerImageFile] = createSignal<File>();
|
||||
const [bannerImageHolder, setBannerImageHolder] = createSignal<
|
||||
string | ArrayBuffer | null
|
||||
>(null);
|
||||
const [requestedDeleteImage, setRequestedDeleteImage] = createSignal(false);
|
||||
const [published, setPublished] = createSignal(false);
|
||||
const [tags, setTags] = createSignal<string[]>([]);
|
||||
const [tagInputValue, setTagInputValue] = createSignal("");
|
||||
const [loading, setLoading] = createSignal(false);
|
||||
const [error, setError] = createSignal("");
|
||||
const [showAutoSaveMessage, setShowAutoSaveMessage] = createSignal(false);
|
||||
|
||||
let autosaveInterval: number | undefined;
|
||||
|
||||
// Populate form when data loads
|
||||
createEffect(() => {
|
||||
const postData = data();
|
||||
if (postData?.post) {
|
||||
const p = postData.post as any;
|
||||
setTitle(p.title || "");
|
||||
setTitle(p.title?.replaceAll("_", " ") || "");
|
||||
setSubtitle(p.subtitle || "");
|
||||
setBody(p.body || "");
|
||||
setBannerPhoto(p.banner_photo || "");
|
||||
@@ -67,6 +81,106 @@ export default function EditPost() {
|
||||
}
|
||||
});
|
||||
|
||||
const autoSave = async () => {
|
||||
const titleVal = title();
|
||||
const postData = data();
|
||||
|
||||
if (titleVal && postData?.post) {
|
||||
try {
|
||||
let bannerImageKey = "";
|
||||
const bannerFile = bannerImageFile();
|
||||
if (bannerFile) {
|
||||
bannerImageKey = (await AddImageToS3(
|
||||
bannerFile,
|
||||
titleVal,
|
||||
"blog"
|
||||
)) as string;
|
||||
}
|
||||
|
||||
await api.database.updatePost.mutate({
|
||||
id: parseInt(params.id),
|
||||
title: titleVal.replaceAll(" ", "_"),
|
||||
subtitle: subtitle() || null,
|
||||
body: body() || null,
|
||||
banner_photo:
|
||||
bannerImageKey !== ""
|
||||
? bannerImageKey
|
||||
: requestedDeleteImage()
|
||||
? "_DELETE_IMAGE_"
|
||||
: null,
|
||||
published: published(),
|
||||
tags: tags().length > 0 ? tags() : null,
|
||||
author_id: data()!.userID
|
||||
});
|
||||
|
||||
showAutoSaveTrigger();
|
||||
} catch (err) {
|
||||
console.error("Autosave failed:", err);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const showAutoSaveTrigger = () => {
|
||||
setShowAutoSaveMessage(true);
|
||||
setTimeout(() => {
|
||||
setShowAutoSaveMessage(false);
|
||||
}, 5000);
|
||||
};
|
||||
|
||||
// Set up autosave interval (2 minutes)
|
||||
autosaveInterval = setInterval(
|
||||
() => {
|
||||
autoSave();
|
||||
},
|
||||
2 * 60 * 1000
|
||||
) as unknown as number;
|
||||
|
||||
onCleanup(() => {
|
||||
if (autosaveInterval) {
|
||||
clearInterval(autosaveInterval);
|
||||
}
|
||||
});
|
||||
|
||||
const handleBannerImageDrop = (acceptedFiles: File[]) => {
|
||||
if (acceptedFiles.length > 0) {
|
||||
const file = acceptedFiles[0];
|
||||
setRequestedDeleteImage(false);
|
||||
setBannerImageFile(file);
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => {
|
||||
setBannerImageHolder(reader.result);
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
}
|
||||
};
|
||||
|
||||
const removeBannerImage = () => {
|
||||
setBannerImageFile(undefined);
|
||||
setBannerImageHolder(null);
|
||||
setRequestedDeleteImage(true);
|
||||
};
|
||||
|
||||
const tagHandler = (input: string) => {
|
||||
const split = input.split(" ");
|
||||
if (split.length > 1) {
|
||||
const newSplit: string[] = [];
|
||||
split.forEach((word) => {
|
||||
if (word[0] === "#" && word.length > 1) {
|
||||
setTags((prevTags) => [...prevTags, word]);
|
||||
} else {
|
||||
newSplit.push(word);
|
||||
}
|
||||
});
|
||||
setTagInputValue(newSplit.join(" "));
|
||||
} else {
|
||||
setTagInputValue(input);
|
||||
}
|
||||
};
|
||||
|
||||
const deleteTag = (idx: number) => {
|
||||
setTags((prevTags) => prevTags.filter((_, index) => index !== idx));
|
||||
};
|
||||
|
||||
const handleSubmit = async (e: Event) => {
|
||||
e.preventDefault();
|
||||
|
||||
@@ -79,19 +193,33 @@ export default function EditPost() {
|
||||
setError("");
|
||||
|
||||
try {
|
||||
let bannerImageKey = "";
|
||||
const bannerFile = bannerImageFile();
|
||||
if (bannerFile) {
|
||||
bannerImageKey = (await AddImageToS3(
|
||||
bannerFile,
|
||||
title(),
|
||||
"blog"
|
||||
)) as string;
|
||||
}
|
||||
|
||||
await api.database.updatePost.mutate({
|
||||
id: parseInt(params.id),
|
||||
title: title(),
|
||||
title: title().replaceAll(" ", "_"),
|
||||
subtitle: subtitle() || null,
|
||||
body: body() || null,
|
||||
banner_photo: bannerPhoto() || null,
|
||||
banner_photo:
|
||||
bannerImageKey !== ""
|
||||
? bannerImageKey
|
||||
: requestedDeleteImage()
|
||||
? "_DELETE_IMAGE_"
|
||||
: null,
|
||||
published: published(),
|
||||
tags: tags().length > 0 ? tags() : null,
|
||||
author_id: data()!.userID
|
||||
});
|
||||
|
||||
// Redirect to the post
|
||||
navigate(`/blog/${encodeURIComponent(title())}`);
|
||||
navigate(`/blog/${encodeURIComponent(title().replaceAll(" ", "_"))}`);
|
||||
} catch (err) {
|
||||
console.error("Error updating post:", err);
|
||||
setError("Failed to update post. Please try again.");
|
||||
@@ -108,7 +236,7 @@ export default function EditPost() {
|
||||
when={data()?.privilegeLevel === "admin"}
|
||||
fallback={
|
||||
<div class="w-full pt-[30vh] text-center">
|
||||
<div class="text-2xl">Unauthorized</div>
|
||||
<div class="text-text text-2xl">Unauthorized</div>
|
||||
<div class="text-subtext0 mt-4">
|
||||
You must be an admin to edit posts.
|
||||
</div>
|
||||
@@ -119,110 +247,113 @@ export default function EditPost() {
|
||||
when={data()}
|
||||
fallback={
|
||||
<div class="w-full pt-[30vh] text-center">
|
||||
<div class="text-xl">Loading post...</div>
|
||||
<div class="text-text text-xl">Loading post...</div>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div class="bg-base min-h-screen px-4 py-12">
|
||||
<div class="mx-auto max-w-4xl">
|
||||
<h1 class="mb-8 text-center text-4xl font-bold">Edit Post</h1>
|
||||
|
||||
<form onSubmit={handleSubmit} class="space-y-6">
|
||||
<div class="bg-base text-text min-h-screen px-8 py-32">
|
||||
<div class="text-center text-2xl tracking-wide">Edit a Blog</div>
|
||||
<div class="flex h-full w-full justify-center">
|
||||
<form
|
||||
onSubmit={handleSubmit}
|
||||
class="w-full md:w-3/4 lg:w-1/3 xl:w-1/2"
|
||||
>
|
||||
{/* Title */}
|
||||
<div>
|
||||
<label for="title" class="mb-2 block text-sm font-medium">
|
||||
Title *
|
||||
</label>
|
||||
<div class="input-group mx-4">
|
||||
<input
|
||||
id="title"
|
||||
type="text"
|
||||
required
|
||||
value={title()}
|
||||
onInput={(e) => setTitle(e.currentTarget.value)}
|
||||
class="border-surface2 bg-surface0 w-full rounded-md border px-4 py-2"
|
||||
placeholder="Enter post title"
|
||||
required
|
||||
name="title"
|
||||
placeholder=" "
|
||||
class="underlinedInput w-full bg-transparent"
|
||||
/>
|
||||
<span class="bar"></span>
|
||||
<label class="underlinedInputLabel">Title</label>
|
||||
</div>
|
||||
|
||||
{/* Subtitle */}
|
||||
<div>
|
||||
<label for="subtitle" class="mb-2 block text-sm font-medium">
|
||||
Subtitle
|
||||
</label>
|
||||
<div class="input-group mx-4">
|
||||
<input
|
||||
id="subtitle"
|
||||
type="text"
|
||||
value={subtitle()}
|
||||
onInput={(e) => setSubtitle(e.currentTarget.value)}
|
||||
class="border-surface2 bg-surface0 w-full rounded-md border px-4 py-2"
|
||||
placeholder="Enter post subtitle"
|
||||
name="subtitle"
|
||||
placeholder=" "
|
||||
class="underlinedInput w-full bg-transparent"
|
||||
/>
|
||||
<span class="bar"></span>
|
||||
<label class="underlinedInputLabel">Subtitle</label>
|
||||
</div>
|
||||
|
||||
{/* Body */}
|
||||
<div>
|
||||
<label for="body" class="mb-2 block text-sm font-medium">
|
||||
Body (HTML)
|
||||
</label>
|
||||
<textarea
|
||||
id="body"
|
||||
rows={15}
|
||||
value={body()}
|
||||
onInput={(e) => setBody(e.currentTarget.value)}
|
||||
class="border-surface2 bg-surface0 w-full rounded-md border px-4 py-2 font-mono text-sm"
|
||||
placeholder="Enter post content (HTML)"
|
||||
{/* Banner */}
|
||||
<div class="pt-8 text-center text-xl">Banner</div>
|
||||
<div class="flex justify-center pb-8">
|
||||
<Dropzone
|
||||
onDrop={handleBannerImageDrop}
|
||||
accept="image/jpg, image/jpeg, image/png"
|
||||
fileHolder={bannerImageHolder()}
|
||||
preSet={
|
||||
requestedDeleteImage() ? null : bannerPhoto() || null
|
||||
}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
class="z-50 -ml-6 h-fit rounded-full"
|
||||
onClick={removeBannerImage}
|
||||
>
|
||||
<XCircle
|
||||
height={36}
|
||||
width={36}
|
||||
stroke={"currentColor"}
|
||||
strokeWidth={1}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Banner Photo URL */}
|
||||
<div>
|
||||
<label for="banner" class="mb-2 block text-sm font-medium">
|
||||
Banner Photo URL
|
||||
</label>
|
||||
<input
|
||||
id="banner"
|
||||
type="text"
|
||||
value={bannerPhoto()}
|
||||
onInput={(e) => setBannerPhoto(e.currentTarget.value)}
|
||||
class="border-surface2 bg-surface0 w-full rounded-md border px-4 py-2"
|
||||
placeholder="Enter banner photo URL"
|
||||
/>
|
||||
{/* Attachments */}
|
||||
<AddAttachmentSection
|
||||
type="blog"
|
||||
postId={parseInt(params.id)}
|
||||
postTitle={title()}
|
||||
existingAttachments={
|
||||
(data()?.post as any)?.attachments || undefined
|
||||
}
|
||||
/>
|
||||
|
||||
{/* Text Editor */}
|
||||
<div class="-mx-6 md:-mx-36">
|
||||
<TextEditor updateContent={setBody} preSet={body()} />
|
||||
</div>
|
||||
|
||||
{/* Tags */}
|
||||
<div>
|
||||
<label for="tags" class="mb-2 block text-sm font-medium">
|
||||
Tags (comma-separated)
|
||||
</label>
|
||||
<input
|
||||
id="tags"
|
||||
type="text"
|
||||
value={tags().join(", ")}
|
||||
onInput={(e) =>
|
||||
setTags(
|
||||
e.currentTarget.value
|
||||
.split(",")
|
||||
.map((t) => t.trim())
|
||||
.filter(Boolean)
|
||||
)
|
||||
}
|
||||
class="border-surface2 bg-surface0 w-full rounded-md border px-4 py-2"
|
||||
placeholder="tag1, tag2, tag3"
|
||||
/>
|
||||
<TagMaker
|
||||
tagInputValue={tagInputValue()}
|
||||
tagHandler={tagHandler}
|
||||
tags={tags()}
|
||||
deleteTag={deleteTag}
|
||||
/>
|
||||
|
||||
{/* Auto-save message */}
|
||||
<div
|
||||
class={`${
|
||||
showAutoSaveMessage() ? "" : "user-select opacity-0"
|
||||
} text-green flex min-h-[16px] justify-center text-center italic transition-opacity duration-500 ease-in-out`}
|
||||
>
|
||||
{showAutoSaveMessage() ? "Auto save success!" : ""}
|
||||
</div>
|
||||
|
||||
{/* Published */}
|
||||
<div class="flex items-center gap-2">
|
||||
{/* Publish checkbox */}
|
||||
<div class="flex justify-end pt-4 pb-2">
|
||||
<input
|
||||
id="published"
|
||||
type="checkbox"
|
||||
class="my-auto"
|
||||
name="publish"
|
||||
checked={published()}
|
||||
onChange={(e) => setPublished(e.currentTarget.checked)}
|
||||
class="h-4 w-4"
|
||||
/>
|
||||
<label for="published" class="text-sm font-medium">
|
||||
Published
|
||||
</label>
|
||||
<div class="my-auto px-2 text-sm font-normal">Published</div>
|
||||
</div>
|
||||
|
||||
{/* Error message */}
|
||||
@@ -231,31 +362,35 @@ export default function EditPost() {
|
||||
</Show>
|
||||
|
||||
{/* Submit button */}
|
||||
<div class="flex gap-4">
|
||||
<div class="flex justify-end">
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading()}
|
||||
class={`flex-1 rounded-md px-6 py-3 text-base transition-all ${
|
||||
class={`${
|
||||
loading()
|
||||
? "bg-blue cursor-not-allowed brightness-50"
|
||||
: "bg-blue hover:brightness-125 active:scale-95"
|
||||
}`}
|
||||
? "bg-surface2 cursor-not-allowed"
|
||||
: published()
|
||||
? "bg-peach hover:brightness-125"
|
||||
: "bg-green hover:brightness-125"
|
||||
} text-crust flex w-36 justify-center rounded py-3 transition-all duration-300 ease-out active:scale-90`}
|
||||
>
|
||||
{loading() ? "Saving..." : "Save Changes"}
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
navigate(`/blog/${encodeURIComponent(title())}`)
|
||||
}
|
||||
class="border-surface2 rounded-md border px-6 py-3 transition-all hover:brightness-125"
|
||||
>
|
||||
Cancel
|
||||
{loading()
|
||||
? "Loading..."
|
||||
: published()
|
||||
? "Publish!"
|
||||
: "Save as Draft"}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="mt-2 flex justify-center">
|
||||
<a
|
||||
href={`/blog/${encodeURIComponent(title().replaceAll(" ", "_"))}`}
|
||||
class="border-blue bg-blue hover:bg-blue rounded border px-4 py-2 shadow-md transition-all duration-300 ease-in-out hover:brightness-125 active:scale-90"
|
||||
>
|
||||
Go to Post
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
</Show>
|
||||
|
||||
Reference in New Issue
Block a user