aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBobby <[email protected]>2025-07-18 17:07:23 +0530
committerBobby <[email protected]>2025-07-18 17:07:23 +0530
commitaa0405ee98c45a9bb25dd9959d899bbd56bc1b02 (patch)
treec6b75124708f3a3ab5fecbdb454eb5f530dd2ffa
parent821773b12c07a4bc23628e7d98ac4b34da1eb9e1 (diff)
downloadimageboard-aa0405ee98c45a9bb25dd9959d899bbd56bc1b02.tar.xz
imageboard-aa0405ee98c45a9bb25dd9959d899bbd56bc1b02.zip
favourite system and ∂etails on single page
-rw-r--r--controllers/posts.go46
-rw-r--r--models/image.go57
-rw-r--r--models/user.go1
-rw-r--r--router/routes.go1
-rw-r--r--static/css/main.css53
-rw-r--r--templates/posts/list.django4
-rw-r--r--templates/posts/single.django57
-rw-r--r--utils/auth/auth.go4
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 != "" {