✨ Edit & Delete
This commit is contained in:
		| @@ -2,6 +2,7 @@ package server | ||||
|  | ||||
| import ( | ||||
| 	"strings" | ||||
| 	"time" | ||||
|  | ||||
| 	"code.smartsheep.studio/hydrogen/interactive/pkg/database" | ||||
| 	"code.smartsheep.studio/hydrogen/interactive/pkg/models" | ||||
| @@ -16,7 +17,10 @@ func listPost(c *fiber.Ctx) error { | ||||
| 	offset := c.QueryInt("offset", 0) | ||||
| 	authorId := c.QueryInt("authorId", 0) | ||||
|  | ||||
| 	tx := database.C.Where(&models.Post{RealmID: nil}).Order("created_at desc") | ||||
| 	tx := database.C. | ||||
| 		Where(&models.Post{RealmID: nil}). | ||||
| 		Where("published_at <= ? OR published_at IS NULL", time.Now()). | ||||
| 		Order("created_at desc") | ||||
|  | ||||
| 	if authorId > 0 { | ||||
| 		tx = tx.Where(&models.Post{AuthorID: uint(authorId)}) | ||||
| @@ -49,6 +53,7 @@ func createPost(c *fiber.Ctx) error { | ||||
| 		Content     string            `json:"content" validate:"required"` | ||||
| 		Tags        []models.Tag      `json:"tags"` | ||||
| 		Categories  []models.Category `json:"categories"` | ||||
| 		PublishedAt *time.Time        `json:"published_at"` | ||||
| 		RepostTo    uint              `json:"repost_to"` | ||||
| 		ReplyTo     uint              `json:"reply_to"` | ||||
| 	} | ||||
| @@ -84,7 +89,58 @@ func createPost(c *fiber.Ctx) error { | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	post, err := services.NewPost(user, data.Alias, data.Title, data.Content, data.Categories, data.Tags, replyTo, repostTo) | ||||
| 	post, err := services.NewPost( | ||||
| 		user, | ||||
| 		data.Alias, | ||||
| 		data.Title, | ||||
| 		data.Content, | ||||
| 		data.Categories, | ||||
| 		data.Tags, | ||||
| 		data.PublishedAt, | ||||
| 		replyTo, | ||||
| 		repostTo, | ||||
| 	) | ||||
| 	if err != nil { | ||||
| 		return fiber.NewError(fiber.StatusBadRequest, err.Error()) | ||||
| 	} | ||||
|  | ||||
| 	return c.JSON(post) | ||||
| } | ||||
|  | ||||
| func editPost(c *fiber.Ctx) error { | ||||
| 	user := c.Locals("principal").(models.Account) | ||||
| 	id, _ := c.ParamsInt("postId", 0) | ||||
|  | ||||
| 	var data struct { | ||||
| 		Alias       string            `json:"alias" validate:"required"` | ||||
| 		Title       string            `json:"title"` | ||||
| 		Content     string            `json:"content" validate:"required"` | ||||
| 		PublishedAt *time.Time        `json:"published_at"` | ||||
| 		Tags        []models.Tag      `json:"tags"` | ||||
| 		Categories  []models.Category `json:"categories"` | ||||
| 	} | ||||
|  | ||||
| 	if err := BindAndValidate(c, &data); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	var post models.Post | ||||
| 	if err := database.C.Where(&models.Post{ | ||||
| 		BaseModel: models.BaseModel{ID: uint(id)}, | ||||
| 		AuthorID:  user.ID, | ||||
| 	}).First(&post).Error; err != nil { | ||||
| 		return fiber.NewError(fiber.StatusNotFound, err.Error()) | ||||
| 	} | ||||
|  | ||||
| 	post, err := services.EditPost( | ||||
| 		post, | ||||
| 		data.Alias, | ||||
| 		data.Title, | ||||
| 		data.Content, | ||||
| 		data.PublishedAt, | ||||
| 		data.Categories, | ||||
| 		data.Tags, | ||||
| 	) | ||||
| 	if err != nil { | ||||
| 		return fiber.NewError(fiber.StatusBadRequest, err.Error()) | ||||
| 	} | ||||
| @@ -120,3 +176,22 @@ func reactPost(c *fiber.Ctx) error { | ||||
| 		return fiber.NewError(fiber.StatusBadRequest, "unsupported reaction") | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func deletePost(c *fiber.Ctx) error { | ||||
| 	user := c.Locals("principal").(models.Account) | ||||
| 	id, _ := c.ParamsInt("postId", 0) | ||||
|  | ||||
| 	var post models.Post | ||||
| 	if err := database.C.Where(&models.Post{ | ||||
| 		BaseModel: models.BaseModel{ID: uint(id)}, | ||||
| 		AuthorID:  user.ID, | ||||
| 	}).First(&post).Error; err != nil { | ||||
| 		return fiber.NewError(fiber.StatusNotFound, err.Error()) | ||||
| 	} | ||||
|  | ||||
| 	if err := services.DeletePost(post); err != nil { | ||||
| 		return fiber.NewError(fiber.StatusBadRequest, err.Error()) | ||||
| 	} | ||||
|  | ||||
| 	return c.SendStatus(fiber.StatusOK) | ||||
| } | ||||
|   | ||||
| @@ -68,6 +68,8 @@ func NewServer() { | ||||
| 		api.Get("/posts", listPost) | ||||
| 		api.Post("/posts", auth, createPost) | ||||
| 		api.Post("/posts/:postId/react/:reactType", auth, reactPost) | ||||
| 		api.Put("/posts/:postId", auth, editPost) | ||||
| 		api.Delete("/posts/:postId", auth, deletePost) | ||||
| 	} | ||||
|  | ||||
| 	A.Use("/", cache.New(cache.Config{ | ||||
|   | ||||
| @@ -3,6 +3,7 @@ package services | ||||
| import ( | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
| 	"time" | ||||
|  | ||||
| 	"code.smartsheep.studio/hydrogen/interactive/pkg/database" | ||||
| 	"code.smartsheep.studio/hydrogen/interactive/pkg/models" | ||||
| @@ -67,9 +68,21 @@ func NewPost( | ||||
| 	alias, title, content string, | ||||
| 	categories []models.Category, | ||||
| 	tags []models.Tag, | ||||
| 	publishedAt *time.Time, | ||||
| 	replyTo, repostTo *uint, | ||||
| ) (models.Post, error) { | ||||
| 	return NewPostWithRealm(user, nil, alias, title, content, categories, tags, replyTo, repostTo) | ||||
| 	return NewPostWithRealm( | ||||
| 		user, | ||||
| 		nil, | ||||
| 		alias, | ||||
| 		title, | ||||
| 		content, | ||||
| 		categories, | ||||
| 		tags, | ||||
| 		publishedAt, | ||||
| 		replyTo, | ||||
| 		repostTo, | ||||
| 	) | ||||
| } | ||||
|  | ||||
| func NewPostWithRealm( | ||||
| @@ -78,6 +91,7 @@ func NewPostWithRealm( | ||||
| 	alias, title, content string, | ||||
| 	categories []models.Category, | ||||
| 	tags []models.Tag, | ||||
| 	publishedAt *time.Time, | ||||
| 	replyTo, repostTo *uint, | ||||
| ) (models.Post, error) { | ||||
| 	var err error | ||||
| @@ -100,6 +114,10 @@ func NewPostWithRealm( | ||||
| 		realmId = &realm.ID | ||||
| 	} | ||||
|  | ||||
| 	if publishedAt == nil { | ||||
| 		publishedAt = lo.ToPtr(time.Now()) | ||||
| 	} | ||||
|  | ||||
| 	post = models.Post{ | ||||
| 		Alias:       alias, | ||||
| 		Title:       title, | ||||
| @@ -108,6 +126,7 @@ func NewPostWithRealm( | ||||
| 		Categories:  categories, | ||||
| 		AuthorID:    user.ID, | ||||
| 		RealmID:     realmId, | ||||
| 		PublishedAt: *publishedAt, | ||||
| 		RepostID:    repostTo, | ||||
| 		ReplyID:     replyTo, | ||||
| 	} | ||||
| @@ -119,6 +138,41 @@ func NewPostWithRealm( | ||||
| 	return post, nil | ||||
| } | ||||
|  | ||||
| func EditPost( | ||||
| 	post models.Post, | ||||
| 	alias, title, content string, | ||||
| 	publishedAt *time.Time, | ||||
| 	categories []models.Category, | ||||
| 	tags []models.Tag, | ||||
| ) (models.Post, error) { | ||||
| 	var err error | ||||
| 	for idx, category := range categories { | ||||
| 		categories[idx], err = GetCategory(category.Alias, category.Name) | ||||
| 		if err != nil { | ||||
| 			return post, err | ||||
| 		} | ||||
| 	} | ||||
| 	for idx, tag := range tags { | ||||
| 		tags[idx], err = GetTag(tag.Alias, tag.Name) | ||||
| 		if err != nil { | ||||
| 			return post, err | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	if publishedAt == nil { | ||||
| 		publishedAt = lo.ToPtr(time.Now()) | ||||
| 	} | ||||
|  | ||||
| 	post.Alias = alias | ||||
| 	post.Title = title | ||||
| 	post.Content = content | ||||
| 	post.PublishedAt = *publishedAt | ||||
|  | ||||
| 	err = database.C.Save(&post).Error | ||||
|  | ||||
| 	return post, err | ||||
| } | ||||
|  | ||||
| func LikePost(user models.Account, post models.Post) (bool, error) { | ||||
| 	var like models.PostLike | ||||
| 	if err := database.C.Where(&models.PostLike{ | ||||
| @@ -156,3 +210,7 @@ func DislikePost(user models.Account, post models.Post) (bool, error) { | ||||
| 		return false, database.C.Delete(&dislike).Error | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func DeletePost(post models.Post) error { | ||||
| 	return database.C.Delete(&post).Error | ||||
| } | ||||
|   | ||||
| @@ -8,6 +8,7 @@ export default function PostItem(props: { | ||||
|   onRepost?: (post: any) => void, | ||||
|   onReply?: (post: any) => void, | ||||
|   onEdit?: (post: any) => void, | ||||
|   onDelete?: (post: any) => void, | ||||
|   onError: (message: string | null) => void, | ||||
|   onReact: () => void | ||||
| }) { | ||||
| @@ -127,6 +128,9 @@ export default function PostItem(props: { | ||||
|                 <i class="fa-solid fa-ellipsis-vertical"></i> | ||||
|               </div> | ||||
|               <ul tabIndex="0" class="dropdown-content z-[1] menu p-2 shadow bg-base-100 rounded-box w-52"> | ||||
|                 <Show when={userinfo?.profiles?.id === props.post.author_id}> | ||||
|                   <li><a onClick={() => props.onDelete && props.onDelete(props.post)}>Delete</a></li> | ||||
|                 </Show> | ||||
|                 <Show when={userinfo?.profiles?.id === props.post.author_id}> | ||||
|                   <li><a onClick={() => props.onEdit && props.onEdit(props.post)}>Edit</a></li> | ||||
|                 </Show> | ||||
|   | ||||
| @@ -2,6 +2,7 @@ import { createMemo, createSignal, For, Show } from "solid-js"; | ||||
|  | ||||
| import styles from "./PostList.module.css"; | ||||
| import PostItem from "./PostItem.tsx"; | ||||
| import { getAtk } from "../stores/userinfo.tsx"; | ||||
|  | ||||
| export default function PostList(props: { | ||||
|   info: { data: any[], count: number } | null, | ||||
| @@ -27,6 +28,23 @@ export default function PostList(props: { | ||||
|  | ||||
|   readPosts(); | ||||
|  | ||||
|   async function deletePost(item: any) { | ||||
|     if (!confirm(`Are you sure to delete post#${item.id}?`)) return; | ||||
|  | ||||
|     setLoading(true); | ||||
|     const res = await fetch(`/api/posts/${item.id}`, { | ||||
|       method: "DELETE", | ||||
|       headers: { "Authorization": `Bearer ${getAtk()}` } | ||||
|     }); | ||||
|     if (res.status !== 200) { | ||||
|       props.onError(await res.text()); | ||||
|     } else { | ||||
|       await readPosts(); | ||||
|       props.onError(null); | ||||
|     } | ||||
|     setLoading(false); | ||||
|   } | ||||
|  | ||||
|   function changePage(pn: number) { | ||||
|     setPage(pn); | ||||
|     readPosts().then(() => { | ||||
| @@ -43,6 +61,7 @@ export default function PostList(props: { | ||||
|             onRepost={props.onRepost} | ||||
|             onReply={props.onReply} | ||||
|             onEdit={props.onEdit} | ||||
|             onDelete={deletePost} | ||||
|             onReact={() => readPosts()} | ||||
|             onError={props.onError} | ||||
|           />} | ||||
|   | ||||
| @@ -7,6 +7,7 @@ export default function PostPublish(props: { | ||||
|   replying?: any, | ||||
|   reposting?: any, | ||||
|   editing?: any, | ||||
|   onReset: () => void, | ||||
|   onError: (message: string | null) => void, | ||||
|   onPost: () => void | ||||
| }) { | ||||
| @@ -33,7 +34,37 @@ export default function PostPublish(props: { | ||||
|         title: data.title, | ||||
|         content: data.content, | ||||
|         repost_to: props.reposting?.id, | ||||
|         reply_to: props.replying?.id, | ||||
|         reply_to: props.replying?.id | ||||
|       }) | ||||
|     }); | ||||
|     if (res.status !== 200) { | ||||
|       props.onError(await res.text()); | ||||
|     } else { | ||||
|       form.reset(); | ||||
|       props.onPost(); | ||||
|       props.onError(null); | ||||
|     } | ||||
|     setSubmitting(false); | ||||
|   } | ||||
|  | ||||
|   async function doEdit(evt: SubmitEvent) { | ||||
|     evt.preventDefault(); | ||||
|  | ||||
|     const form = evt.target as HTMLFormElement; | ||||
|     const data = Object.fromEntries(new FormData(form)); | ||||
|     if (!data.content) return; | ||||
|  | ||||
|     setSubmitting(true); | ||||
|     const res = await fetch(`/api/posts/${props.editing?.id}`, { | ||||
|       method: "PUT", | ||||
|       headers: { | ||||
|         "Content-Type": "application/json", | ||||
|         "Authorization": `Bearer ${getAtk()}` | ||||
|       }, | ||||
|       body: JSON.stringify({ | ||||
|         alias: data.alias ?? crypto.randomUUID().replace(/-/g, ""), | ||||
|         title: data.title, | ||||
|         content: data.content | ||||
|       }) | ||||
|     }); | ||||
|     if (res.status !== 200) { | ||||
| @@ -47,7 +78,7 @@ export default function PostPublish(props: { | ||||
|   } | ||||
|  | ||||
|   return ( | ||||
|     <form id="publish" onSubmit={doPost}> | ||||
|     <form id="publish" onSubmit={props.editing ? doEdit : doPost} onReset={props.onReset}> | ||||
|       <div id="publish-identity" class="flex border-y border-base-200"> | ||||
|         <div class="avatar pl-[20px]"> | ||||
|           <div class="w-12"> | ||||
| @@ -58,7 +89,8 @@ export default function PostPublish(props: { | ||||
|           </div> | ||||
|         </div> | ||||
|         <div class="flex flex-grow"> | ||||
|           <input name="title" class={`${styles.publishInput} input w-full`} | ||||
|           <input name="title" value={props.editing?.title ?? ""} | ||||
|                  class={`${styles.publishInput} input w-full`} | ||||
|                  placeholder="The describe for a long content (Optional)" /> | ||||
|         </div> | ||||
|       </div> | ||||
| @@ -83,7 +115,8 @@ export default function PostPublish(props: { | ||||
|         </div> | ||||
|       </Show> | ||||
|  | ||||
|       <textarea name="content" class={`${styles.publishInput} textarea w-full`} | ||||
|       <textarea name="content" value={props.editing?.content ?? ""} | ||||
|                 class={`${styles.publishInput} textarea w-full`} | ||||
|                 placeholder="What's happend?!" /> | ||||
|  | ||||
|       <div id="publish-actions" class="flex justify-between border-y border-base-200"> | ||||
| @@ -93,12 +126,17 @@ export default function PostPublish(props: { | ||||
|           </button> | ||||
|         </div> | ||||
|  | ||||
|         <div> | ||||
|           <button type="reset" class="btn btn-ghost w-12" disabled={submitting()}> | ||||
|             <i class="fa-solid fa-xmark"></i> | ||||
|           </button> | ||||
|           <button type="submit" class="btn btn-primary" disabled={submitting()}> | ||||
|           <Show when={submitting()} fallback={"Post a post"}> | ||||
|             <Show when={submitting()} fallback={props.editing ? "Save changes" : "Post a post"}> | ||||
|               <span class="loading"></span> | ||||
|             </Show> | ||||
|           </button> | ||||
|         </div> | ||||
|       </div> | ||||
|     </form> | ||||
|   ); | ||||
| } | ||||
| @@ -49,6 +49,7 @@ export default function DashboardPage() { | ||||
|         replying={publishMeta.replying} | ||||
|         reposting={publishMeta.reposting} | ||||
|         editing={publishMeta.editing} | ||||
|         onReset={() => setPublishMeta({ reposting: null, replying: null, editing: null })} | ||||
|         onPost={() => readPosts()} | ||||
|         onError={setError} | ||||
|       /> | ||||
|   | ||||
		Reference in New Issue
	
	Block a user