♻️ Interactive v2 #1
							
								
								
									
										46
									
								
								.air.toml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								.air.toml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,46 @@ | |||||||
|  | root = "." | ||||||
|  | testdata_dir = "testdata" | ||||||
|  | tmp_dir = "dist" | ||||||
|  |  | ||||||
|  | [build] | ||||||
|  |   args_bin = [] | ||||||
|  |   bin = "./dist/server" | ||||||
|  |   cmd = "go build -o ./dist/server ./pkg/cmd/main.go" | ||||||
|  |   delay = 1000 | ||||||
|  |   exclude_dir = ["assets", "tmp", "vendor", "testdata", "pkg/views"] | ||||||
|  |   exclude_file = [] | ||||||
|  |   exclude_regex = ["_test.go"] | ||||||
|  |   exclude_unchanged = false | ||||||
|  |   follow_symlink = false | ||||||
|  |   full_bin = "" | ||||||
|  |   include_dir = [] | ||||||
|  |   include_ext = ["go", "tpl", "tmpl", "html"] | ||||||
|  |   include_file = [] | ||||||
|  |   kill_delay = "0s" | ||||||
|  |   log = "build-errors.log" | ||||||
|  |   poll = false | ||||||
|  |   poll_interval = 0 | ||||||
|  |   post_cmd = [] | ||||||
|  |   pre_cmd = [] | ||||||
|  |   rerun = false | ||||||
|  |   rerun_delay = 500 | ||||||
|  |   send_interrupt = false | ||||||
|  |   stop_on_error = false | ||||||
|  |  | ||||||
|  | [color] | ||||||
|  |   app = "" | ||||||
|  |   build = "yellow" | ||||||
|  |   main = "magenta" | ||||||
|  |   runner = "green" | ||||||
|  |   watcher = "cyan" | ||||||
|  |  | ||||||
|  | [log] | ||||||
|  |   main_only = false | ||||||
|  |   time = false | ||||||
|  |  | ||||||
|  | [misc] | ||||||
|  |   clean_on_exit = false | ||||||
|  |  | ||||||
|  | [screen] | ||||||
|  |   clear_on_rebuild = false | ||||||
|  |   keep_scroll = true | ||||||
							
								
								
									
										3
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										3
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -1 +1,4 @@ | |||||||
| /uploads | /uploads | ||||||
|  | /dist | ||||||
|  |  | ||||||
|  | .DS_Store | ||||||
|   | |||||||
| @@ -22,6 +22,37 @@ func contextComment() *services.PostTypeContext { | |||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
|  | func listComment(c *fiber.Ctx) error { | ||||||
|  | 	take := c.QueryInt("take", 0) | ||||||
|  | 	offset := c.QueryInt("offset", 0) | ||||||
|  | 	noReact := c.QueryBool("noReact", false) | ||||||
|  |  | ||||||
|  | 	alias := c.Params("postId") | ||||||
|  |  | ||||||
|  | 	mx := c.Locals(postContextKey).(*services.PostTypeContext). | ||||||
|  | 		FilterPublishedAt(time.Now()) | ||||||
|  |  | ||||||
|  | 	item, err := mx.GetViaAlias(alias) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return fiber.NewError(fiber.StatusNotFound, err.Error()) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	data, err := mx.ListComment(item.ID, take, offset, noReact) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return fiber.NewError(fiber.StatusInternalServerError, err.Error()) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	count, err := mx.CountComment(item.ID) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return fiber.NewError(fiber.StatusInternalServerError, err.Error()) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return c.JSON(fiber.Map{ | ||||||
|  | 		"count": count, | ||||||
|  | 		"data":  data, | ||||||
|  | 	}) | ||||||
|  | } | ||||||
|  |  | ||||||
| func createComment(c *fiber.Ctx) error { | func createComment(c *fiber.Ctx) error { | ||||||
| 	user := c.Locals("principal").(models.Account) | 	user := c.Locals("principal").(models.Account) | ||||||
|  |  | ||||||
| @@ -32,8 +63,6 @@ func createComment(c *fiber.Ctx) error { | |||||||
| 		Categories  []models.Category   `json:"categories"` | 		Categories  []models.Category   `json:"categories"` | ||||||
| 		Attachments []models.Attachment `json:"attachments"` | 		Attachments []models.Attachment `json:"attachments"` | ||||||
| 		PublishedAt *time.Time          `json:"published_at"` | 		PublishedAt *time.Time          `json:"published_at"` | ||||||
| 		ArticleID   *uint               `json:"article_id"` |  | ||||||
| 		MomentID    *uint               `json:"moment_id"` |  | ||||||
| 		ReplyTo     uint                `json:"reply_to"` | 		ReplyTo     uint                `json:"reply_to"` | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| @@ -55,13 +84,29 @@ func createComment(c *fiber.Ctx) error { | |||||||
| 		Content:    data.Content, | 		Content:    data.Content, | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	if data.ArticleID == nil && data.MomentID == nil { | 	postType := c.Params("postType") | ||||||
|  | 	alias := c.Params("postId") | ||||||
|  |  | ||||||
|  | 	var err error | ||||||
|  | 	var res models.Feed | ||||||
|  |  | ||||||
|  | 	switch postType { | ||||||
|  | 	case "moments": | ||||||
|  | 		err = database.C.Model(&models.Moment{}).Where("alias = ?", alias).Select("id").First(&res).Error | ||||||
|  | 	case "articles": | ||||||
|  | 		err = database.C.Model(&models.Article{}).Where("alias = ?", alias).Select("id").First(&res).Error | ||||||
|  | 	default: | ||||||
| 		return fiber.NewError(fiber.StatusBadRequest, "comment must belongs to a resource") | 		return fiber.NewError(fiber.StatusBadRequest, "comment must belongs to a resource") | ||||||
| 	} | 	} | ||||||
| 	if data.ArticleID != nil { |  | ||||||
| 		var article models.Article | 	if err != nil { | ||||||
| 		if err := database.C.Where("id = ?", data.ArticleID).First(&article).Error; err != nil { |  | ||||||
| 		return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("belongs to resource was not found: %v", err)) | 		return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("belongs to resource was not found: %v", err)) | ||||||
|  | 	} else { | ||||||
|  | 		switch postType { | ||||||
|  | 		case "moments": | ||||||
|  | 			item.MomentID = &res.ID | ||||||
|  | 		case "articles": | ||||||
|  | 			item.ArticleID = &res.ID | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| @@ -77,12 +122,11 @@ func createComment(c *fiber.Ctx) error { | |||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	item, err := services.NewPost(item) | 	if item, err := services.NewPost(item); err != nil { | ||||||
| 	if err != nil { |  | ||||||
| 		return fiber.NewError(fiber.StatusBadRequest, err.Error()) | 		return fiber.NewError(fiber.StatusBadRequest, err.Error()) | ||||||
| 	} | 	} else { | ||||||
|  |  | ||||||
| 		return c.JSON(item) | 		return c.JSON(item) | ||||||
|  | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
| func editComment(c *fiber.Ctx) error { | func editComment(c *fiber.Ctx) error { | ||||||
|   | |||||||
| @@ -1,9 +1,10 @@ | |||||||
| package server | package server | ||||||
|  |  | ||||||
| import ( | import ( | ||||||
|  | 	"fmt" | ||||||
|  |  | ||||||
| 	"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" | ||||||
| 	"fmt" |  | ||||||
| 	"github.com/gofiber/fiber/v2" | 	"github.com/gofiber/fiber/v2" | ||||||
| 	"github.com/samber/lo" | 	"github.com/samber/lo" | ||||||
| 	"github.com/spf13/viper" | 	"github.com/spf13/viper" | ||||||
|   | |||||||
| @@ -31,7 +31,8 @@ func useDynamicContext(c *fiber.Ctx) error { | |||||||
| func getPost(c *fiber.Ctx) error { | func getPost(c *fiber.Ctx) error { | ||||||
| 	alias := c.Params("postId") | 	alias := c.Params("postId") | ||||||
|  |  | ||||||
| 	mx := c.Locals(postContextKey).(*services.PostTypeContext) | 	mx := c.Locals(postContextKey).(*services.PostTypeContext). | ||||||
|  | 		FilterPublishedAt(time.Now()) | ||||||
|  |  | ||||||
| 	item, err := mx.GetViaAlias(alias) | 	item, err := mx.GetViaAlias(alias) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
|   | |||||||
| @@ -76,6 +76,8 @@ func NewServer() { | |||||||
| 			posts.Get("/", listPost) | 			posts.Get("/", listPost) | ||||||
| 			posts.Get("/:postId", getPost) | 			posts.Get("/:postId", getPost) | ||||||
| 			posts.Post("/:postId/react", authMiddleware, reactPost) | 			posts.Post("/:postId/react", authMiddleware, reactPost) | ||||||
|  | 			posts.Get("/:postId/comments", listComment) | ||||||
|  | 			posts.Post("/:postId/comments", authMiddleware, createComment) | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		moments := api.Group("/moments").Name("Moments API") | 		moments := api.Group("/moments").Name("Moments API") | ||||||
| @@ -94,7 +96,6 @@ func NewServer() { | |||||||
|  |  | ||||||
| 		comments := api.Group("/p/comments").Name("Comments API") | 		comments := api.Group("/p/comments").Name("Comments API") | ||||||
| 		{ | 		{ | ||||||
| 			comments.Post("/", authMiddleware, createComment) |  | ||||||
| 			comments.Put("/:commentId", authMiddleware, editComment) | 			comments.Put("/:commentId", authMiddleware, editComment) | ||||||
| 			comments.Delete("/:commentId", authMiddleware, deleteComment) | 			comments.Delete("/:commentId", authMiddleware, deleteComment) | ||||||
| 		} | 		} | ||||||
|   | |||||||
							
								
								
									
										82
									
								
								pkg/services/comments.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										82
									
								
								pkg/services/comments.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,82 @@ | |||||||
|  | package services | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"fmt" | ||||||
|  | 	"time" | ||||||
|  |  | ||||||
|  | 	"code.smartsheep.studio/hydrogen/interactive/pkg/database" | ||||||
|  | 	"code.smartsheep.studio/hydrogen/interactive/pkg/models" | ||||||
|  | 	"github.com/samber/lo" | ||||||
|  | 	"github.com/spf13/viper" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | func (v *PostTypeContext) ListComment(id uint, take int, offset int, noReact ...bool) ([]*models.Feed, error) { | ||||||
|  | 	if take > 20 { | ||||||
|  | 		take = 20 | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	var items []*models.Feed | ||||||
|  | 	table := viper.GetString("database.prefix") + "comments" | ||||||
|  | 	userTable := viper.GetString("database.prefix") + "accounts" | ||||||
|  | 	if err := v.Tx. | ||||||
|  | 		Table(table). | ||||||
|  | 		Select("*, ? as model_type", "comments"). | ||||||
|  | 		Where(v.ColumnName+"_id = ?", id). | ||||||
|  | 		Joins(fmt.Sprintf("INNER JOIN %s as author ON author_id = author.id", userTable)). | ||||||
|  | 		Limit(take).Offset(offset).Find(&items).Error; err != nil { | ||||||
|  | 		return items, err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	idx := lo.Map(items, func(item *models.Feed, index int) uint { | ||||||
|  | 		return item.ID | ||||||
|  | 	}) | ||||||
|  |  | ||||||
|  | 	if len(noReact) <= 0 || !noReact[0] { | ||||||
|  | 		var reactions []struct { | ||||||
|  | 			PostID uint | ||||||
|  | 			Symbol string | ||||||
|  | 			Count  int64 | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		if err := database.C.Model(&models.Reaction{}). | ||||||
|  | 			Select("comment_id as post_id, symbol, COUNT(id) as count"). | ||||||
|  | 			Where("comment_id IN (?)", idx). | ||||||
|  | 			Group("post_id, symbol"). | ||||||
|  | 			Scan(&reactions).Error; err != nil { | ||||||
|  | 			return items, err | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		itemMap := lo.SliceToMap(items, func(item *models.Feed) (uint, *models.Feed) { | ||||||
|  | 			return item.ID, item | ||||||
|  | 		}) | ||||||
|  |  | ||||||
|  | 		list := map[uint]map[string]int64{} | ||||||
|  | 		for _, info := range reactions { | ||||||
|  | 			if _, ok := list[info.PostID]; !ok { | ||||||
|  | 				list[info.PostID] = make(map[string]int64) | ||||||
|  | 			} | ||||||
|  | 			list[info.PostID][info.Symbol] = info.Count | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		for k, v := range list { | ||||||
|  | 			if post, ok := itemMap[k]; ok { | ||||||
|  | 				post.ReactionList = v | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return items, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (v *PostTypeContext) CountComment(id uint) (int64, error) { | ||||||
|  | 	var count int64 | ||||||
|  | 	if err := database.C. | ||||||
|  | 		Model(&models.Comment{}). | ||||||
|  | 		Where(v.ColumnName+"_id = ?", id). | ||||||
|  | 		Where("published_at <= ?", time.Now()). | ||||||
|  | 		Count(&count).Error; err != nil { | ||||||
|  | 		return count, err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return count, nil | ||||||
|  | } | ||||||
| @@ -72,9 +72,13 @@ func (v *PostTypeContext) SortCreatedAt(order string) *PostTypeContext { | |||||||
| 	return v | 	return v | ||||||
| } | } | ||||||
|  |  | ||||||
| func (v *PostTypeContext) GetViaAlias(alias string, noComments ...bool) (models.Feed, error) { | func (v *PostTypeContext) GetViaAlias(alias string) (models.Feed, error) { | ||||||
| 	var item models.Feed | 	var item models.Feed | ||||||
| 	if err := v.Tx.Where("alias = ?", alias).First(&item).Error; err != nil { | 	table := viper.GetString("database.prefix") + v.TableName | ||||||
|  | 	if err := v.Tx. | ||||||
|  | 		Table(table). | ||||||
|  | 		Where("alias = ?", alias). | ||||||
|  | 		First(&item).Error; err != nil { | ||||||
| 		return item, err | 		return item, err | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| @@ -83,7 +87,9 @@ func (v *PostTypeContext) GetViaAlias(alias string, noComments ...bool) (models. | |||||||
|  |  | ||||||
| func (v *PostTypeContext) Get(id uint, noComments ...bool) (models.Feed, error) { | func (v *PostTypeContext) Get(id uint, noComments ...bool) (models.Feed, error) { | ||||||
| 	var item models.Feed | 	var item models.Feed | ||||||
|  | 	table := viper.GetString("database.prefix") + v.TableName | ||||||
| 	if err := v.Tx. | 	if err := v.Tx. | ||||||
|  | 		Table(table). | ||||||
| 		Select("*, ? as model_type", v.ColumnName). | 		Select("*, ? as model_type", v.ColumnName). | ||||||
| 		Where("id = ?", id).First(&item).Error; err != nil { | 		Where("id = ?", id).First(&item).Error; err != nil { | ||||||
| 		return item, err | 		return item, err | ||||||
| @@ -131,7 +137,9 @@ func (v *PostTypeContext) List(take int, offset int, noReact ...bool) ([]*models | |||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	var items []*models.Feed | 	var items []*models.Feed | ||||||
|  | 	table := viper.GetString("database.prefix") + v.TableName | ||||||
| 	if err := v.Tx. | 	if err := v.Tx. | ||||||
|  | 		Table(table). | ||||||
| 		Select("*, ? as model_type", v.ColumnName). | 		Select("*, ? as model_type", v.ColumnName). | ||||||
| 		Limit(take).Offset(offset).Find(&items).Error; err != nil { | 		Limit(take).Offset(offset).Find(&items).Error; err != nil { | ||||||
| 		return items, err | 		return items, err | ||||||
|   | |||||||
| @@ -24,12 +24,8 @@ | |||||||
|  |  | ||||||
|       <v-card title="Reactions" class="mt-3"> |       <v-card title="Reactions" class="mt-3"> | ||||||
|         <div class="px-[1rem] pb-[0.825rem] mt-[-12px]"> |         <div class="px-[1rem] pb-[0.825rem] mt-[-12px]"> | ||||||
|           <post-reaction |           <post-reaction :item="post" :model="route.params.postType" :reactions="post?.reaction_list ?? {}" | ||||||
|             :item="post" |             @update="updateReactions" /> | ||||||
|             :model="route.params.postType" |  | ||||||
|             :reactions="post?.reaction_list ?? {}" |  | ||||||
|             @update="updateReactions" |  | ||||||
|           /> |  | ||||||
|         </div> |         </div> | ||||||
|       </v-card> |       </v-card> | ||||||
|     </div> |     </div> | ||||||
| @@ -51,7 +47,7 @@ const route = useRoute(); | |||||||
|  |  | ||||||
| async function readPost() { | async function readPost() { | ||||||
|   loading.value = true; |   loading.value = true; | ||||||
|   const res = await request(`/api/${route.params.postType}/${route.params.alias}?`); |   const res = await request(`/api/p/${route.params.postType}/${route.params.alias}?`); | ||||||
|   if (res.status !== 200) { |   if (res.status !== 200) { | ||||||
|     error.value = await res.text(); |     error.value = await res.text(); | ||||||
|   } else { |   } else { | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user