aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBobby <[email protected]>2025-07-07 22:57:31 +0530
committerBobby <[email protected]>2025-07-07 22:57:31 +0530
commit52a0248c1c81a14699b3d33ba7efe0c56bbe7477 (patch)
tree02d35fee769d518beb5dd01715e84d13ea421d51
parentb6a04140f2668a0dcae4befcd272e05b75bd14e5 (diff)
downloadimageboard-52a0248c1c81a14699b3d33ba7efe0c56bbe7477.tar.xz
imageboard-52a0248c1c81a14699b3d33ba7efe0c56bbe7477.zip
massive y2k retro overhaul with sidebar, context processors, and proper database organization
-rw-r--r--controllers/home.go12
-rw-r--r--controllers/login.go1
-rw-r--r--controllers/posts.go1
-rw-r--r--controllers/preferences.go1
-rw-r--r--controllers/register.go1
-rw-r--r--database/comments.go11
-rw-r--r--database/images.go34
-rw-r--r--database/tags.go23
-rw-r--r--models/image.go13
-rw-r--r--processors/processors.go2
-rw-r--r--processors/request.go45
-rw-r--r--processors/sidebar.go98
-rw-r--r--router/routes.go14
-rw-r--r--static/.gitkeep2
-rw-r--r--static/css/main.css402
-rw-r--r--static/images/image_main.pngbin0 -> 4481980 bytes
-rw-r--r--templates/home.django5
-rw-r--r--templates/layouts/main.django12
-rw-r--r--templates/partials/navbar.django27
-rw-r--r--templates/partials/sidebar.django44
-rw-r--r--utils/format/format.go27
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
new file mode 100644
index 0000000..132dadb
--- /dev/null
+++ b/static/images/image_main.png
Binary files differ
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>&copy; 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)
+ }
+}