diff options
| author | Bobby <[email protected]> | 2025-07-07 22:57:31 +0530 |
|---|---|---|
| committer | Bobby <[email protected]> | 2025-07-07 22:57:31 +0530 |
| commit | 52a0248c1c81a14699b3d33ba7efe0c56bbe7477 (patch) | |
| tree | 02d35fee769d518beb5dd01715e84d13ea421d51 | |
| parent | b6a04140f2668a0dcae4befcd272e05b75bd14e5 (diff) | |
| download | imageboard-52a0248c1c81a14699b3d33ba7efe0c56bbe7477.tar.xz imageboard-52a0248c1c81a14699b3d33ba7efe0c56bbe7477.zip | |
massive y2k retro overhaul with sidebar, context processors, and proper database organization
| -rw-r--r-- | controllers/home.go | 12 | ||||
| -rw-r--r-- | controllers/login.go | 1 | ||||
| -rw-r--r-- | controllers/posts.go | 1 | ||||
| -rw-r--r-- | controllers/preferences.go | 1 | ||||
| -rw-r--r-- | controllers/register.go | 1 | ||||
| -rw-r--r-- | database/comments.go | 11 | ||||
| -rw-r--r-- | database/images.go | 34 | ||||
| -rw-r--r-- | database/tags.go | 23 | ||||
| -rw-r--r-- | models/image.go | 13 | ||||
| -rw-r--r-- | processors/processors.go | 2 | ||||
| -rw-r--r-- | processors/request.go | 45 | ||||
| -rw-r--r-- | processors/sidebar.go | 98 | ||||
| -rw-r--r-- | router/routes.go | 14 | ||||
| -rw-r--r-- | static/.gitkeep | 2 | ||||
| -rw-r--r-- | static/css/main.css | 402 | ||||
| -rw-r--r-- | static/images/image_main.png | bin | 0 -> 4481980 bytes | |||
| -rw-r--r-- | templates/home.django | 5 | ||||
| -rw-r--r-- | templates/layouts/main.django | 12 | ||||
| -rw-r--r-- | templates/partials/navbar.django | 27 | ||||
| -rw-r--r-- | templates/partials/sidebar.django | 44 | ||||
| -rw-r--r-- | utils/format/format.go | 27 |
21 files changed, 411 insertions, 364 deletions
diff --git a/controllers/home.go b/controllers/home.go new file mode 100644 index 0000000..5e14b9e --- /dev/null +++ b/controllers/home.go @@ -0,0 +1,12 @@ +package controllers + +import ( + "imageboard/utils/shortcuts" + + "github.com/gofiber/fiber/v2" +) + +func HomeController(ctx *fiber.Ctx) error { + ctx.Locals("Title", "Home Page") + return shortcuts.Render(ctx, "home", nil) +} diff --git a/controllers/login.go b/controllers/login.go index dc8dd48..1d6bc5e 100644 --- a/controllers/login.go +++ b/controllers/login.go @@ -8,6 +8,5 @@ import ( func LoginController(ctx *fiber.Ctx) error { ctx.Locals("Title", "Login") - ctx.Locals("request", fiber.Map{"path": ctx.Path()}) return shortcuts.Render(ctx, "login", nil) } diff --git a/controllers/posts.go b/controllers/posts.go index c3da9c1..6fdcd26 100644 --- a/controllers/posts.go +++ b/controllers/posts.go @@ -8,7 +8,6 @@ import ( func PostsController(ctx *fiber.Ctx) error {
ctx.Locals("Title", "Posts")
- ctx.Locals("request", fiber.Map{"path": ctx.Path()})
searchQuery := ctx.Query("tags", "")
diff --git a/controllers/preferences.go b/controllers/preferences.go index 3a504ed..86e0fb3 100644 --- a/controllers/preferences.go +++ b/controllers/preferences.go @@ -8,6 +8,5 @@ import ( func PreferencesController(ctx *fiber.Ctx) error { ctx.Locals("Title", "Site Preferences") - ctx.Locals("request", fiber.Map{"path": ctx.Path()}) return shortcuts.Render(ctx, "preferences", nil) } diff --git a/controllers/register.go b/controllers/register.go index 9343732..3be4e64 100644 --- a/controllers/register.go +++ b/controllers/register.go @@ -8,6 +8,5 @@ import ( func RegisterController(ctx *fiber.Ctx) error { ctx.Locals("Title", "Register") - ctx.Locals("request", fiber.Map{"path": ctx.Path()}) return shortcuts.Render(ctx, "register", nil) } diff --git a/database/comments.go b/database/comments.go new file mode 100644 index 0000000..1203be4 --- /dev/null +++ b/database/comments.go @@ -0,0 +1,11 @@ +package database + +import ( + "imageboard/models" +) + +func GetTotalCommentsCount() (int64, error) { + var count int64 + err := DB.Model(&models.Comment{}).Count(&count).Error + return count, err +} diff --git a/database/images.go b/database/images.go new file mode 100644 index 0000000..8fa1a47 --- /dev/null +++ b/database/images.go @@ -0,0 +1,34 @@ +package database + +import ( + "imageboard/models" + "imageboard/utils/format" + "time" +) + +func GetTotalPostsCount() (int64, error) { + var count int64 + err := DB.Model(&models.Image{}).Count(&count).Error + return count, err +} + +func GetTodayPostsCount() (int64, error) { + var count int64 + today := time.Now().Truncate(24 * time.Hour) + err := DB.Model(&models.Image{}).Where("created_at >= ?", today).Count(&count).Error + return count, err +} + +func GetTotalStorageSize() (string, error) { + var imageSizes []models.ImageSize + if err := DB.Select("file_size").Find(&imageSizes).Error; err != nil { + return "0 B", err + } + + var totalSize int64 + for _, size := range imageSizes { + totalSize += size.FileSize + } + + return format.FileSize(totalSize), nil +} diff --git a/database/tags.go b/database/tags.go new file mode 100644 index 0000000..199087a --- /dev/null +++ b/database/tags.go @@ -0,0 +1,23 @@ +package database + +import ( + "imageboard/models" +) + +func GetTotalTagsCount() (int64, error) { + var count int64 + err := DB.Model(&models.Tag{}).Where("is_deleted = ?", false).Count(&count).Error + return count, err +} + +func GetPopularTags(limit int) ([]models.Tag, error) { + var tags []models.Tag + err := DB.Where("is_deleted = ?", false).Order("count DESC").Limit(limit).Find(&tags).Error + return tags, err +} + +func GetRecentTags(limit int) ([]models.Tag, error) { + var tags []models.Tag + err := DB.Where("is_deleted = ?", false).Order("created_at DESC").Limit(limit).Find(&tags).Error + return tags, err +} diff --git a/models/image.go b/models/image.go index fef3bf8..03d4c38 100644 --- a/models/image.go +++ b/models/image.go @@ -3,6 +3,7 @@ package models import ( "fmt" "imageboard/config" + "imageboard/utils/format" "imageboard/utils/math" "strings" @@ -50,17 +51,7 @@ func (s *ImageSize) GetDimensions() string { } func (s *ImageSize) GetFileSizeFormatted() string { - const unit = 1024 - if s.FileSize < unit { - return fmt.Sprintf("%d B", s.FileSize) - } - div, exp := int64(unit), 0 - for n := s.FileSize / unit; n >= unit; n /= unit { - div *= unit - exp++ - } - - return fmt.Sprintf("%.2f %sB", float64(s.FileSize)/float64(div), "KMGTPE"[exp:exp+1]) + return format.FileSize(s.FileSize) } type Image struct { diff --git a/processors/processors.go b/processors/processors.go index 69fbbec..e060abe 100644 --- a/processors/processors.go +++ b/processors/processors.go @@ -3,5 +3,7 @@ package processors import "github.com/gofiber/fiber/v2"
func Initialize(app *fiber.App) {
+ app.Use(RequestContextProcessor)
app.Use(MetaContextProcessor)
+ app.Use(SidebarContextProcessor)
}
diff --git a/processors/request.go b/processors/request.go new file mode 100644 index 0000000..f4fe8d2 --- /dev/null +++ b/processors/request.go @@ -0,0 +1,45 @@ +package processors + +import ( + "github.com/gofiber/fiber/v2" +) + +type QueryParam struct { + Key string + Value string +} + +type Request struct { + Path string + Method string + Query []QueryParam + Params []QueryParam + QueryString string + IP string + URL string +} + +func RequestContextProcessor(ctx *fiber.Ctx) error { + queryParams := []QueryParam{} + for k, v := range ctx.Queries() { + queryParams = append(queryParams, QueryParam{Key: k, Value: v}) + } + + routeParams := []QueryParam{} + for k, v := range ctx.AllParams() { + routeParams = append(routeParams, QueryParam{Key: k, Value: v}) + } + + request := Request{ + Path: ctx.Path(), + Method: ctx.Method(), + Query: queryParams, + Params: routeParams, + QueryString: string(ctx.Request().URI().QueryString()), + IP: ctx.IP(), + URL: ctx.OriginalURL(), + } + + ctx.Locals("Request", request) + return ctx.Next() +} diff --git a/processors/sidebar.go b/processors/sidebar.go new file mode 100644 index 0000000..26138f1 --- /dev/null +++ b/processors/sidebar.go @@ -0,0 +1,98 @@ +package processors + +import ( + "fmt" + "imageboard/database" + "imageboard/models" + + "github.com/gofiber/fiber/v2" +) + +type SiteStats struct { + Posts string + Tags string + Today string + Storage string + Comments string +} + +func SidebarContextProcessor(ctx *fiber.Ctx) error { + popularTags, popularTagsErr := database.GetPopularTags(15) + if popularTagsErr != nil || len(popularTags) == 0 { + mockTags := []models.Tag{ + {Name: "anime", Type: models.TagTypeGeneral, Count: 1523}, + {Name: "manga", Type: models.TagTypeGeneral, Count: 892}, + {Name: "kawaii", Type: models.TagTypeGeneral, Count: 756}, + {Name: "retro", Type: models.TagTypeMeta, Count: 634}, + {Name: "y2k", Type: models.TagTypeMeta, Count: 511}, + {Name: "aesthetic", Type: models.TagTypeGeneral, Count: 445}, + {Name: "sakura", Type: models.TagTypeArtist, Count: 389}, + {Name: "studio_ghibli", Type: models.TagTypeCopyright, Count: 312}, + {Name: "totoro", Type: models.TagTypeCharacter, Count: 298}, + {Name: "sailor_moon", Type: models.TagTypeCharacter, Count: 267}, + {Name: "pokemon", Type: models.TagTypeCopyright, Count: 234}, + {Name: "pixiv", Type: models.TagTypeMeta, Count: 198}, + {Name: "digital_art", Type: models.TagTypeMeta, Count: 176}, + {Name: "watercolor", Type: models.TagTypeGeneral, Count: 145}, + {Name: "minimalist", Type: models.TagTypeGeneral, Count: 123}, + } + ctx.Locals("PopularTags", mockTags) + } else { + ctx.Locals("PopularTags", popularTags) + } + + recentTags, recentTagsErr := database.GetRecentTags(10) + if recentTagsErr != nil || len(recentTags) == 0 { + mockRecentTags := []models.Tag{ + {Name: "cyberpunk", Type: models.TagTypeGeneral, Count: 23}, + {Name: "vaporwave", Type: models.TagTypeMeta, Count: 45}, + {Name: "synthwave", Type: models.TagTypeGeneral, Count: 12}, + {Name: "retrocomputing", Type: models.TagTypeMeta, Count: 8}, + {Name: "neon", Type: models.TagTypeGeneral, Count: 67}, + {Name: "glitch", Type: models.TagTypeMeta, Count: 34}, + {Name: "pixel_art", Type: models.TagTypeGeneral, Count: 89}, + {Name: "lo_fi", Type: models.TagTypeGeneral, Count: 56}, + } + ctx.Locals("RecentTags", mockRecentTags) + } else { + ctx.Locals("RecentTags", recentTags) + } + + postsCount, postsErr := database.GetTotalPostsCount() + tagsCount, tagsCountErr := database.GetTotalTagsCount() + commentsCount, commentsErr := database.GetTotalCommentsCount() + todayCount, todayErr := database.GetTodayPostsCount() + storageSize, storageErr := database.GetTotalStorageSize() + + var stats SiteStats + + if postsErr == nil { + stats.Posts = fmt.Sprintf("%d", postsCount) + } else { + stats.Posts = "0" + } + if tagsCountErr == nil { + stats.Tags = fmt.Sprintf("%d", tagsCount) + } else { + stats.Tags = "0" + } + if commentsErr == nil { + stats.Comments = fmt.Sprintf("%d", commentsCount) + } else { + stats.Comments = "0" + } + if todayErr == nil { + stats.Today = fmt.Sprintf("%d new", todayCount) + } else { + stats.Today = "0 new" + } + if storageErr == nil { + stats.Storage = storageSize + } else { + stats.Storage = "0 B" + } + + ctx.Locals("SiteStats", stats) + + return ctx.Next() +} diff --git a/router/routes.go b/router/routes.go index 216719f..8422f99 100644 --- a/router/routes.go +++ b/router/routes.go @@ -7,10 +7,16 @@ import ( )
func Initialize(router *fiber.App) {
- router.Get("/", controllers.PostsController)
- router.Get("/register", controllers.RegisterController)
- router.Get("/login", controllers.LoginController)
- router.Get("/preferences", controllers.PreferencesController)
+ main := router.Group("/")
+ main.Get("/", controllers.HomeController)
+
+ posts := router.Group("/posts")
+ posts.Get("/", controllers.PostsController)
+
+ // router.Get("/posts", controllers.PostsController)
+ // router.Get("/register", controllers.RegisterController)
+ // router.Get("/login", controllers.LoginController)
+ // router.Get("/preferences", controllers.PreferencesController)
router.Use(func(c *fiber.Ctx) error {
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{
diff --git a/static/.gitkeep b/static/.gitkeep deleted file mode 100644 index 1eb7d0c..0000000 --- a/static/.gitkeep +++ /dev/null @@ -1,2 +0,0 @@ -# Static files directory -This directory contains static assets served by the imageboard application. diff --git a/static/css/main.css b/static/css/main.css index ac463c9..1fc8ce6 100644 --- a/static/css/main.css +++ b/static/css/main.css @@ -1,374 +1,126 @@ -:root { - --bg-main: #ffffff; - --bg-section: #f8f8f8; - --bg-nav: #e0e0e0; - --text-main: #000000; - --text-dim: #666666; - --text-active: #ff0000; - --link-default: #0000ee; - --link-visited: #551a8b; - --link-hover: #ff0000; - --border-main: #c0c0c0; - --border-dark: #808080; - --button-bg: #e0e0e0; - --button-shadow: #808080; - --input-bg: #ffffff; - --error-bg: #ffe0e0; - --error-border: #ff0000; - --success-bg: #e0ffe0; - --success-border: #00aa00; -} - -[data-theme="dark"] { - --bg-main: #000000; - --bg-section: #1a1a1a; - --bg-nav: #333333; - --text-main: #c0c0c0; - --text-dim: #808080; - --text-active: #ff6666; - --link-default: #6699ff; - --link-visited: #cc99ff; - --link-hover: #ffff66; - --border-main: #666666; - --border-dark: #999999; - --button-bg: #404040; - --button-shadow: #202020; - --input-bg: #1a1a1a; - --error-bg: #330000; - --error-border: #ff6666; - --success-bg: #003300; - --success-border: #66ff66; -} +@import url('https://fonts.googleapis.com/css2?family=LXGW+WenKai+Mono+TC:wght@400;700&display=swap'); -* { +*, +html, +body { margin: 0; padding: 0; box-sizing: border-box; + font-family: "LXGW WenKai Mono TC", monospace; + font-size: 14px; } body { - font-family: "MS Gothic", "MS ゴシック", "Courier New", monospace; - font-size: 12px; - line-height: 1.2; - background: var(--bg-main); - color: var(--text-main); - width: 800px; - margin: 0 auto; -} - -nav { - background: var(--bg-nav); - border: 2px outset var(--border-main); - padding: 6px 8px; - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 8px; + background-color: #000033; + background-image: + radial-gradient(circle at 25% 25%, #1a0033 0%, transparent 50%), + radial-gradient(circle at 75% 75%, #001a33 0%, transparent 50%); + color: #ccccff; + margin: 0; + padding: 0; + overflow-x: hidden; } -nav div { - display: flex; - gap: 12px; - align-items: center; +a { + color: #99ccff; + text-decoration: none; } -nav a { - color: var(--link-default); +a:hover { + color: #99ffcc; text-decoration: underline; - font-size: 12px; - font-weight: normal; -} - -nav a:visited { - color: var(--link-visited); -} - -nav a:hover { - color: var(--link-hover); -} - -nav a.active { - color: var(--text-active); - font-weight: bold; - text-decoration: none; } -main { - padding: 8px; - min-height: 400px; +nav { + background: linear-gradient(to bottom, #330066, #1a001a); + padding: 8px 16px; + display: flex; + justify-content: space-between; + align-items: center; + position: relative; + border-bottom: 2px solid #ff99cc; } -h1 { - font-size: 14px; - font-weight: bold; +nav::before { + content: "✦ ✧ ✦ ✧ ✦ ✧ ✦ ✧ ✦ ✧ ✦ ✧ ✦ ✧ ✦ ✧ ✦ ✧ ✦ ✧"; + position: absolute; + top: 2px; + left: 0; + right: 0; text-align: center; - margin-bottom: 12px; - color: var(--text-main); -} - -h2 { - font-size: 13px; - font-weight: bold; - margin-bottom: 8px; - color: var(--text-main); -} - -h3, -h4 { + color: #ffccee; font-size: 12px; - font-weight: bold; - margin-bottom: 6px; - color: var(--text-main); } -form { - margin: 8px 0; +.nav-left, +.nav-right { + display: flex; + align-items: center; + gap: 16px; } -label { - display: block; +.nav-title { + color: #ffccff; font-weight: bold; - font-size: 12px; - margin: 4px 0 2px 0; - color: var(--text-main); -} - -input, -textarea, -select { - background: var(--input-bg); - color: var(--text-main); - border: 2px inset var(--border-main); - font-family: inherit; - font-size: 12px; - padding: 2px 4px; - margin-bottom: 6px; -} - -input[type="text"], -input[type="password"], -input[type="email"], -textarea { - width: 180px; } -input[type="checkbox"], -input[type="radio"] { - width: auto; - margin-right: 4px; -} - -button, -input[type="submit"] { - background: var(--button-bg); - color: var(--text-main); - border: 2px outset var(--border-main); - font-family: inherit; - font-size: 12px; - padding: 3px 8px; - cursor: pointer; - margin: 2px 4px 2px 0; +.nav-title:hover { + color: #ff99ff; + text-decoration: none; } -button:hover, -input[type="submit"]:hover { - background: var(--bg-section); +.user-status { + color: #ccffcc; } -button:active, -input[type="submit"]:active { - border: 2px inset var(--border-main); +main { + display: flex; + max-width: 1200px; + margin: 0 auto; + gap: 10px; + padding: 10px; } -.posts-container { - display: grid; - grid-template-columns: repeat(4, 1fr); - gap: 4px; - margin: 8px 0; +.sidebar { + width: 180px; + background-color: #0d0020; + border: 1px solid #4d4d80; + padding: 8px; + height: fit-content; } -article { - background: var(--bg-section); - border: 1px solid var(--border-main); +.sidebar h3 { + background: linear-gradient(to right, #330066, #1a0033); + color: #ffccff; + margin: 0 0 8px 0; padding: 4px; text-align: center; -} - -article img { - width: 100%; - height: 120px; - object-fit: cover; - border: 1px solid var(--border-dark); - margin-bottom: 4px; -} - -article h4 { - font-size: 11px; + border: 1px solid #ff99cc; font-weight: bold; - margin: 2px 0; - color: var(--text-main); -} - -article p { - font-size: 10px; - color: var(--text-dim); - margin: 1px 0; -} - -aside { - background: var(--bg-section); - border: 2px inset var(--border-main); - padding: 6px; - margin: 8px 0; - display: flex; - align-items: center; - gap: 8px; -} - -aside input[type="text"] { - flex: 1; - margin-bottom: 0; -} - -.empty-state { - background: var(--bg-section); - border: 2px inset var(--border-main); - padding: 24px; - text-align: center; - margin: 12px 0; -} - -.empty-state h3 { - color: var(--text-main); - margin-bottom: 8px; -} - -.error-message, -.error { - background: var(--error-bg); - color: var(--text-main); - border: 1px solid var(--error-border); - padding: 6px; - margin: 6px 0; - text-align: center; -} - -.success-message, -.success { - background: var(--success-bg); - color: var(--text-main); - border: 1px solid var(--success-border); - padding: 6px; - margin: 6px 0; - text-align: center; } -footer { - background: var(--bg-nav); - border: 2px outset var(--border-main); - padding: 8px; - text-align: center; - font-size: 10px; - color: var(--text-dim); +.sidebar div:not(:first-child) h3 { margin-top: 16px; } -footer p { - margin: 1px 0; -} - -a { - color: var(--link-default); - text-decoration: underline; -} - -a:visited { - color: var(--link-visited); -} - -a:hover { - color: var(--link-hover); -} - -p { - margin: 4px 0; - line-height: 1.3; -} - -small { - font-size: 10px; - color: var(--text-dim); -} - -.button-group { - margin: 8px 0; +.sidebar-content { display: flex; - gap: 4px; -} - -section { - background: var(--bg-section); - border: 2px inset var(--border-main); - padding: 12px; - margin: 12px 0; -} - -section h2 { - text-align: center; - margin-bottom: 12px; - color: var(--text-main); -} - -.center { - text-align: center; -} - -/* Form Elements */ -fieldset { - background: var(--bg-section); - border: 2px inset var(--border-main); - padding: 12px; - margin: 8px 0; -} - -legend { - font-weight: bold; - font-size: 12px; - color: var(--text-main); - padding: 0 4px; - background: var(--bg-main); -} - -.form-group { - margin: 8px 0; -} - -.form-group label { - display: block; - font-weight: bold; - font-size: 12px; - margin: 4px 0 2px 0; - color: var(--text-main); -} - -.form-group small { - display: block; - font-size: 10px; - color: var(--text-dim); - margin-top: 2px; + flex-direction: column; + gap: 8px; } -.form-actions { - margin: 12px 0 8px 0; - text-align: center; +.sidebar-tag:hover { + text-decoration: none; + filter: brightness(1.3); } -.radio-group div { - margin: 4px 0; +.sidebar-tag::before, +.sidebar-stat::before { + content: "»"; + margin: 0px 8px; } -.radio-group label { - display: inline; - font-weight: normal; - margin-left: 4px; +.tag-count, +.sidebar-stat-value { + color: #cccccc; }
\ No newline at end of file diff --git a/static/images/image_main.png b/static/images/image_main.png Binary files differnew file mode 100644 index 0000000..132dadb --- /dev/null +++ b/static/images/image_main.png diff --git a/templates/home.django b/templates/home.django new file mode 100644 index 0000000..e23df14 --- /dev/null +++ b/templates/home.django @@ -0,0 +1,5 @@ +{% extends 'layouts/main.django' %} +{% include 'partials/search.django' %} +{% block content %} + <h2>{{ Title }}</h2> +{% endblock %} diff --git a/templates/layouts/main.django b/templates/layouts/main.django index 2d9c241..6f0407e 100644 --- a/templates/layouts/main.django +++ b/templates/layouts/main.django @@ -7,17 +7,19 @@ </head> <body> {% include 'partials/navbar.django' %} - <main> - {% block content %} + <aside class="sidebar"> + {% include 'partials/sidebar.django' %} + </aside> + <section class="content"> + {% block content %} - {% endblock %} + {% endblock %} + </section> </main> <footer> <p>© 2025 {{ Appname }}. All rights reserved.</p> </footer> - - <script src="/scripts/theme.js"></script> </body> </html> diff --git a/templates/partials/navbar.django b/templates/partials/navbar.django index 52596ee..70e7950 100644 --- a/templates/partials/navbar.django +++ b/templates/partials/navbar.django @@ -1,23 +1,24 @@ <nav> <div class="nav-left"> - <a href="/">{{ Appname }}</a> - <a href="/" class="{% if request.path == '/' %}active{% endif %}">POSTS</a> - <a href="/comments" class="{% if request.path == '/comments' %}active{% endif %}">COMMENTS</a> - <a href="/tags" class="{% if request.path == '/tags' %}active{% endif %}">TAGS</a> - {% if User %}{% if User.IsAdmin %} - <a href="/users" class="{% if request.path == '/users' %}active{% endif %}">USERS</a> - {% endif %}{% endif %} + <a href="/" class="nav-title">★彡 {{ Appname }} 彡★</a> + <a href="/posts">Posts</a> + <a href="/comments">Comments</a> + <a href="/tags">Tags</a> + {% if User and User.IsJanitor %} + <a href="/users">Users</a> + {% endif %} </div> <div class="nav-right"> {% if User %} - <a href="/account">{{ User.Username }}</a> - <a href="/preferences" class="{% if request.path == '/preferences' %}active{% endif %}">⚙</a> - <a href="/logout">LOGOUT</a> + <a href="/account" class="user-status">{{ User.Username }}</a> + <a href="/logout">Logout</a> {% else %} - <a href="/login" class="{% if request.path == '/login' %}active{% endif %}">LOGIN</a> - <a href="/register" class="{% if request.path == '/register' %}active{% endif %}">REGISTER</a> - <a href="/preferences" class="{% if request.path == '/preferences' %}active{% endif %}">⚙</a> + <span class="user-status">Guest</span> + <a href="/login">Login</a> + <a href="/register">Register</a> {% endif %} + <a href="/preferences">Preferences</a> + <a href="/help">Help</a> </div> </nav> diff --git a/templates/partials/sidebar.django b/templates/partials/sidebar.django new file mode 100644 index 0000000..79a747b --- /dev/null +++ b/templates/partials/sidebar.django @@ -0,0 +1,44 @@ +<div> + <h3>♡ Popular Tags</h3> + <div class="sidebar-content"> + {% for tag in PopularTags %} + <a href="/posts?tags={{ tag.Name }}" style="color: {{ tag.Type.Color }};" class="sidebar-tag">{{ tag.Name }} <span class="tag-count">({{ tag.Count }})</span></a> + {% endfor %} + {% if not PopularTags %} + <p>No popular tags found.</p> + {% endif %} + </div> +</div> + +<div> + <h3>☆ Site Stats</h3> + <div class="sidebar-content"> + <p class="sidebar-stat"> + Posts: <span class="sidebar-stat-value">{{ SiteStats.Posts }}</span> + </p> + <p class="sidebar-stat"> + Tags: <span class="sidebar-stat-value">{{ SiteStats.Tags }}</span> + </p> + <p class="sidebar-stat"> + Today: <span class="sidebar-stat-value">{{ SiteStats.Today }}</span> + </p> + <p class="sidebar-stat"> + Storage: <span class="sidebar-stat-value">{{ SiteStats.Storage }}</span> + </p> + <p class="sidebar-stat"> + Comments: <span class="sidebar-stat-value">{{ SiteStats.Comments }}</span> + </p> + </div> +</div> + +<div> + <h3>★ Recent Tags</h3> + <div class="sidebar-content"> + {% for tag in RecentTags %} + <a href="/posts?tags={{ tag.Name }}" style="color: {{ tag.Type.Color }};" class="sidebar-tag">{{ tag.Name }} <span class="tag-count">({{ tag.Count }})</span></a> + {% endfor %} + {% if not RecentTags %} + <p>No recent tags found.</p> + {% endif %} + </div> +</div> diff --git a/utils/format/format.go b/utils/format/format.go new file mode 100644 index 0000000..53c813e --- /dev/null +++ b/utils/format/format.go @@ -0,0 +1,27 @@ +package format + +import "fmt" + +func FileSize(size int64) string { + const unit = 1024 + if size < unit { + return fmt.Sprintf("%d B", size) + } + div, exp := int64(unit), 0 + for n := size / unit; n >= unit; n /= unit { + div *= unit + exp++ + } + + return fmt.Sprintf("%.2f %sB", float64(size)/float64(div), "KMGTPE"[exp:exp+1]) +} + +func Count(count int64) string { + if count < 1000 { + return fmt.Sprintf("%d", count) + } else if count < 1000000 { + return fmt.Sprintf("%.1fK", float64(count)/1000) + } else { + return fmt.Sprintf("%.1fM", float64(count)/1000000) + } +} |
