✨ Edit & Delete
This commit is contained in:
		| @@ -2,6 +2,7 @@ package server | |||||||
|  |  | ||||||
| import ( | import ( | ||||||
| 	"strings" | 	"strings" | ||||||
|  | 	"time" | ||||||
|  |  | ||||||
| 	"code.smartsheep.studio/hydrogen/interactive/pkg/database" | 	"code.smartsheep.studio/hydrogen/interactive/pkg/database" | ||||||
| 	"code.smartsheep.studio/hydrogen/interactive/pkg/models" | 	"code.smartsheep.studio/hydrogen/interactive/pkg/models" | ||||||
| @@ -16,7 +17,10 @@ func listPost(c *fiber.Ctx) error { | |||||||
| 	offset := c.QueryInt("offset", 0) | 	offset := c.QueryInt("offset", 0) | ||||||
| 	authorId := c.QueryInt("authorId", 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 { | 	if authorId > 0 { | ||||||
| 		tx = tx.Where(&models.Post{AuthorID: uint(authorId)}) | 		tx = tx.Where(&models.Post{AuthorID: uint(authorId)}) | ||||||
| @@ -44,13 +48,14 @@ func createPost(c *fiber.Ctx) error { | |||||||
| 	user := c.Locals("principal").(models.Account) | 	user := c.Locals("principal").(models.Account) | ||||||
|  |  | ||||||
| 	var data struct { | 	var data struct { | ||||||
| 		Alias      string            `json:"alias"` | 		Alias       string            `json:"alias"` | ||||||
| 		Title      string            `json:"title"` | 		Title       string            `json:"title"` | ||||||
| 		Content    string            `json:"content" validate:"required"` | 		Content     string            `json:"content" validate:"required"` | ||||||
| 		Tags       []models.Tag      `json:"tags"` | 		Tags        []models.Tag      `json:"tags"` | ||||||
| 		Categories []models.Category `json:"categories"` | 		Categories  []models.Category `json:"categories"` | ||||||
| 		RepostTo   uint              `json:"repost_to"` | 		PublishedAt *time.Time        `json:"published_at"` | ||||||
| 		ReplyTo    uint              `json:"reply_to"` | 		RepostTo    uint              `json:"repost_to"` | ||||||
|  | 		ReplyTo     uint              `json:"reply_to"` | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	if err := BindAndValidate(c, &data); err != nil { | 	if err := BindAndValidate(c, &data); err != nil { | ||||||
| @@ -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 { | 	if err != nil { | ||||||
| 		return fiber.NewError(fiber.StatusBadRequest, err.Error()) | 		return fiber.NewError(fiber.StatusBadRequest, err.Error()) | ||||||
| 	} | 	} | ||||||
| @@ -120,3 +176,22 @@ func reactPost(c *fiber.Ctx) error { | |||||||
| 		return fiber.NewError(fiber.StatusBadRequest, "unsupported reaction") | 		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.Get("/posts", listPost) | ||||||
| 		api.Post("/posts", auth, createPost) | 		api.Post("/posts", auth, createPost) | ||||||
| 		api.Post("/posts/:postId/react/:reactType", auth, reactPost) | 		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{ | 	A.Use("/", cache.New(cache.Config{ | ||||||
|   | |||||||
| @@ -3,6 +3,7 @@ package services | |||||||
| import ( | import ( | ||||||
| 	"errors" | 	"errors" | ||||||
| 	"fmt" | 	"fmt" | ||||||
|  | 	"time" | ||||||
|  |  | ||||||
| 	"code.smartsheep.studio/hydrogen/interactive/pkg/database" | 	"code.smartsheep.studio/hydrogen/interactive/pkg/database" | ||||||
| 	"code.smartsheep.studio/hydrogen/interactive/pkg/models" | 	"code.smartsheep.studio/hydrogen/interactive/pkg/models" | ||||||
| @@ -67,9 +68,21 @@ func NewPost( | |||||||
| 	alias, title, content string, | 	alias, title, content string, | ||||||
| 	categories []models.Category, | 	categories []models.Category, | ||||||
| 	tags []models.Tag, | 	tags []models.Tag, | ||||||
|  | 	publishedAt *time.Time, | ||||||
| 	replyTo, repostTo *uint, | 	replyTo, repostTo *uint, | ||||||
| ) (models.Post, error) { | ) (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( | func NewPostWithRealm( | ||||||
| @@ -78,6 +91,7 @@ func NewPostWithRealm( | |||||||
| 	alias, title, content string, | 	alias, title, content string, | ||||||
| 	categories []models.Category, | 	categories []models.Category, | ||||||
| 	tags []models.Tag, | 	tags []models.Tag, | ||||||
|  | 	publishedAt *time.Time, | ||||||
| 	replyTo, repostTo *uint, | 	replyTo, repostTo *uint, | ||||||
| ) (models.Post, error) { | ) (models.Post, error) { | ||||||
| 	var err error | 	var err error | ||||||
| @@ -100,16 +114,21 @@ func NewPostWithRealm( | |||||||
| 		realmId = &realm.ID | 		realmId = &realm.ID | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | 	if publishedAt == nil { | ||||||
|  | 		publishedAt = lo.ToPtr(time.Now()) | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	post = models.Post{ | 	post = models.Post{ | ||||||
| 		Alias:      alias, | 		Alias:       alias, | ||||||
| 		Title:      title, | 		Title:       title, | ||||||
| 		Content:    content, | 		Content:     content, | ||||||
| 		Tags:       tags, | 		Tags:        tags, | ||||||
| 		Categories: categories, | 		Categories:  categories, | ||||||
| 		AuthorID:   user.ID, | 		AuthorID:    user.ID, | ||||||
| 		RealmID:    realmId, | 		RealmID:     realmId, | ||||||
| 		RepostID:   repostTo, | 		PublishedAt: *publishedAt, | ||||||
| 		ReplyID:    replyTo, | 		RepostID:    repostTo, | ||||||
|  | 		ReplyID:     replyTo, | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	if err := database.C.Save(&post).Error; err != nil { | 	if err := database.C.Save(&post).Error; err != nil { | ||||||
| @@ -119,6 +138,41 @@ func NewPostWithRealm( | |||||||
| 	return post, nil | 	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) { | func LikePost(user models.Account, post models.Post) (bool, error) { | ||||||
| 	var like models.PostLike | 	var like models.PostLike | ||||||
| 	if err := database.C.Where(&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 | 		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, |   onRepost?: (post: any) => void, | ||||||
|   onReply?: (post: any) => void, |   onReply?: (post: any) => void, | ||||||
|   onEdit?: (post: any) => void, |   onEdit?: (post: any) => void, | ||||||
|  |   onDelete?: (post: any) => void, | ||||||
|   onError: (message: string | null) => void, |   onError: (message: string | null) => void, | ||||||
|   onReact: () => void |   onReact: () => void | ||||||
| }) { | }) { | ||||||
| @@ -127,6 +128,9 @@ export default function PostItem(props: { | |||||||
|                 <i class="fa-solid fa-ellipsis-vertical"></i> |                 <i class="fa-solid fa-ellipsis-vertical"></i> | ||||||
|               </div> |               </div> | ||||||
|               <ul tabIndex="0" class="dropdown-content z-[1] menu p-2 shadow bg-base-100 rounded-box w-52"> |               <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}> |                 <Show when={userinfo?.profiles?.id === props.post.author_id}> | ||||||
|                   <li><a onClick={() => props.onEdit && props.onEdit(props.post)}>Edit</a></li> |                   <li><a onClick={() => props.onEdit && props.onEdit(props.post)}>Edit</a></li> | ||||||
|                 </Show> |                 </Show> | ||||||
|   | |||||||
| @@ -2,6 +2,7 @@ import { createMemo, createSignal, For, Show } from "solid-js"; | |||||||
|  |  | ||||||
| import styles from "./PostList.module.css"; | import styles from "./PostList.module.css"; | ||||||
| import PostItem from "./PostItem.tsx"; | import PostItem from "./PostItem.tsx"; | ||||||
|  | import { getAtk } from "../stores/userinfo.tsx"; | ||||||
|  |  | ||||||
| export default function PostList(props: { | export default function PostList(props: { | ||||||
|   info: { data: any[], count: number } | null, |   info: { data: any[], count: number } | null, | ||||||
| @@ -27,6 +28,23 @@ export default function PostList(props: { | |||||||
|  |  | ||||||
|   readPosts(); |   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) { |   function changePage(pn: number) { | ||||||
|     setPage(pn); |     setPage(pn); | ||||||
|     readPosts().then(() => { |     readPosts().then(() => { | ||||||
| @@ -43,6 +61,7 @@ export default function PostList(props: { | |||||||
|             onRepost={props.onRepost} |             onRepost={props.onRepost} | ||||||
|             onReply={props.onReply} |             onReply={props.onReply} | ||||||
|             onEdit={props.onEdit} |             onEdit={props.onEdit} | ||||||
|  |             onDelete={deletePost} | ||||||
|             onReact={() => readPosts()} |             onReact={() => readPosts()} | ||||||
|             onError={props.onError} |             onError={props.onError} | ||||||
|           />} |           />} | ||||||
|   | |||||||
| @@ -7,6 +7,7 @@ export default function PostPublish(props: { | |||||||
|   replying?: any, |   replying?: any, | ||||||
|   reposting?: any, |   reposting?: any, | ||||||
|   editing?: any, |   editing?: any, | ||||||
|  |   onReset: () => void, | ||||||
|   onError: (message: string | null) => void, |   onError: (message: string | null) => void, | ||||||
|   onPost: () => void |   onPost: () => void | ||||||
| }) { | }) { | ||||||
| @@ -33,7 +34,37 @@ export default function PostPublish(props: { | |||||||
|         title: data.title, |         title: data.title, | ||||||
|         content: data.content, |         content: data.content, | ||||||
|         repost_to: props.reposting?.id, |         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) { |     if (res.status !== 200) { | ||||||
| @@ -47,7 +78,7 @@ export default function PostPublish(props: { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   return ( |   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 id="publish-identity" class="flex border-y border-base-200"> | ||||||
|         <div class="avatar pl-[20px]"> |         <div class="avatar pl-[20px]"> | ||||||
|           <div class="w-12"> |           <div class="w-12"> | ||||||
| @@ -58,7 +89,8 @@ export default function PostPublish(props: { | |||||||
|           </div> |           </div> | ||||||
|         </div> |         </div> | ||||||
|         <div class="flex flex-grow"> |         <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)" /> |                  placeholder="The describe for a long content (Optional)" /> | ||||||
|         </div> |         </div> | ||||||
|       </div> |       </div> | ||||||
| @@ -83,7 +115,8 @@ export default function PostPublish(props: { | |||||||
|         </div> |         </div> | ||||||
|       </Show> |       </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?!" /> |                 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"> | ||||||
| @@ -93,11 +126,16 @@ export default function PostPublish(props: { | |||||||
|           </button> |           </button> | ||||||
|         </div> |         </div> | ||||||
|  |  | ||||||
|         <button type="submit" class="btn btn-primary" disabled={submitting()}> |         <div> | ||||||
|           <Show when={submitting()} fallback={"Post a post"}> |           <button type="reset" class="btn btn-ghost w-12" disabled={submitting()}> | ||||||
|             <span class="loading"></span> |             <i class="fa-solid fa-xmark"></i> | ||||||
|           </Show> |           </button> | ||||||
|         </button> |           <button type="submit" class="btn btn-primary" disabled={submitting()}> | ||||||
|  |             <Show when={submitting()} fallback={props.editing ? "Save changes" : "Post a post"}> | ||||||
|  |               <span class="loading"></span> | ||||||
|  |             </Show> | ||||||
|  |           </button> | ||||||
|  |         </div> | ||||||
|       </div> |       </div> | ||||||
|     </form> |     </form> | ||||||
|   ); |   ); | ||||||
|   | |||||||
| @@ -49,6 +49,7 @@ export default function DashboardPage() { | |||||||
|         replying={publishMeta.replying} |         replying={publishMeta.replying} | ||||||
|         reposting={publishMeta.reposting} |         reposting={publishMeta.reposting} | ||||||
|         editing={publishMeta.editing} |         editing={publishMeta.editing} | ||||||
|  |         onReset={() => setPublishMeta({ reposting: null, replying: null, editing: null })} | ||||||
|         onPost={() => readPosts()} |         onPost={() => readPosts()} | ||||||
|         onError={setError} |         onError={setError} | ||||||
|       /> |       /> | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user