✨ Reply & Repost
This commit is contained in:
		| @@ -8,8 +8,8 @@ type Post struct { | ||||
| 	Alias            string        `json:"alias" gorm:"uniqueIndex"` | ||||
| 	Title            string        `json:"title"` | ||||
| 	Content          string        `json:"content"` | ||||
| 	Tags             []Tag         `gorm:"many2many:post_tags"` | ||||
| 	Categories       []Category    `gorm:"many2many:post_categories"` | ||||
| 	Tags             []Tag         `json:"tags" gorm:"many2many:post_tags"` | ||||
| 	Categories       []Category    `json:"categories" gorm:"many2many:post_categories"` | ||||
| 	LikedAccounts    []PostLike    `json:"liked_accounts"` | ||||
| 	DislikedAccounts []PostDislike `json:"disliked_accounts"` | ||||
| 	RepostTo         *Post         `json:"repost_to" gorm:"foreignKey:RepostID"` | ||||
|   | ||||
| @@ -1,13 +1,14 @@ | ||||
| package server | ||||
|  | ||||
| import ( | ||||
| 	"strings" | ||||
|  | ||||
| 	"code.smartsheep.studio/hydrogen/interactive/pkg/database" | ||||
| 	"code.smartsheep.studio/hydrogen/interactive/pkg/models" | ||||
| 	"code.smartsheep.studio/hydrogen/interactive/pkg/services" | ||||
| 	"github.com/gofiber/fiber/v2" | ||||
| 	"github.com/google/uuid" | ||||
| 	"github.com/samber/lo" | ||||
| 	"strings" | ||||
| ) | ||||
|  | ||||
| func listPost(c *fiber.Ctx) error { | ||||
| @@ -37,7 +38,6 @@ func listPost(c *fiber.Ctx) error { | ||||
| 		"count": count, | ||||
| 		"data":  posts, | ||||
| 	}) | ||||
|  | ||||
| } | ||||
|  | ||||
| func createPost(c *fiber.Ctx) error { | ||||
| @@ -49,6 +49,8 @@ func createPost(c *fiber.Ctx) error { | ||||
| 		Content    string            `json:"content" validate:"required"` | ||||
| 		Tags       []models.Tag      `json:"tags"` | ||||
| 		Categories []models.Category `json:"categories"` | ||||
| 		RepostTo   uint              `json:"repost_to"` | ||||
| 		ReplyTo    uint              `json:"reply_to"` | ||||
| 	} | ||||
|  | ||||
| 	if err := BindAndValidate(c, &data); err != nil { | ||||
| @@ -57,7 +59,32 @@ func createPost(c *fiber.Ctx) error { | ||||
| 		data.Alias = strings.ReplaceAll(uuid.NewString(), "-", "") | ||||
| 	} | ||||
|  | ||||
| 	post, err := services.NewPost(user, data.Alias, data.Title, data.Content, data.Categories, data.Tags) | ||||
| 	var repostTo *uint = nil | ||||
| 	var replyTo *uint = nil | ||||
| 	var relatedCount int64 | ||||
| 	if data.RepostTo > 0 { | ||||
| 		if err := database.C.Where(&models.Post{ | ||||
| 			BaseModel: models.BaseModel{ID: data.RepostTo}, | ||||
| 		}).Model(&models.Post{}).Count(&relatedCount).Error; err != nil { | ||||
| 			return fiber.NewError(fiber.StatusBadRequest, err.Error()) | ||||
| 		} else if relatedCount <= 0 { | ||||
| 			return fiber.NewError(fiber.StatusNotFound, "related post was not found") | ||||
| 		} else { | ||||
| 			repostTo = &data.RepostTo | ||||
| 		} | ||||
| 	} else if data.ReplyTo > 0 { | ||||
| 		if err := database.C.Where(&models.Post{ | ||||
| 			BaseModel: models.BaseModel{ID: data.ReplyTo}, | ||||
| 		}).Model(&models.Post{}).Count(&relatedCount).Error; err != nil { | ||||
| 			return fiber.NewError(fiber.StatusBadRequest, err.Error()) | ||||
| 		} else if relatedCount <= 0 { | ||||
| 			return fiber.NewError(fiber.StatusNotFound, "related post was not found") | ||||
| 		} else { | ||||
| 			replyTo = &data.ReplyTo | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	post, err := services.NewPost(user, data.Alias, data.Title, data.Content, data.Categories, data.Tags, replyTo, repostTo) | ||||
| 	if err != nil { | ||||
| 		return fiber.NewError(fiber.StatusBadRequest, err.Error()) | ||||
| 	} | ||||
|   | ||||
| @@ -1,10 +1,11 @@ | ||||
| package services | ||||
|  | ||||
| import ( | ||||
| 	"code.smartsheep.studio/hydrogen/interactive/pkg/database" | ||||
| 	"code.smartsheep.studio/hydrogen/interactive/pkg/models" | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
|  | ||||
| 	"code.smartsheep.studio/hydrogen/interactive/pkg/database" | ||||
| 	"code.smartsheep.studio/hydrogen/interactive/pkg/models" | ||||
| 	"github.com/samber/lo" | ||||
| 	"github.com/spf13/viper" | ||||
| 	"gorm.io/gorm" | ||||
| @@ -16,6 +17,10 @@ func ListPost(tx *gorm.DB, take int, offset int) ([]*models.Post, error) { | ||||
| 		Limit(take). | ||||
| 		Offset(offset). | ||||
| 		Preload("Author"). | ||||
| 		Preload("RepostTo"). | ||||
| 		Preload("ReplyTo"). | ||||
| 		Preload("RepostTo.Author"). | ||||
| 		Preload("ReplyTo.Author"). | ||||
| 		Find(&posts).Error; err != nil { | ||||
| 		return posts, err | ||||
| 	} | ||||
| @@ -62,8 +67,9 @@ func NewPost( | ||||
| 	alias, title, content string, | ||||
| 	categories []models.Category, | ||||
| 	tags []models.Tag, | ||||
| 	replyTo, repostTo *uint, | ||||
| ) (models.Post, error) { | ||||
| 	return NewPostWithRealm(user, nil, alias, title, content, categories, tags) | ||||
| 	return NewPostWithRealm(user, nil, alias, title, content, categories, tags, replyTo, repostTo) | ||||
| } | ||||
|  | ||||
| func NewPostWithRealm( | ||||
| @@ -72,6 +78,7 @@ func NewPostWithRealm( | ||||
| 	alias, title, content string, | ||||
| 	categories []models.Category, | ||||
| 	tags []models.Tag, | ||||
| 	replyTo, repostTo *uint, | ||||
| ) (models.Post, error) { | ||||
| 	var err error | ||||
| 	var post models.Post | ||||
| @@ -101,6 +108,8 @@ func NewPostWithRealm( | ||||
| 		Categories: categories, | ||||
| 		AuthorID:   user.ID, | ||||
| 		RealmID:    realmId, | ||||
| 		RepostID:   repostTo, | ||||
| 		ReplyID:    replyTo, | ||||
| 	} | ||||
|  | ||||
| 	if err := database.C.Save(&post).Error; err != nil { | ||||
|   | ||||
| @@ -1,7 +1,16 @@ | ||||
| import { createSignal, Show } from "solid-js"; | ||||
| import { getAtk, useUserinfo } from "../stores/userinfo.tsx"; | ||||
|  | ||||
| export default function PostItem(props: { post: any, onError: (message: string | null) => void, onReact: () => void }) { | ||||
| export default function PostItem(props: { | ||||
|   post: any, | ||||
|   noAuthor?: boolean, | ||||
|   noControl?: boolean, | ||||
|   onRepost?: (post: any) => void, | ||||
|   onReply?: (post: any) => void, | ||||
|   onEdit?: (post: any) => void, | ||||
|   onError: (message: string | null) => void, | ||||
|   onReact: () => void | ||||
| }) { | ||||
|   const [reacting, setReacting] = createSignal(false); | ||||
|  | ||||
|   const userinfo = useUserinfo(); | ||||
| @@ -23,7 +32,7 @@ export default function PostItem(props: { post: any, onError: (message: string | | ||||
|  | ||||
|   return ( | ||||
|     <div class="post-item"> | ||||
|  | ||||
|       <Show when={!props.noAuthor}> | ||||
|         <a href={`/accounts/${props.post.author.id}`}> | ||||
|           <div class="flex bg-base-200"> | ||||
|             <div class="avatar pl-[20px]"> | ||||
| @@ -42,14 +51,44 @@ export default function PostItem(props: { post: any, onError: (message: string | | ||||
|             </div> | ||||
|           </div> | ||||
|         </a> | ||||
|       </Show> | ||||
|  | ||||
|       <article class="py-5 px-7"> | ||||
|       <div class="py-5 px-7"> | ||||
|         <h2 class="card-title">{props.post.title}</h2> | ||||
|         <article class="prose">{props.post.content}</article> | ||||
|       </article> | ||||
|  | ||||
|         <Show when={props.post.repost_to}> | ||||
|           <p class="text-xs mt-3 mb-2"> | ||||
|             <i class="fa-solid fa-retweet me-2"></i> | ||||
|             Reposted a post | ||||
|           </p> | ||||
|           <div class="border border-base-200"> | ||||
|             <PostItem | ||||
|               noControl | ||||
|               post={props.post.repost_to} | ||||
|               onError={props.onError} | ||||
|               onReact={props.onReact} | ||||
|             /> | ||||
|           </div> | ||||
|         </Show> | ||||
|         <Show when={props.post.reply_to}> | ||||
|           <p class="text-xs mt-3 mb-2"> | ||||
|             <i class="fa-solid fa-reply me-2"></i> | ||||
|             Replied a post | ||||
|           </p> | ||||
|           <div class="border border-base-200"> | ||||
|             <PostItem | ||||
|               noControl | ||||
|               post={props.post.reply_to} | ||||
|               onError={props.onError} | ||||
|               onReact={props.onReact} | ||||
|             /> | ||||
|           </div> | ||||
|         </Show> | ||||
|       </div> | ||||
|  | ||||
|       <Show when={!props.noControl}> | ||||
|         <div class="grid grid-cols-3 border-y border-base-200"> | ||||
|  | ||||
|           <div class="grid grid-cols-2"> | ||||
|             <div class="tooltip" data-tip="Daisuki"> | ||||
|               <button type="button" class="btn btn-ghost btn-block" disabled={reacting()} | ||||
| @@ -70,13 +109,15 @@ export default function PostItem(props: { post: any, onError: (message: string | | ||||
|  | ||||
|           <div class="col-span-2 flex justify-end"> | ||||
|             <div class="tooltip" data-tip="Reply"> | ||||
|             <button type="button" class="btn btn-ghost btn-block"> | ||||
|               <button type="button" class="btn btn-ghost btn-block" | ||||
|                       onClick={() => props.onReply && props.onReply(props.post)}> | ||||
|                 <i class="fa-solid fa-reply"></i> | ||||
|               </button> | ||||
|             </div> | ||||
|  | ||||
|             <div class="tooltip" data-tip="Repost"> | ||||
|             <button type="button" class="btn btn-ghost btn-block"> | ||||
|               <button type="button" class="btn btn-ghost btn-block" | ||||
|                       onClick={() => props.onRepost && props.onRepost(props.post)}> | ||||
|                 <i class="fa-solid fa-retweet"></i> | ||||
|               </button> | ||||
|             </div> | ||||
| @@ -87,14 +128,14 @@ export default function PostItem(props: { post: any, onError: (message: string | | ||||
|               </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>Edit</a></li> | ||||
|                   <li><a onClick={() => props.onEdit && props.onEdit(props.post)}>Edit</a></li> | ||||
|                 </Show> | ||||
|                 <li><a>Report</a></li> | ||||
|               </ul> | ||||
|             </div> | ||||
|           </div> | ||||
|  | ||||
|         </div> | ||||
|       </Show> | ||||
|  | ||||
|     </div> | ||||
|   ); | ||||
|   | ||||
| @@ -5,13 +5,16 @@ import PostItem from "./PostItem.tsx"; | ||||
|  | ||||
| export default function PostList(props: { | ||||
|   info: { data: any[], count: number } | null, | ||||
|   onRepost?: (post: any) => void, | ||||
|   onReply?: (post: any) => void, | ||||
|   onEdit?: (post: any) => void, | ||||
|   onUpdate: (pn: number) => Promise<void>, | ||||
|   onError: (message: string | null) => void | ||||
| }) { | ||||
|   const [loading, setLoading] = createSignal(true); | ||||
|  | ||||
|   const posts = createMemo(() => props.info?.data) | ||||
|   const postCount = createMemo<number>(() => props.info?.count ?? 0) | ||||
|   const posts = createMemo(() => props.info?.data); | ||||
|   const postCount = createMemo<number>(() => props.info?.count ?? 0); | ||||
|  | ||||
|   const [page, setPage] = createSignal(1); | ||||
|   const pageCount = createMemo(() => Math.ceil(postCount() / 10)); | ||||
| @@ -35,7 +38,14 @@ export default function PostList(props: { | ||||
|     <div id="post-list"> | ||||
|       <div id="posts"> | ||||
|         <For each={posts()}> | ||||
|           {item => <PostItem post={item} onReact={() => readPosts()} onError={props.onError} />} | ||||
|           {item => <PostItem | ||||
|             post={item} | ||||
|             onRepost={props.onRepost} | ||||
|             onReply={props.onReply} | ||||
|             onEdit={props.onEdit} | ||||
|             onReact={() => readPosts()} | ||||
|             onError={props.onError} | ||||
|           />} | ||||
|         </For> | ||||
|  | ||||
|         <div class="flex justify-center"> | ||||
|   | ||||
| @@ -6,6 +6,7 @@ import styles from "./PostPublish.module.css"; | ||||
| export default function PostPublish(props: { | ||||
|   replying?: any, | ||||
|   reposting?: any, | ||||
|   editing?: any, | ||||
|   onError: (message: string | null) => void, | ||||
|   onPost: () => void | ||||
| }) { | ||||
| @@ -30,7 +31,9 @@ export default function PostPublish(props: { | ||||
|       body: JSON.stringify({ | ||||
|         alias: data.alias ?? crypto.randomUUID().replace(/-/g, ""), | ||||
|         title: data.title, | ||||
|         content: data.content | ||||
|         content: data.content, | ||||
|         repost_to: props.reposting?.id, | ||||
|         reply_to: props.replying?.id, | ||||
|       }) | ||||
|     }); | ||||
|     if (res.status !== 200) { | ||||
| @@ -60,6 +63,26 @@ export default function PostPublish(props: { | ||||
|         </div> | ||||
|       </div> | ||||
|  | ||||
|       <Show when={props.reposting}> | ||||
|         <div role="alert" class="px-5 py-3 bg-base-200"> | ||||
|           <i class="fa-solid fa-circle-info me-3"></i> | ||||
|           You are reposting a post from <b>{props.reposting?.author?.name}</b> | ||||
|         </div> | ||||
|       </Show> | ||||
|       <Show when={props.replying}> | ||||
|         <div role="alert" class="px-5 py-3 bg-base-200"> | ||||
|           <i class="fa-solid fa-circle-info me-3"></i> | ||||
|           You are replying a post from <b>{props.replying?.author?.name}</b> | ||||
|         </div> | ||||
|       </Show> | ||||
|       <Show when={props.editing}> | ||||
|         <div role="alert" class="px-5 py-3 bg-base-200"> | ||||
|           <i class="fa-solid fa-circle-info me-3"></i> | ||||
|           You are editing a post published at{" "} | ||||
|           <b>{new Date(props.editing?.created_at).toLocaleString()}</b> | ||||
|         </div> | ||||
|       </Show> | ||||
|  | ||||
|       <textarea name="content" class={`${styles.publishInput} textarea w-full`} | ||||
|                 placeholder="What's happend?!" /> | ||||
|  | ||||
|   | ||||
| @@ -2,6 +2,7 @@ import { createSignal, Show } from "solid-js"; | ||||
|  | ||||
| import PostList from "../components/PostList.tsx"; | ||||
| import PostPublish from "../components/PostPublish.tsx"; | ||||
| import { createStore } from "solid-js/store"; | ||||
|  | ||||
| export default function DashboardPage() { | ||||
|   const [error, setError] = createSignal<string | null>(null); | ||||
| @@ -23,6 +24,12 @@ export default function DashboardPage() { | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   const [publishMeta, setPublishMeta] = createStore<any>({ | ||||
|     replying: null, | ||||
|     reposting: null, | ||||
|     editing: null | ||||
|   }); | ||||
|  | ||||
|   return ( | ||||
|     <> | ||||
|       <div id="alerts"> | ||||
| @@ -38,9 +45,22 @@ export default function DashboardPage() { | ||||
|         </Show> | ||||
|       </div> | ||||
|  | ||||
|       <PostPublish onPost={() => readPosts()} onError={setError} /> | ||||
|       <PostPublish | ||||
|         replying={publishMeta.replying} | ||||
|         reposting={publishMeta.reposting} | ||||
|         editing={publishMeta.editing} | ||||
|         onPost={() => readPosts()} | ||||
|         onError={setError} | ||||
|       /> | ||||
|  | ||||
|       <PostList info={info()} onUpdate={readPosts} onError={setError} /> | ||||
|       <PostList | ||||
|         info={info()} | ||||
|         onUpdate={readPosts} | ||||
|         onError={setError} | ||||
|         onRepost={(item) => setPublishMeta({ reposting: item, replying: null, editing: null })} | ||||
|         onReply={(item) => setPublishMeta({ reposting: null, replying: item, editing: null })} | ||||
|         onEdit={(item) => setPublishMeta({ reposting: null, replying: null, editing: item })} | ||||
|       /> | ||||
|     </> | ||||
|   ); | ||||
| } | ||||
		Reference in New Issue
	
	Block a user