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