From aa0405ee98c45a9bb25dd9959d899bbd56bc1b02 Mon Sep 17 00:00:00 2001 From: Bobby Date: Fri, 18 Jul 2025 17:07:23 +0530 Subject: =?UTF-8?q?favourite=20system=20and=20=E2=88=82etails=20on=20singl?= =?UTF-8?q?e=20page?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- controllers/posts.go | 46 ++++++++++++++++++++++++++++++++-- models/image.go | 57 ++++++++++++++++++++++++------------------- models/user.go | 1 + router/routes.go | 1 + static/css/main.css | 53 ++++++++++++++++++++++++++++++++++++++++ templates/posts/list.django | 4 +-- templates/posts/single.django | 57 +++++++++++++++++++++++++++++++++++++++++-- utils/auth/auth.go | 4 +++ 8 files changed, 192 insertions(+), 31 deletions(-) diff --git a/controllers/posts.go b/controllers/posts.go index 11525b8..41bbc86 100644 --- a/controllers/posts.go +++ b/controllers/posts.go @@ -299,9 +299,51 @@ func PostsSinglePostPageController(ctx *fiber.Ctx) error { return renderSinglePostError(ctx, "Failed to retrieve post. "+err.Error(), fiber.StatusInternalServerError) } + currentUser := auth.GetCurrentUser(ctx) + isUserFavourited := false + if currentUser != nil { + isUserFavourited = post.IsUserFavourited(database.DB, currentUser) + } + ctx.Locals("Title", config.PT_POST_SINGLE+" #"+format.Int64ToString(int64(post.ID))) return shortcuts.Render(ctx, config.TEMPLATE_POST_SINGLE, fiber.Map{ - "Post": post, - "CDNURL": format.GetCDNURL(), + "Post": post, + "CDNURL": format.GetCDNURL(), + "IsUserFavourited": isUserFavourited, }) } + +func PostsSinglePostFavouriteController(ctx *fiber.Ctx) error { + if !auth.IsAuthenticated(ctx) { + return ctx.Redirect(auth.GetLoginURLWithNextField(ctx), fiber.StatusFound) + } + + postID := ctx.Params("id") + if postID == "" { + return renderSinglePostError(ctx, "Post ID is required", fiber.StatusBadRequest) + } + + uintPostID, err := format.StringToUint(postID) + if err != nil { + return renderSinglePostError(ctx, "Invalid Post ID", fiber.StatusBadRequest) + } + + post, err := database.GetPostByID(uintPostID) + if err != nil { + if err.Error() == "record not found" { + return renderSinglePostError(ctx, "Post not found", fiber.StatusNotFound) + } + return renderSinglePostError(ctx, "Failed to retrieve post. "+err.Error(), fiber.StatusInternalServerError) + } + + currentUser := auth.GetCurrentUser(ctx) + if currentUser == nil { + return renderSinglePostError(ctx, "User not found", fiber.StatusUnauthorized) + } + + if err := post.ToggleFavourite(database.DB, currentUser); err != nil { + return renderSinglePostError(ctx, "Failed to toggle favourite. "+err.Error(), fiber.StatusInternalServerError) + } + + return ctx.Redirect("/posts/" + postID) +} diff --git a/models/image.go b/models/image.go index 17f369d..35b407e 100644 --- a/models/image.go +++ b/models/image.go @@ -175,31 +175,6 @@ func (i *Image) GetAspectRatio() string { return "Unknown" } -func (i *Image) AddSize(tx *gorm.DB, sizeType config.ImageSizeType, width, height int, fileSize int64) (*ImageSize, error) { - if width <= 0 || height <= 0 { - return nil, fmt.Errorf("image dimensions must be greater than zero") - } - - if fileSize <= 0 { - return nil, fmt.Errorf("file size must be greater than zero") - } - - size := &ImageSize{ - ImageID: i.ID, - SizeType: sizeType, - Width: width, - Height: height, - FileSize: fileSize, - } - - if err := tx.Create(size).Error; err != nil { - return nil, fmt.Errorf("failed to create image size: %v", err) - } - - i.Sizes = append(i.Sizes, *size) - return size, nil -} - func (i *Image) AddRelatedImage(tx *gorm.DB, relatedImage *Image) error { if relatedImage.ID == 0 { return fmt.Errorf("related image must be saved before adding relationship") @@ -319,3 +294,35 @@ func (i *Image) DeleteImage(tx *gorm.DB) error { i.IsDeleted = true return tx.Save(i).Error } + +func (i *Image) ToggleFavourite(tx *gorm.DB, user *User) error { + if i.IsDeleted { + return fmt.Errorf("cannot favourite deleted image") + } + + var count int64 + if err := tx.Table("user_favorites").Where("user_id = ? AND image_id = ?", user.ID, i.ID).Count(&count).Error; err != nil { + return err + } + + if count > 0 { + if err := tx.Model(user).Association("FavoritedImages").Delete(i); err != nil { + return err + } + return tx.Model(i).UpdateColumn("favourite_count", gorm.Expr("GREATEST(favourite_count - ?, 0)", 1)).Error + } else { + if err := tx.Model(user).Association("FavoritedImages").Append(i); err != nil { + return err + } + return tx.Model(i).UpdateColumn("favourite_count", gorm.Expr("favourite_count + ?", 1)).Error + } +} + +func (i *Image) IsUserFavourited(tx *gorm.DB, user *User) bool { + if user == nil { + return false + } + var count int64 + tx.Table("user_favorites").Where("user_id = ? AND image_id = ?", user.ID, i.ID).Count(&count) + return count > 0 +} diff --git a/models/user.go b/models/user.go index ecb139f..d918db7 100644 --- a/models/user.go +++ b/models/user.go @@ -30,6 +30,7 @@ type User struct { LastLoginAt *time.Time `gorm:"default:null" json:"last_login_at"` LastActivityAt *time.Time `gorm:"default:null" json:"last_activity_at"` Images []Image `gorm:"foreignKey:UploaderID" json:"images,omitempty"` + FavoritedImages []Image `gorm:"many2many:user_favorites" json:"favorited_images,omitempty"` } func (u *User) BeforeCreate(tx *gorm.DB) error { diff --git a/router/routes.go b/router/routes.go index 786e592..b573ad9 100644 --- a/router/routes.go +++ b/router/routes.go @@ -18,6 +18,7 @@ func Initialize(router *fiber.App) { posts.Post("/new", controllers.PostsUploadPostController) posts.Get("/new/ilinkfetch", controllers.PostsUploadImageLinkProxyController) posts.Get("/:id", controllers.PostsSinglePostPageController) + posts.Post("/:id/favourite", controllers.PostsSinglePostFavouriteController) login := router.Group("/login") login.Get("/", controllers.LoginPageController) diff --git a/static/css/main.css b/static/css/main.css index c6cb393..cdd39e5 100644 --- a/static/css/main.css +++ b/static/css/main.css @@ -845,6 +845,7 @@ footer::before { #post-image { display: block; height: auto; + margin-bottom: 8px; } #post-image.fit-width { @@ -867,4 +868,56 @@ footer::before { width: auto; height: auto; max-width: none; +} + +.post-details { + margin: 4px auto; + display: flex; + flex-direction: row; + align-items: center; + gap: 16px; +} + +.post-detail-item { + display: flex; + align-items: center; + gap: 4px; +} + +.post-detail-label { + color: #cccccc; + white-space: nowrap; +} + +.post-favourite-actions { + display: flex; + align-items: center; + gap: 4px; +} + +.post-favourite-actions form { + margin: 0; + padding: 0; +} + +.icon-button { + background: none; + border: none; + cursor: pointer; + padding: 2px; + display: flex; + align-items: center; + justify-content: center; + color: #99ccff; + transition: color 0.3s ease; +} + +.icon-button:hover { + color: #99ffcc; +} + +.icon-button svg { + width: 16px; + height: 16px; + fill: currentColor; } \ No newline at end of file diff --git a/templates/posts/list.django b/templates/posts/list.django index 3aca8d2..bafd27e 100644 --- a/templates/posts/list.django +++ b/templates/posts/list.django @@ -9,8 +9,8 @@ {% if Posts %}
{% for image in Posts %} - - {{ image.Title }} + + {{ image.Title }}
ID: {{ image.ID }}
diff --git a/templates/posts/single.django b/templates/posts/single.django index ef6bc35..b805af8 100644 --- a/templates/posts/single.django +++ b/templates/posts/single.django @@ -82,6 +82,9 @@
Medium Large Original + {% if Post.Uploader.Username == User.Username or User.CanEditTags %} + | Edit Post + {% endif %}
@@ -98,11 +101,61 @@
Created: - {{ Post.CreatedAt|naturaltime }} + {{ Post.CreatedAt|naturaltime }}
Original Size: - {{ Post.GetOriginalSize.Width }}x{{ Post.GetOriginalSize.Height }} ({{ Post.GetOriginalSize.GetFileSizeFormatted }}) + {{ Post.GetOriginalSize.Width }}x{{ Post.GetOriginalSize.Height }} ({{ Post.GetOriginalSize.GetFileSizeFormatted }}) +
+
+ Favourites: + + {{ Post.FavouriteCount }} +
+ + +
+
+
+
+ Rating: + {{ Post.Rating }} +
+
+
+
+ Source: + + {% if Post.SourceURL %} + {{ Post.SourceURL }} + {% else %} + N/A + {% endif %} + +
+
+
+
+ Tags: + + {% if Post.Tags %} + {% for tag in Post.Tags %} + + {% endfor %} + {% else %} + No tags + {% endif %} +
diff --git a/utils/auth/auth.go b/utils/auth/auth.go index f92e955..82b7353 100644 --- a/utils/auth/auth.go +++ b/utils/auth/auth.go @@ -54,6 +54,10 @@ func GetLoginURLWithRedirect(ctx *fiber.Ctx) string { return config.URL_LOGIN + "?next=" + url.QueryEscape(currentPath) } +func GetLoginURLWithNextField(ctx *fiber.Ctx) string { + return config.URL_LOGIN + "?next=" + url.QueryEscape(GetRedirectURL(ctx)) +} + func GetLogoutURLWithRedirect(ctx *fiber.Ctx) string { currentPath := ctx.Path() if queryString := string(ctx.Request().URI().QueryString()); queryString != "" { -- cgit v1.2.3