✨ Post with categories and tags
This commit is contained in:
		| @@ -3,7 +3,7 @@ package models | |||||||
| type Tag struct { | type Tag struct { | ||||||
| 	BaseModel | 	BaseModel | ||||||
|  |  | ||||||
| 	Alias       string `json:"alias" gorm:"uniqueIndex"` | 	Alias       string `json:"alias" gorm:"uniqueIndex" validate:"lowercase,alphanum,min=4,max=24"` | ||||||
| 	Name        string `json:"name"` | 	Name        string `json:"name"` | ||||||
| 	Description string `json:"description"` | 	Description string `json:"description"` | ||||||
| 	Posts       []Post `json:"posts" gorm:"many2many:post_tags"` | 	Posts       []Post `json:"posts" gorm:"many2many:post_tags"` | ||||||
| @@ -12,7 +12,7 @@ type Tag struct { | |||||||
| type Category struct { | type Category struct { | ||||||
| 	BaseModel | 	BaseModel | ||||||
|  |  | ||||||
| 	Alias       string `json:"alias" gorm:"uniqueIndex"` | 	Alias       string `json:"alias" gorm:"uniqueIndex" validate:"lowercase,alphanum,min=4,max=24"` | ||||||
| 	Name        string `json:"name"` | 	Name        string `json:"name"` | ||||||
| 	Description string `json:"description"` | 	Description string `json:"description"` | ||||||
| 	Posts       []Post `json:"categories" gorm:"many2many:post_categories"` | 	Posts       []Post `json:"categories" gorm:"many2many:post_categories"` | ||||||
|   | |||||||
| @@ -19,12 +19,18 @@ func ListPost(tx *gorm.DB, take int, offset int) ([]*models.Post, error) { | |||||||
| 		Offset(offset). | 		Offset(offset). | ||||||
| 		Preload("Author"). | 		Preload("Author"). | ||||||
| 		Preload("Attachments"). | 		Preload("Attachments"). | ||||||
|  | 		Preload("Categories"). | ||||||
|  | 		Preload("Tags"). | ||||||
| 		Preload("RepostTo"). | 		Preload("RepostTo"). | ||||||
| 		Preload("ReplyTo"). | 		Preload("ReplyTo"). | ||||||
| 		Preload("RepostTo.Author"). | 		Preload("RepostTo.Author"). | ||||||
| 		Preload("ReplyTo.Author"). | 		Preload("ReplyTo.Author"). | ||||||
| 		Preload("RepostTo.Attachments"). | 		Preload("RepostTo.Attachments"). | ||||||
| 		Preload("ReplyTo.Attachments"). | 		Preload("ReplyTo.Attachments"). | ||||||
|  | 		Preload("RepostTo.Categories"). | ||||||
|  | 		Preload("ReplyTo.Categories"). | ||||||
|  | 		Preload("RepostTo.Tags"). | ||||||
|  | 		Preload("ReplyTo.Tags"). | ||||||
| 		Find(&posts).Error; err != nil { | 		Find(&posts).Error; err != nil { | ||||||
| 		return posts, err | 		return posts, err | ||||||
| 	} | 	} | ||||||
|   | |||||||
| @@ -1,4 +1,4 @@ | |||||||
| import { createSignal, Show } from "solid-js"; | import { createSignal, For, Show } from "solid-js"; | ||||||
| import { getAtk, useUserinfo } from "../stores/userinfo.tsx"; | import { getAtk, useUserinfo } from "../stores/userinfo.tsx"; | ||||||
| import PostAttachments from "./PostAttachments.tsx"; | import PostAttachments from "./PostAttachments.tsx"; | ||||||
|  |  | ||||||
| @@ -59,6 +59,19 @@ export default function PostItem(props: { | |||||||
|         <h2 class="card-title">{props.post.title}</h2> |         <h2 class="card-title">{props.post.title}</h2> | ||||||
|         <article class="prose">{props.post.content}</article> |         <article class="prose">{props.post.content}</article> | ||||||
|  |  | ||||||
|  |         <div class="mt-2 flex gap-2"> | ||||||
|  |           <For each={props.post.categories}> | ||||||
|  |             {item => <a href={`/categories/${item.alias}`} class="link link-primary"> | ||||||
|  |               #{item.name} | ||||||
|  |             </a>} | ||||||
|  |           </For> | ||||||
|  |           <For each={props.post.tags}> | ||||||
|  |             {item => <a href={`/tags/${item.alias}`} class="link link-primary"> | ||||||
|  |               #{item.name} | ||||||
|  |             </a>} | ||||||
|  |           </For> | ||||||
|  |         </div> | ||||||
|  |  | ||||||
|         <PostAttachments attachments={props.post.attachments ?? []} /> |         <PostAttachments attachments={props.post.attachments ?? []} /> | ||||||
|  |  | ||||||
|         <Show when={props.post.repost_to}> |         <Show when={props.post.repost_to}> | ||||||
|   | |||||||
| @@ -1,4 +1,8 @@ | |||||||
| .publishInput { | .publishInput { | ||||||
|     outline-style: none !important; |     outline-style: none !important; | ||||||
|     outline-width: 0 !important; |     outline-width: 0 !important; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .description { | ||||||
|  |     color: var(--fallback-bc, oklch(var(--bc)/.8)); | ||||||
| } | } | ||||||
| @@ -18,8 +18,14 @@ export default function PostPublish(props: { | |||||||
|   const [uploading, setUploading] = createSignal(false); |   const [uploading, setUploading] = createSignal(false); | ||||||
|  |  | ||||||
|   const [attachments, setAttachments] = createSignal<any[]>([]); |   const [attachments, setAttachments] = createSignal<any[]>([]); | ||||||
|  |   const [categories, setCategories] = createSignal<{ alias: string, name: string }[]>([]); | ||||||
|  |   const [tags, setTags] = createSignal<{ alias: string, name: string }[]>([]); | ||||||
|  |  | ||||||
|   createEffect(() => setAttachments(props.editing?.attachments ?? []), [props.editing]); |   createEffect(() => { | ||||||
|  |     setAttachments(props.editing?.attachments ?? []); | ||||||
|  |     setCategories(props.editing?.categories ?? []); | ||||||
|  |     setTags(props.editing?.tags ?? []); | ||||||
|  |   }, [props.editing]); | ||||||
|  |  | ||||||
|   async function doPost(evt: SubmitEvent) { |   async function doPost(evt: SubmitEvent) { | ||||||
|     evt.preventDefault(); |     evt.preventDefault(); | ||||||
| @@ -40,6 +46,8 @@ export default function PostPublish(props: { | |||||||
|         title: data.title, |         title: data.title, | ||||||
|         content: data.content, |         content: data.content, | ||||||
|         attachments: attachments(), |         attachments: attachments(), | ||||||
|  |         categories: categories(), | ||||||
|  |         tags: tags(), | ||||||
|         published_at: data.published_at ? new Date(data.published_at as string) : new Date(), |         published_at: data.published_at ? new Date(data.published_at as string) : new Date(), | ||||||
|         repost_to: props.reposting?.id, |         repost_to: props.reposting?.id, | ||||||
|         reply_to: props.replying?.id |         reply_to: props.replying?.id | ||||||
| @@ -75,6 +83,8 @@ export default function PostPublish(props: { | |||||||
|         title: data.title, |         title: data.title, | ||||||
|         content: data.content, |         content: data.content, | ||||||
|         attachments: attachments(), |         attachments: attachments(), | ||||||
|  |         categories: categories(), | ||||||
|  |         tags: tags(), | ||||||
|         published_at: data.published_at ? new Date(data.published_at as string) : new Date() |         published_at: data.published_at ? new Date(data.published_at as string) : new Date() | ||||||
|       }) |       }) | ||||||
|     }); |     }); | ||||||
| @@ -91,7 +101,8 @@ export default function PostPublish(props: { | |||||||
|   async function uploadAttachments(evt: SubmitEvent) { |   async function uploadAttachments(evt: SubmitEvent) { | ||||||
|     evt.preventDefault(); |     evt.preventDefault(); | ||||||
|  |  | ||||||
|     const data = new FormData(evt.target as HTMLFormElement); |     const form = evt.target as HTMLFormElement; | ||||||
|  |     const data = new FormData(form); | ||||||
|     if (!data.get("attachment")) return; |     if (!data.get("attachment")) return; | ||||||
|  |  | ||||||
|     setUploading(true); |     setUploading(true); | ||||||
| @@ -106,12 +117,47 @@ export default function PostPublish(props: { | |||||||
|       const data = await res.json(); |       const data = await res.json(); | ||||||
|       setAttachments(attachments().concat([data.info])); |       setAttachments(attachments().concat([data.info])); | ||||||
|       props.onError(null); |       props.onError(null); | ||||||
|  |       form.reset(); | ||||||
|     } |     } | ||||||
|     setUploading(false); |     setUploading(false); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   function addCategory(evt: SubmitEvent) { | ||||||
|  |     evt.preventDefault(); | ||||||
|  |  | ||||||
|  |     const form = evt.target as HTMLFormElement; | ||||||
|  |     const data = Object.fromEntries(new FormData(form)); | ||||||
|  |     if (!data.alias) data.alias = crypto.randomUUID().replace(/-/g, ""); | ||||||
|  |     if (!data.name) return; | ||||||
|  |  | ||||||
|  |     setCategories(categories().concat([data as any])); | ||||||
|  |     form.reset(); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   function removeCategory(target: any) { | ||||||
|  |     setCategories(categories().filter(item => item.alias !== target.alias)) | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   function addTag(evt: SubmitEvent) { | ||||||
|  |     evt.preventDefault(); | ||||||
|  |  | ||||||
|  |     const form = evt.target as HTMLFormElement; | ||||||
|  |     const data = Object.fromEntries(new FormData(evt.target as HTMLFormElement)); | ||||||
|  |     if (!data.alias) data.alias = crypto.randomUUID().replace(/-/g, ""); | ||||||
|  |     if (!data.name) return; | ||||||
|  |  | ||||||
|  |     setTags(tags().concat([data as any])); | ||||||
|  |     form.reset(); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   function removeTag(target: any) { | ||||||
|  |     setTags(tags().filter(item => item.alias !== target.alias)) | ||||||
|  |   } | ||||||
|  |  | ||||||
|   function resetForm() { |   function resetForm() { | ||||||
|     setAttachments([]); |     setAttachments([]); | ||||||
|  |     setCategories([]); | ||||||
|  |     setTags([]); | ||||||
|     props.onReset(); |     props.onReset(); | ||||||
|   } |   } | ||||||
|  |  | ||||||
| @@ -174,13 +220,16 @@ export default function PostPublish(props: { | |||||||
|                   placeholder="What's happend?!" /> |                   placeholder="What's happend?!" /> | ||||||
|  |  | ||||||
|         <div id="publish-actions" class="flex justify-between border-y border-base-200"> |         <div id="publish-actions" class="flex justify-between border-y border-base-200"> | ||||||
|           <div class="flex"> |           <div class="flex pl-[20px]"> | ||||||
|             <button type="button" class="btn btn-ghost w-12" onClick={() => openModel("#attachments")}> |             <button type="button" class="btn btn-ghost w-12" onClick={() => openModel("#attachments")}> | ||||||
|               <i class="fa-solid fa-paperclip"></i> |               <i class="fa-solid fa-paperclip"></i> | ||||||
|             </button> |             </button> | ||||||
|             <button type="button" class="btn btn-ghost w-12" onClick={() => openModel("#planning-publish")}> |             <button type="button" class="btn btn-ghost w-12" onClick={() => openModel("#planning-publish")}> | ||||||
|               <i class="fa-solid fa-calendar-day"></i> |               <i class="fa-solid fa-calendar-day"></i> | ||||||
|             </button> |             </button> | ||||||
|  |             <button type="button" class="btn btn-ghost w-12" onClick={() => openModel("#categories-and-tags")}> | ||||||
|  |               <i class="fa-solid fa-tag"></i> | ||||||
|  |             </button> | ||||||
|           </div> |           </div> | ||||||
|  |  | ||||||
|           <div> |           <div> | ||||||
| @@ -215,7 +264,6 @@ export default function PostPublish(props: { | |||||||
|         </dialog> |         </dialog> | ||||||
|       </form> |       </form> | ||||||
|  |  | ||||||
|  |  | ||||||
|       <dialog id="attachments" class="modal"> |       <dialog id="attachments" class="modal"> | ||||||
|         <div class="modal-box"> |         <div class="modal-box"> | ||||||
|           <h3 class="font-bold text-lg mx-1">Attachments</h3> |           <h3 class="font-bold text-lg mx-1">Attachments</h3> | ||||||
| @@ -225,7 +273,8 @@ export default function PostPublish(props: { | |||||||
|                 <span class="label-text">Pick a file</span> |                 <span class="label-text">Pick a file</span> | ||||||
|               </div> |               </div> | ||||||
|               <div class="join"> |               <div class="join"> | ||||||
|                 <input required type="file" name="attachment" class="join-item file-input file-input-bordered w-full" /> |                 <input required type="file" name="attachment" | ||||||
|  |                        class="join-item file-input file-input-bordered w-full" /> | ||||||
|                 <button type="submit" class="join-item btn btn-primary" disabled={uploading()}> |                 <button type="submit" class="join-item btn btn-primary" disabled={uploading()}> | ||||||
|                   <i class="fa-solid fa-upload"></i> |                   <i class="fa-solid fa-upload"></i> | ||||||
|                 </button> |                 </button> | ||||||
| @@ -253,6 +302,87 @@ export default function PostPublish(props: { | |||||||
|           </div> |           </div> | ||||||
|         </div> |         </div> | ||||||
|       </dialog> |       </dialog> | ||||||
|  |  | ||||||
|  |       <dialog id="categories-and-tags" class="modal"> | ||||||
|  |         <div class="modal-box"> | ||||||
|  |           <h3 class="font-bold text-lg mx-1">Categories & Tags</h3> | ||||||
|  |           <form class="w-full mt-3" onSubmit={addCategory}> | ||||||
|  |             <label class="form-control"> | ||||||
|  |               <div class="label"> | ||||||
|  |                 <span class="label-text">Add a category</span> | ||||||
|  |               </div> | ||||||
|  |               <div class="join"> | ||||||
|  |                 <input type="text" name="alias" placeholder="Alias" class="join-item input input-bordered w-full" /> | ||||||
|  |                 <input type="text" name="name" placeholder="Name" class="join-item input input-bordered w-full" /> | ||||||
|  |                 <button type="submit" class="join-item btn btn-primary"> | ||||||
|  |                   <i class="fa-solid fa-plus"></i> | ||||||
|  |                 </button> | ||||||
|  |               </div> | ||||||
|  |               <div class="label"> | ||||||
|  |                 <span class="label-text-alt"> | ||||||
|  |                   Alias is the url key of this category. Lowercase only, required length 4-24. | ||||||
|  |                   Leave blank for auto generate. | ||||||
|  |                 </span> | ||||||
|  |               </div> | ||||||
|  |             </label> | ||||||
|  |           </form> | ||||||
|  |  | ||||||
|  |           <Show when={categories().length > 0}> | ||||||
|  |             <h3 class="font-bold mt-3 mx-1">Category list</h3> | ||||||
|  |             <ol class="mt-2 mx-1 text-sm"> | ||||||
|  |               <For each={categories()}> | ||||||
|  |                 {item => <li> | ||||||
|  |                   <i class="fa-solid fa-layer-group me-2"></i> | ||||||
|  |                   {item.name} <span class={styles.description}>#{item.alias}</span> | ||||||
|  |                   <button class="ml-2" onClick={() => removeCategory(item)}> | ||||||
|  |                     <i class="fa-solid fa-delete-left"></i> | ||||||
|  |                   </button> | ||||||
|  |                 </li>} | ||||||
|  |               </For> | ||||||
|  |             </ol> | ||||||
|  |           </Show> | ||||||
|  |  | ||||||
|  |           <form class="w-full mt-3" onSubmit={addTag}> | ||||||
|  |             <label class="form-control"> | ||||||
|  |               <div class="label"> | ||||||
|  |                 <span class="label-text">Add a tag</span> | ||||||
|  |               </div> | ||||||
|  |               <div class="join"> | ||||||
|  |                 <input type="text" name="alias" placeholder="Alias" class="join-item input input-bordered w-full" /> | ||||||
|  |                 <input type="text" name="name" placeholder="Name" class="join-item input input-bordered w-full" /> | ||||||
|  |                 <button type="submit" class="join-item btn btn-primary"> | ||||||
|  |                   <i class="fa-solid fa-plus"></i> | ||||||
|  |                 </button> | ||||||
|  |               </div> | ||||||
|  |               <div class="label"> | ||||||
|  |                 <span class="label-text-alt"> | ||||||
|  |                   Alias is the url key of this tag. Lowercase only, required length 4-24. | ||||||
|  |                   Leave blank for auto generate. | ||||||
|  |                 </span> | ||||||
|  |               </div> | ||||||
|  |             </label> | ||||||
|  |           </form> | ||||||
|  |  | ||||||
|  |           <Show when={tags().length > 0}> | ||||||
|  |             <h3 class="font-bold mt-3 mx-1">Category list</h3> | ||||||
|  |             <ol class="mt-2 mx-1 text-sm"> | ||||||
|  |               <For each={tags()}> | ||||||
|  |                 {item => <li> | ||||||
|  |                   <i class="fa-solid fa-tag me-2"></i> | ||||||
|  |                   {item.name} <span class={styles.description}>#{item.alias}</span> | ||||||
|  |                   <button class="ml-2" onClick={() => removeTag(item)}> | ||||||
|  |                     <i class="fa-solid fa-delete-left"></i> | ||||||
|  |                   </button> | ||||||
|  |                 </li>} | ||||||
|  |               </For> | ||||||
|  |             </ol> | ||||||
|  |           </Show> | ||||||
|  |  | ||||||
|  |           <div class="modal-action"> | ||||||
|  |             <button type="button" class="btn" onClick={() => closeModel("#categories-and-tags")}>Close</button> | ||||||
|  |           </div> | ||||||
|  |         </div> | ||||||
|  |       </dialog> | ||||||
|     </> |     </> | ||||||
|   ); |   ); | ||||||
| } | } | ||||||
		Reference in New Issue
	
	Block a user