From 268841fb4dbdbc1a07532297c6177679d5e3067c Mon Sep 17 00:00:00 2001 From: Michael Freno Date: Sat, 20 Dec 2025 23:18:05 -0500 Subject: [PATCH] consolidate most of post edit/create --- src/app.css | 3 +- src/components/blog/PostForm.tsx | 404 ++++++++++++++++++++++++++++ src/components/blog/TextEditor.tsx | 7 +- src/routes/account.tsx | 10 +- src/routes/blog/create/index.tsx | 307 +-------------------- src/routes/blog/edit/[id]/index.tsx | 376 ++------------------------ 6 files changed, 444 insertions(+), 663 deletions(-) create mode 100644 src/components/blog/PostForm.tsx diff --git a/src/app.css b/src/app.css index 6f197ed..51c505b 100644 --- a/src/app.css +++ b/src/app.css @@ -628,6 +628,7 @@ a.hover-underline-animation:hover::after { ul, ol { padding: 0 1rem; + margin-left: 2rem; } h1, @@ -654,7 +655,7 @@ a.hover-underline-animation:hover::after { hr { border: none; - border-top: 2px solid rgba(#0d0d0d, 0.1); + border-top: 2px solid var(--color-text); margin: 2rem 0; } diff --git a/src/components/blog/PostForm.tsx b/src/components/blog/PostForm.tsx new file mode 100644 index 0000000..8ccae99 --- /dev/null +++ b/src/components/blog/PostForm.tsx @@ -0,0 +1,404 @@ +import { Show, createSignal, createEffect, onCleanup } from "solid-js"; +import { useNavigate } from "@solidjs/router"; +import { api } from "~/lib/api"; +import { debounce } from "es-toolkit"; +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"; + +interface PostFormProps { + mode: "create" | "edit"; + postId?: number; + initialData?: { + title: string; + subtitle: string; + body: string; + banner_photo: string; + published: boolean; + tags: string[]; + }; + userID: number; +} + +export default function PostForm(props: PostFormProps) { + const navigate = useNavigate(); + + const [title, setTitle] = createSignal(props.initialData?.title || ""); + const [subtitle, setSubtitle] = createSignal( + props.initialData?.subtitle || "" + ); + const [body, setBody] = createSignal(props.initialData?.body || ""); + const [bannerPhoto, setBannerPhoto] = createSignal( + props.initialData?.banner_photo || "" + ); + const [bannerImageFile, setBannerImageFile] = createSignal(); + const [bannerImageHolder, setBannerImageHolder] = createSignal< + string | ArrayBuffer | null + >(null); + const [requestedDeleteImage, setRequestedDeleteImage] = createSignal(false); + const [published, setPublished] = createSignal( + props.initialData?.published || false + ); + const [tags, setTags] = createSignal(props.initialData?.tags || []); + const [tagInputValue, setTagInputValue] = createSignal(""); + const [loading, setLoading] = createSignal(false); + const [error, setError] = createSignal(""); + const [showAutoSaveMessage, setShowAutoSaveMessage] = createSignal(false); + const [isInitialLoad, setIsInitialLoad] = createSignal(props.mode === "edit"); + const [initialBody, setInitialBody] = createSignal( + props.initialData?.body + ); + const [hasSaved, setHasSaved] = createSignal(props.mode === "edit"); + + // Mark initial load as complete after data is loaded (for edit mode) + createEffect(() => { + if (props.mode === "edit" && props.initialData) { + setIsInitialLoad(false); + } + }); + + const autoSave = async () => { + const titleVal = title(); + + if (titleVal) { + try { + let bannerImageKey = ""; + const bannerFile = bannerImageFile(); + if (bannerFile) { + bannerImageKey = (await AddImageToS3( + bannerFile, + titleVal, + "blog" + )) as string; + } + + if (props.mode === "edit") { + await api.database.updatePost.mutate({ + id: props.postId!, + 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: props.userID + }); + } else { + // Create mode: only save once + if (!hasSaved()) { + await api.database.createPost.mutate({ + category: "blog", + title: titleVal.replaceAll(" ", "_"), + subtitle: subtitle() || null, + body: body() || null, + banner_photo: bannerImageKey !== "" ? bannerImageKey : null, + published: published(), + tags: tags().length > 0 ? tags() : null, + author_id: props.userID + }); + setHasSaved(true); + } + } + + showAutoSaveTrigger(); + } catch (err) { + console.error("Autosave failed:", err); + } + } + }; + + const showAutoSaveTrigger = () => { + setShowAutoSaveMessage(true); + setTimeout(() => { + setShowAutoSaveMessage(false); + }, 5000); + }; + + // Debounced auto-save (1 second after last change) + const debouncedAutoSave = debounce(autoSave, 1000); + + // Track changes to trigger auto-save + createEffect(() => { + const titleVal = title(); + const subtitleVal = subtitle(); + const bodyVal = body(); + const tagsVal = tags(); + const publishedVal = published(); + + // Only trigger auto-save if conditions are met + if (props.mode === "edit" && !isInitialLoad() && titleVal) { + debouncedAutoSave(); + } else if (props.mode === "create" && titleVal) { + debouncedAutoSave(); + } + }); + + onCleanup(() => { + debouncedAutoSave.cancel(); + }); + + const handleBannerImageDrop = (acceptedFiles: File[]) => { + if (acceptedFiles.length > 0) { + const file = acceptedFiles[0]; + if (props.mode === "edit") { + setRequestedDeleteImage(false); + } + setBannerImageFile(file); + const reader = new FileReader(); + reader.onload = () => { + setBannerImageHolder(reader.result); + }; + reader.readAsDataURL(file); + } + }; + + const removeBannerImage = () => { + setBannerImageFile(undefined); + setBannerImageHolder(null); + if (props.mode === "edit") { + 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(); + + if (!props.userID) { + setError(`You must be logged in to ${props.mode} posts`); + return; + } + + if (!title()) { + setError("Title is required to publish"); + return; + } + + setLoading(true); + setError(""); + + try { + let bannerImageKey = ""; + const bannerFile = bannerImageFile(); + if (bannerFile) { + bannerImageKey = (await AddImageToS3( + bannerFile, + title(), + "blog" + )) as string; + } + + if (props.mode === "edit") { + await api.database.updatePost.mutate({ + id: props.postId!, + title: title().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: props.userID + }); + } else { + await api.database.createPost.mutate({ + category: "blog", + title: title().replaceAll(" ", "_"), + subtitle: subtitle() || null, + body: body() || null, + banner_photo: bannerImageKey !== "" ? bannerImageKey : null, + published: published(), + tags: tags().length > 0 ? tags() : null, + author_id: props.userID + }); + } + + navigate(`/blog/${encodeURIComponent(title().replaceAll(" ", "_"))}`); + } catch (err) { + console.error(`Error ${props.mode}ing post:`, err); + setError(`Failed to ${props.mode} post. Please try again.`); + } finally { + setLoading(false); + } + }; + + return ( +
+
+ {props.mode === "edit" ? "Edit a Blog" : "Create a Blog"} +
+
+
+ {/* Title */} +
+ setTitle(e.currentTarget.value)} + name="title" + placeholder=" " + class="underlinedInput w-full bg-transparent" + /> + + +
+ + {/* Subtitle */} +
+ setSubtitle(e.currentTarget.value)} + name="subtitle" + placeholder=" " + class="underlinedInput w-full bg-transparent" + /> + + +
+ + {/* Banner */} +
Banner
+
+ + +
+ + {/* Attachments */} + + + {/* Text Editor */} +
+ +
+ + {/* Tags */} + + + {/* Auto-save message */} +
+ {showAutoSaveMessage() ? "Auto save success!" : ""} +
+ + {/* Publish checkbox */} +
+ setPublished(e.currentTarget.checked)} + /> +
Published
+
+ + {/* Error message */} + +
{error()}
+
+ + {/* Submit button */} +
+ +
+ +
+ + + +
+ ); +} diff --git a/src/components/blog/TextEditor.tsx b/src/components/blog/TextEditor.tsx index 9dccbaf..fabd582 100644 --- a/src/components/blog/TextEditor.tsx +++ b/src/components/blog/TextEditor.tsx @@ -117,6 +117,11 @@ export default function TextEditor(props: TextEditorProps) { IframeEmbed ], content: props.preSet || `

Hello! World

`, + editorProps: { + attributes: { + class: "focus:outline-none" + } + }, onUpdate: ({ editor }) => { untrack(() => { props.updateContent(editor.getHTML()); @@ -497,7 +502,7 @@ export default function TextEditor(props: TextEditorProps) {
); diff --git a/src/routes/account.tsx b/src/routes/account.tsx index d5ab908..5355bc6 100644 --- a/src/routes/account.tsx +++ b/src/routes/account.tsx @@ -8,6 +8,7 @@ import XCircle from "~/components/icons/XCircle"; import Dropzone from "~/components/blog/Dropzone"; import AddImageToS3 from "~/lib/s3upload"; import { validatePassword, isValidEmail } from "~/lib/validation"; +import { TerminalSplash } from "~/components/TerminalSplash"; type UserProfile = { id: string; @@ -463,14 +464,7 @@ export default function AccountPage() {
- -
Loading...
-
- } - > + }> {(currentUser) => ( <>
diff --git a/src/routes/blog/create/index.tsx b/src/routes/blog/create/index.tsx index fcc9909..0a9f44b 100644 --- a/src/routes/blog/create/index.tsx +++ b/src/routes/blog/create/index.tsx @@ -1,16 +1,9 @@ -import { Show, createSignal, createEffect, onCleanup } from "solid-js"; -import { useNavigate, query } from "@solidjs/router"; +import { Show } from "solid-js"; +import { query } from "@solidjs/router"; import { Title, Meta } from "@solidjs/meta"; import { createAsync } from "@solidjs/router"; import { getRequestEvent } from "solid-js/web"; -import { api } from "~/lib/api"; -import { debounce } from "es-toolkit"; -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"; +import PostForm from "~/components/blog/PostForm"; const getAuthState = query(async () => { "use server"; @@ -23,175 +16,8 @@ const getAuthState = query(async () => { }, "auth-state"); export default function CreatePost() { - const navigate = useNavigate(); const authState = createAsync(() => getAuthState()); - const [title, setTitle] = createSignal(""); - const [subtitle, setSubtitle] = createSignal(""); - const [body, setBody] = createSignal(""); - const [bannerPhoto, setBannerPhoto] = createSignal(""); - const [bannerImageFile, setBannerImageFile] = createSignal(); - const [bannerImageHolder, setBannerImageHolder] = createSignal< - string | ArrayBuffer | null - >(null); - const [published, setPublished] = createSignal(false); - const [tags, setTags] = createSignal([]); - const [tagInputValue, setTagInputValue] = createSignal(""); - const [loading, setLoading] = createSignal(false); - const [error, setError] = createSignal(""); - const [showAutoSaveMessage, setShowAutoSaveMessage] = createSignal(false); - const [hasSaved, setHasSaved] = createSignal(false); - - 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); - }; - - // Debounced auto-save (1 second after last change) - const debouncedAutoSave = debounce(autoSave, 1000); - - // Track changes to trigger auto-save - createEffect(() => { - // Track all relevant fields - const titleVal = title(); - const subtitleVal = subtitle(); - const bodyVal = body(); - const tagsVal = tags(); - const publishedVal = published(); - - // Only trigger auto-save if we have at least title and body - if (titleVal && bodyVal) { - debouncedAutoSave(); - } - }); - - onCleanup(() => { - debouncedAutoSave.cancel(); - }); - - 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(); - - if (!authState()?.userID) { - setError("You must be logged in to create a post"); - return; - } - - setLoading(true); - 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().replaceAll(" ", "_"), - subtitle: subtitle() || null, - body: body() || null, - banner_photo: bannerImageKey !== "" ? bannerImageKey : null, - published: published(), - tags: tags().length > 0 ? tags() : null, - author_id: authState()!.userID - }); - - if (result.data) { - navigate(`/blog/${encodeURIComponent(title().replaceAll(" ", "_"))}`); - } - } catch (err) { - console.error("Error creating post:", err); - setError("Failed to create post. Please try again."); - } finally { - setLoading(false); - } - }; - return ( <> Create Blog Post | Michael Freno @@ -211,130 +37,9 @@ export default function CreatePost() {
} > -
-
Create a Blog
-
-
- {/* Title */} -
- setTitle(e.currentTarget.value)} - required - name="title" - placeholder=" " - class="underlinedInput w-full bg-transparent" - /> - - -
- - {/* Subtitle */} -
- setSubtitle(e.currentTarget.value)} - name="subtitle" - placeholder=" " - class="underlinedInput w-full bg-transparent" - /> - - -
- - {/* Banner */} -
Banner
-
- - -
- - {/* Attachments */} - - - {/* Text Editor */} -
- -
- - {/* Tags */} - - - {/* Auto-save message */} -
- {showAutoSaveMessage() ? "Auto save success!" : ""} -
- - {/* Publish checkbox */} -
- setPublished(e.currentTarget.checked)} - /> -
Publish
-
- - {/* Error message */} - -
{error()}
-
- - {/* Submit button */} -
- -
- -
-
+ + +
); diff --git a/src/routes/blog/edit/[id]/index.tsx b/src/routes/blog/edit/[id]/index.tsx index cb39406..1b03874 100644 --- a/src/routes/blog/edit/[id]/index.tsx +++ b/src/routes/blog/edit/[id]/index.tsx @@ -1,16 +1,9 @@ -import { Show, createSignal, createEffect, onCleanup } from "solid-js"; -import { useParams, useNavigate, query } from "@solidjs/router"; +import { Show } from "solid-js"; +import { useParams, query } from "@solidjs/router"; import { Title, Meta } from "@solidjs/meta"; import { createAsync } from "@solidjs/router"; import { getRequestEvent } from "solid-js/web"; -import { api } from "~/lib/api"; -import { debounce } from "es-toolkit"; -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"; +import PostForm from "~/components/blog/PostForm"; const getPostForEdit = query(async (id: string) => { "use server"; @@ -41,210 +34,24 @@ const getPostForEdit = query(async (id: string) => { export default function EditPost() { const params = useParams(); - const navigate = useNavigate(); - const data = createAsync(() => getPostForEdit(params.id)); - const [title, setTitle] = createSignal(""); - const [subtitle, setSubtitle] = createSignal(""); - const [body, setBody] = createSignal(""); - const [bannerPhoto, setBannerPhoto] = createSignal(""); - const [bannerImageFile, setBannerImageFile] = createSignal(); - const [bannerImageHolder, setBannerImageHolder] = createSignal< - string | ArrayBuffer | null - >(null); - const [requestedDeleteImage, setRequestedDeleteImage] = createSignal(false); - const [published, setPublished] = createSignal(false); - const [tags, setTags] = createSignal([]); - const [tagInputValue, setTagInputValue] = createSignal(""); - const [loading, setLoading] = createSignal(false); - const [error, setError] = createSignal(""); - const [showAutoSaveMessage, setShowAutoSaveMessage] = createSignal(false); - const [isInitialLoad, setIsInitialLoad] = createSignal(true); - const [initialBody, setInitialBody] = createSignal( - undefined - ); + const postData = () => { + const d = data(); + if (!d?.post) return null; - // Populate form when data loads - createEffect(() => { - const postData = data(); - if (postData?.post) { - const p = postData.post as any; - setTitle(p.title?.replaceAll("_", " ") || ""); - setSubtitle(p.subtitle || ""); - setBody(p.body); + const p = d.post as any; + const tagValues = d.tags ? (d.tags as any[]).map((t) => t.value) : []; - // Set initial body only once for the editor - if (initialBody() === undefined) { - setInitialBody(p.body); - } - - setBannerPhoto(p.banner_photo || ""); - setPublished(p.published || false); - - if (postData.tags) { - const tagValues = (postData.tags as any[]).map((t) => t.value); - setTags(tagValues); - } - - // Mark initial load as complete after data is loaded - setIsInitialLoad(false); - } - }); - - 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); - }; - - // Debounced auto-save (1 second after last change) - const debouncedAutoSave = debounce(autoSave, 1000); - - // Track changes to trigger auto-save (but not on initial load) - createEffect(() => { - // Track all relevant fields - const titleVal = title(); - const subtitleVal = subtitle(); - const bodyVal = body(); - const tagsVal = tags(); - const publishedVal = published(); - - // Only trigger auto-save if not initial load and we have title - if (!isInitialLoad() && titleVal) { - debouncedAutoSave(); - } - }); - - onCleanup(() => { - debouncedAutoSave.cancel(); - }); - - 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(); - - if (!data()?.userID) { - setError("You must be logged in to edit posts"); - return; - } - - setLoading(true); - 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().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 - }); - - navigate(`/blog/${encodeURIComponent(title().replaceAll(" ", "_"))}`); - } catch (err) { - console.error("Error updating post:", err); - setError("Failed to update post. Please try again."); - } finally { - setLoading(false); - } + return { + title: p.title?.replaceAll("_", " ") || "", + subtitle: p.subtitle || "", + body: p.body || "", + banner_photo: p.banner_photo || "", + published: p.published || false, + tags: tagValues, + attachments: p.attachments + }; }; return ( @@ -267,154 +74,19 @@ export default function EditPost() { } >
Loading post...
} > -
-
Edit a Blog
-
-
- {/* Title */} -
- setTitle(e.currentTarget.value)} - required - name="title" - placeholder=" " - class="underlinedInput w-full bg-transparent" - /> - - -
- - {/* Subtitle */} -
- setSubtitle(e.currentTarget.value)} - name="subtitle" - placeholder=" " - class="underlinedInput w-full bg-transparent" - /> - - -
- - {/* Banner */} -
Banner
-
- - -
- - {/* Attachments */} - - - {/* Text Editor */} -
- -
- - {/* Tags */} - - - {/* Auto-save message */} -
- {showAutoSaveMessage() ? "Auto save success!" : ""} -
- - {/* Publish checkbox */} -
- setPublished(e.currentTarget.checked)} - /> -
Published
-
- - {/* Error message */} - -
{error()}
-
- - {/* Submit button */} -
- -
- -
- -
+