diff options
| author | Bobby <[email protected]> | 2025-07-18 17:07:23 +0530 |
|---|---|---|
| committer | Bobby <[email protected]> | 2025-07-18 17:07:23 +0530 |
| commit | aa0405ee98c45a9bb25dd9959d899bbd56bc1b02 (patch) | |
| tree | c6b75124708f3a3ab5fecbdb454eb5f530dd2ffa | |
| parent | 821773b12c07a4bc23628e7d98ac4b34da1eb9e1 (diff) | |
| download | imageboard-aa0405ee98c45a9bb25dd9959d899bbd56bc1b02.tar.xz imageboard-aa0405ee98c45a9bb25dd9959d899bbd56bc1b02.zip | |
favourite system and ∂etails on single page
| -rw-r--r-- | controllers/posts.go | 46 | ||||
| -rw-r--r-- | models/image.go | 57 | ||||
| -rw-r--r-- | models/user.go | 1 | ||||
| -rw-r--r-- | router/routes.go | 1 | ||||
| -rw-r--r-- | static/css/main.css | 53 | ||||
| -rw-r--r-- | templates/posts/list.django | 4 | ||||
| -rw-r--r-- | templates/posts/single.django | 57 | ||||
| -rw-r--r-- | 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 %} <div class="post-list"> {% for image in Posts %} - <a href="/posts/{{ image.ID }}" class="post-item"> - <img src="{{ CDNURL }}/thumbnail/{{ image.FileName }}" alt="{{ image.Title }}" width="{{ image.GetThumbnailSize.Width }}" height="{{ image.GetThumbnailSize.Height }}" /> + <a href="/posts/{{ image.ID }}" class="post-item" width="{{ image.Sizes.1.Width }}" height="{{ image.Sizes.1.Height }}"> + <img src="{{ CDNURL }}/thumbnail/{{ image.FileName }}" alt="{{ image.Title }}" width="{{ image.Sizes.1.Width }}" height="{{ image.Sizes.1.Height }}" /> <div class="post-overlay"> <div class="post-overlay-top"> <div class="post-id">ID: {{ image.ID }}</div> 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 @@ <a href="javascript:void(0);" onclick="switchSize('medium');">Medium</a> <a href="javascript:void(0);" onclick="switchSize('large');">Large</a> <a href="javascript:void(0);" onclick="switchSize('original');">Original</a> + {% if Post.Uploader.Username == User.Username or User.CanEditTags %} + | <a href="/posts/edit/{{ Post.ID }}#tags">Edit Post</a> + {% endif %} </div> </div> <div class="post-image-container"> @@ -98,11 +101,61 @@ </div> <div class="post-detail-item"> <span class="post-detail-label">Created:</span> - <span class="post-detail-value">{{ Post.CreatedAt|naturaltime }}</span> + <span class="post-detail-value"><a href="/posts?date={{ Post.CreatedAt|date:'2006-01-02' }}">{{ Post.CreatedAt|naturaltime }}</a></span> </div> <div class="post-detail-item"> <span class="post-detail-label">Original Size:</span> - <span class="post-detail-value">{{ Post.GetOriginalSize.Width }}x{{ Post.GetOriginalSize.Height }} ({{ Post.GetOriginalSize.GetFileSizeFormatted }})</span> + <span class="post-detail-value"><a href="{{ CDNURL }}/original/{{ Post.FileName }}" target="_blank">{{ Post.GetOriginalSize.Width }}x{{ Post.GetOriginalSize.Height }} ({{ Post.GetOriginalSize.GetFileSizeFormatted }})</a></span> + </div> + <div class="post-detail-item"> + <span class="post-detail-label">Favourites:</span> + <span class="post-detail-value post-favourite-actions"> + {{ Post.FavouriteCount }} + <form action="/posts/{{ Post.ID }}/favourite" method="post"> + <input type="hidden" name="next" value="{{ Request.Path }}" /> + <button type="submit" class="icon-button" title="{{ IsUserFavourited|yesno:'Unfavourite this post,Favourite this post' }}"> + {% if IsUserFavourited %} + <svg viewBox="0 0 24 24"> + <path d="M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z" /> + </svg> + {% else %} + <svg viewBox="0 0 24 24"> + <path d="M16.5 3c-1.74 0-3.41.81-4.5 2.09C10.91 3.81 9.24 3 7.5 3 4.42 3 2 5.42 2 8.5c0 3.78 3.4 6.86 8.55 11.54L12 21.35l1.45-1.32C18.6 15.36 22 12.28 22 8.5 22 5.42 19.58 3 16.5 3zm-4.4 15.55l-.1.1-.1-.1C7.14 14.24 4 11.39 4 8.5 4 6.5 5.5 5 7.5 5c1.54 0 3.04.99 3.57 2.36h1.87C13.46 5.99 14.96 5 16.5 5c2 0 3.5 1.5 3.5 3.5 0 2.89-3.14 5.74-7.9 10.05z" /> + </svg> + {% endif %} + </button> + </form> + </span> + </div> + <div class="post-detail-item"> + <span class="post-detail-label">Rating:</span> + <span class="post-detail-value post-rating {{ Post.Rating }}">{{ Post.Rating }}</span> + </div> + </div> + <div class="post-details"> + <div class="post-detail-item"> + <span class="post-detail-label">Source:</span> + <span class="post-detail-value"> + {% if Post.SourceURL %} + <a href="{{ Post.SourceURL }}" target="_blank">{{ Post.SourceURL }}</a> + {% else %} + N/A + {% endif %} + </span> + </div> + </div> + <div class="post-details"> + <div class="post-detail-item"> + <span class="post-detail-label">Tags:</span> + <span class="post-detail-value"> + {% if Post.Tags %} + {% for tag in Post.Tags %} + <a href="/posts?tags={{ tag.Name }}" style="color: {{ tag.Type.Color }};" class="post-tag">{{ tag.Name }} <span class="tag-count">({{ tag.Count }})</span></a> + {% endfor %} + {% else %} + No tags + {% endif %} + </span> </div> </div> </div> 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 != "" { |
