diff options
| -rw-r--r-- | controllers/preferences.go | 2 | ||||
| -rw-r--r-- | imageboard/main.go | 2 | ||||
| -rw-r--r-- | middleware/middleware.go | 7 | ||||
| -rw-r--r-- | processors/preferences.go | 76 | ||||
| -rw-r--r-- | processors/processors.go | 1 | ||||
| -rw-r--r-- | router/routes.go | 1 | ||||
| -rw-r--r-- | static/css/main.css | 28 | ||||
| -rw-r--r-- | static/scripts/preferences.js | 72 | ||||
| -rw-r--r-- | static/scripts/theme.js | 37 | ||||
| -rw-r--r-- | templates/layouts/main.django | 5 | ||||
| -rw-r--r-- | templates/preferences.django | 91 |
11 files changed, 248 insertions, 74 deletions
diff --git a/controllers/preferences.go b/controllers/preferences.go index 86e0fb3..042e810 100644 --- a/controllers/preferences.go +++ b/controllers/preferences.go @@ -6,7 +6,7 @@ import ( "github.com/gofiber/fiber/v2" ) -func PreferencesController(ctx *fiber.Ctx) error { +func PreferencesPageController(ctx *fiber.Ctx) error { ctx.Locals("Title", "Site Preferences") return shortcuts.Render(ctx, "preferences", nil) } diff --git a/imageboard/main.go b/imageboard/main.go index 178fed2..ba7836b 100644 --- a/imageboard/main.go +++ b/imageboard/main.go @@ -40,8 +40,8 @@ func main() { app.Use(cors.New())
processors.Initialize(app)
+ middleware.Initialize(app)
- app.Use(middleware.JSON)
app.Static("/static", "./static")
router.Initialize(app)
diff --git a/middleware/middleware.go b/middleware/middleware.go new file mode 100644 index 0000000..ee51965 --- /dev/null +++ b/middleware/middleware.go @@ -0,0 +1,7 @@ +package middleware + +import "github.com/gofiber/fiber/v2" + +func Initialize(app *fiber.App) { + app.Use(JSON) +} diff --git a/processors/preferences.go b/processors/preferences.go new file mode 100644 index 0000000..a96d1cb --- /dev/null +++ b/processors/preferences.go @@ -0,0 +1,76 @@ +package processors + +import ( + "encoding/json" + "fmt" + + "github.com/gofiber/fiber/v2" +) + +type SitePreferences struct { + SidebarWidth string `json:"sidebar_width"` + MainContentWidth string `json:"main_content_width"` + H1FontSize string `json:"h1_font_size"` + BodyFontSize string `json:"body_font_size"` + SmallFontSize string `json:"small_font_size"` + PostsPerPage int `json:"posts_per_page"` +} + +func PreferencesContextProcessor(context *fiber.Ctx) error { + defaultPreferences := SitePreferences{ + SidebarWidth: "180px", + MainContentWidth: "1200px", + H1FontSize: "16px", + BodyFontSize: "13px", + SmallFontSize: "11px", + PostsPerPage: 42, + } + + preferences := defaultPreferences + + preferencesCookie := context.Cookies("preferences") + if preferencesCookie != "" { + _ = json.Unmarshal([]byte(preferencesCookie), &preferences) + } + + bytes, err := json.Marshal(preferences) + if err == nil { + context.Cookie(&fiber.Cookie{ + Name: "preferences", + Value: string(bytes), + Path: "/", + SameSite: fiber.CookieSameSiteLaxMode, + }) + } + + context.Locals("Preferences", preferences) + context.Locals("PreferencesCSS", preferencesToCSS(preferences)) + return context.Next() +} + +func preferencesToCSS(preferences SitePreferences) string { + return fmt.Sprintf(` + <style> + main { + width: %s; + } + body { + font-size: %s; + } + h1 { + font-size: %s; + } + small { + font-size: %s; + } + .sidebar { + width: %s; + } + </style>`, + preferences.MainContentWidth, + preferences.BodyFontSize, + preferences.H1FontSize, + preferences.SmallFontSize, + preferences.SidebarWidth, + ) +} diff --git a/processors/processors.go b/processors/processors.go index e060abe..bbd23fe 100644 --- a/processors/processors.go +++ b/processors/processors.go @@ -6,4 +6,5 @@ func Initialize(app *fiber.App) { app.Use(RequestContextProcessor)
app.Use(MetaContextProcessor)
app.Use(SidebarContextProcessor)
+ app.Use(PreferencesContextProcessor)
}
diff --git a/router/routes.go b/router/routes.go index 4541419..91b3816 100644 --- a/router/routes.go +++ b/router/routes.go @@ -11,6 +11,7 @@ func Initialize(router *fiber.App) { main.Get("/", controllers.HomePageController)
main.Get("/login", controllers.LoginPageController)
main.Get("/register", controllers.RegisterPageController)
+ main.Get("/preferences", controllers.PreferencesPageController)
posts := router.Group("/posts")
posts.Get("/", controllers.PostsController)
diff --git a/static/css/main.css b/static/css/main.css index 89fee52..aa92e1f 100644 --- a/static/css/main.css +++ b/static/css/main.css @@ -7,7 +7,6 @@ body { padding: 0; box-sizing: border-box; font-family: "LXGW WenKai Mono TC", monospace; - font-size: 13px; } body { @@ -79,7 +78,6 @@ nav::before { main { display: flex; - width: 1200px; margin: 0 auto; gap: 10px; padding: 10px; @@ -87,7 +85,6 @@ main { } .sidebar { - width: 180px; background-color: #0d0020; border: 1px solid #4d4d80; padding: 8px; @@ -148,10 +145,14 @@ main { .centered-main h1 { color: #ffccff; - font-size: 16px; margin: 8px 0px; } +.content-main h1 { + color: #ff99cc; + margin-bottom: 8px; +} + .centered-main p { color: #99ffcc; } @@ -177,7 +178,8 @@ main { input[type="text"], input[type="email"], -input[type="password"] { +input[type="password"], +input[type="number"] { background-color: #1a0033; border: 1px solid #9999ff; color: #ccccff; @@ -187,7 +189,8 @@ input[type="password"] { input[type="text"]:focus, input[type="email"]:focus, -input[type="password"]:focus { +input[type="password"]:focus, +input[type="number"]:focus { border-color: #ff99cc; background-color: #260040; outline: none; @@ -298,8 +301,6 @@ footer::before { margin: 8px 0; } - - .ibform { background-color: #0d001a; border: 1px solid #ff99cc; @@ -328,7 +329,6 @@ footer::before { .fg-sub small { color: #ff99cc; - font-size: 11px; } .fg-main label { @@ -354,4 +354,14 @@ footer::before { .q-img { max-width: 768px; +} + +.error { + color: #ffccff; + background-color: #330000; + border: 1px solid #ff0000; + padding: 8px; + margin-bottom: 16px; + text-align: center; + display: none; }
\ No newline at end of file diff --git a/static/scripts/preferences.js b/static/scripts/preferences.js new file mode 100644 index 0000000..d02e5cc --- /dev/null +++ b/static/scripts/preferences.js @@ -0,0 +1,72 @@ +function validateCSSFieldValue(value) { + const validCSSValuePattern = /^(auto|(\d+(\.\d+)?px|em|rem|%)|calc\(.+\))$/; + return validCSSValuePattern.test(value); +} + +function validateCSSFontSize(value) { + const validFontSizePattern = /^(small|medium|large|x-large|\d+(\.\d+)?(px|em|rem|%)?)$/; + return validFontSizePattern.test(value); +} + +function showError(message) { + const errorMessage = document.getElementById('error-message'); + errorMessage.textContent = message; + errorMessage.style.display = 'block'; +} + +function hideError() { + const errorMessage = document.getElementById('error-message'); + errorMessage.textContent = ''; + errorMessage.style.display = 'none'; +} + +function setPreferences() { + const preferences = { + sidebar_width: document.getElementById('sidebar-width').value, + main_content_width: document.getElementById('main-width').value, + h1_font_size: document.getElementById('h1-font-size').value, + body_font_size: document.getElementById('body-font-size').value, + small_font_size: document.getElementById('small-font-size').value, + posts_per_page: parseInt(document.getElementById('posts-per-page').value, 10) + } + + for (const key in preferences) { + if (preferences[key] === '') { + showError(`Please fill in the ${key.replace(/_/g, ' ')} field.`); + return; + } + + switch (key) { + case 'sidebar_width': + case 'main_content_width': + if (!validateCSSFieldValue(preferences[key])) { + showError(`Invalid value for ${key.replace(/_/g, ' ')}: ${preferences[key]}. Please enter a valid CSS value (e.g., '300px', '50%', 'auto').`); + return; + } + break; + case 'h1_font_size': + case 'body_font_size': + case 'small_font_size': + if (!validateCSSFontSize(preferences[key])) { + showError(`Invalid font size for ${key.replace(/_/g, ' ')}: ${preferences[key]}. Please enter a valid font size (e.g., 'small', 'medium', 'large', '16px', '1.2em').`); + return; + } + break; + case 'posts_per_page': + if (isNaN(preferences[key]) || preferences[key] <= 0) { + showError('Posts per page must be a positive integer.'); + return; + } + break; + } + } + + document.cookie = `preferences=${JSON.stringify(preferences)}; path=/; SameSite=Lax;` + window.location.reload() +} + +function resetPreferences() { + hideError(); + document.cookie = 'preferences=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/; SameSite=Lax;' + window.location.reload() +} diff --git a/static/scripts/theme.js b/static/scripts/theme.js deleted file mode 100644 index 0ba6625..0000000 --- a/static/scripts/theme.js +++ /dev/null @@ -1,37 +0,0 @@ -document.addEventListener('DOMContentLoaded', function () { - const savedTheme = localStorage.getItem('theme') || 'light'; - document.documentElement.setAttribute('data-theme', savedTheme); - - const preferencesForm = document.getElementById('preferences-form'); - if (preferencesForm) { - const themeRadios = document.querySelectorAll('input[name="theme"]'); - themeRadios.forEach(radio => { - if (radio.value === savedTheme) { - radio.checked = true; - } - }); - - preferencesForm.addEventListener('submit', function (e) { - e.preventDefault(); - const selectedTheme = document.querySelector('input[name="theme"]:checked').value; - localStorage.setItem('theme', selectedTheme); - document.documentElement.setAttribute('data-theme', selectedTheme); - - let successMsg = document.querySelector('.success-message'); - if (successMsg) { - successMsg.remove(); - } - - const message = document.createElement('div'); - message.className = 'success-message'; - message.textContent = 'Preferences saved successfully!'; - preferencesForm.parentNode.insertBefore(message, preferencesForm); - - setTimeout(() => { - if (message.parentNode) { - message.remove(); - } - }, 3000); - }); - } -});
\ No newline at end of file diff --git a/templates/layouts/main.django b/templates/layouts/main.django index 488f9e4..8e776d3 100644 --- a/templates/layouts/main.django +++ b/templates/layouts/main.django @@ -4,6 +4,8 @@ <meta charset="UTF-8" /> <title>{{ Title }} - {{ Appname }}</title> <link rel="stylesheet" href="/static/css/main.css" /> + <meta name="viewport" content="width=device-width, initial-scale=1.0" /> + {{ PreferencesCSS|safe }} </head> <body> {% include 'partials/navbar.django' %} @@ -22,4 +24,7 @@ <p>© 2025 {{ Appname }}. All rights reserved.</p> </footer> </body> + {% block scripts %} + + {% endblock %} </html> diff --git a/templates/preferences.django b/templates/preferences.django index 53fc2fc..6b5f0e3 100644 --- a/templates/preferences.django +++ b/templates/preferences.django @@ -1,33 +1,72 @@ {% extends 'layouts/main.django' %} {% block content %} - <h2>Site Preferences</h2> - - <form id="preferences-form"> - <fieldset> - <legend>Theme Settings</legend> - - <div class="form-group"> - <label>Theme</label> - <div class="radio-group"> - <div> - <input type="radio" name="theme" value="light" id="theme-light" /> - <label for="theme-light">Light Mode</label> - </div> - <div> - <input type="radio" name="theme" value="dark" id="theme-dark" /> - <label for="theme-dark">Dark Mode</label> - </div> + <div class="content-main"> + <h1>Site Preferences</h1> + <p>Customize your experience on {{ Appname }}. These settings are stored in your cookies and will persist across sessions. However, they are not coupled with your account, so they don't apply to other devices or browsers.</p> + <form action="javascript:setPreferences()" method="post" class="ibform"> + <div class="error" id="error-message"></div> + <div class="fgroup"> + <div class="fg-main"> + <label for="sidebar-width">Sidebar Width</label> + </div> + <div class="fg-sub"> + <input type="text" id="sidebar-width" name="sidebar-width" value="{{ Preferences.SidebarWidth }}" placeholder="e.g., 250px" /> + <small>Set the width of the sidebar. Use CSS units like px, em, or %.</small> </div> </div> - - <div class="form-actions"> - <button type="submit">SAVE PREFERENCES</button> + <div class="fgroup"> + <div class="fg-main"> + <label for="main-width">Main Content Width</label> + </div> + <div class="fg-sub"> + <input type="text" id="main-width" name="main-width" value="{{ Preferences.MainContentWidth }}" placeholder="e.g., 800px" /> + <small>Set the width of the main content area. Use CSS units like px, em, or %.</small> + </div> </div> - </fieldset> - </form> - - <p> - <a href="/">Back to Posts</a> - </p> + <div class="fgroup"> + <div class="fg-main"> + <label for="h1-font-size">H1 Font Size</label> + </div> + <div class="fg-sub"> + <input type="text" id="h1-font-size" name="h1-font-size" value="{{ Preferences.H1FontSize }}" min="10" max="100" /> + <small>Set the font size for H1 elements. This will affect the main headings across the site. Use a valid CSS size (e.g., 16px, 1.5em, etc.).</small> + </div> + </div> + <div class="fgroup"> + <div class="fg-main"> + <label for="body-font-size">Body Font Size</label> + </div> + <div class="fg-sub"> + <input type="text" id="body-font-size" name="body-font-size" value="{{ Preferences.BodyFontSize }}" min="10" max="100" /> + <small>Set the font size for body text. This will affect the readability of the content across the site. Use a valid CSS size (e.g., 13px, 1em, etc.).</small> + </div> + </div> + <div class="fgroup"> + <div class="fg-main"> + <label for="small-font-size">Small Font Size</label> + </div> + <div class="fg-sub"> + <input type="text" id="small-font-size" name="small-font-size" value="{{ Preferences.SmallFontSize }}" min="8" max="50" /> + <small>Set the font size for small text elements. This will affect the visibility of smaller text across the site. Use a valid CSS size (e.g., 11px, 0.8em, etc.).</small> + </div> + </div> + <div class="fgroup"> + <div class="fg-main"> + <label for="posts-per-page">Posts Per Page</label> + </div> + <div class="fg-sub"> + <input type="number" id="posts-per-page" name="posts-per-page" value="{{ Preferences.PostsPerPage }}" min="1" max="100" /> + <small>Set the number of posts displayed per page. This can help manage load times</small> + </div> + </div> + <div class="fbtngrp"> + <input type="submit" value="Save Preferences" /> + <input type="button" value="Reset to Default" onclick="resetPreferences()" /> + </div> + </form> + </div> +{% endblock %} +{% block scripts %} + <script src="/static/scripts/preferences.js" type="text/javascript"></script> {% endblock %} |
