From 309e516480374df0d1d62fe4d0ff0ef8824a5e5a Mon Sep 17 00:00:00 2001 From: Jocadbz Date: Thu, 15 Jan 2026 23:21:03 -0300 Subject: [PATCH] Preferences: Add user preferences system with database table and settings page - Add user_preferences table to store per-user settings (auto_save_drafts, markdown_preview_default) - Create UserPreferences model with GetUserPreferences, CreateDefaultPreferences, and UpdateUserPreferences functions - Add PreferencesHandler for GET/POST requests to display and save user preferences - Create preferences.html template with checkbox for draft auto-save and radio buttons for markdown preview default - Add "Preferences" link to navbar for logged-in users - Register /preferences/ route with login requirement This establishes the foundation for advanced features like draft auto-save and markdown preview toggle, allowing users to customize their experience. --- handlers/preferences.go | 90 ++++++++++++++++++++++++++++++++ main.go | 17 ++++++ models/user_preferences.go | 70 +++++++++++++++++++++++++ templates/pages/preferences.html | 51 ++++++++++++++++++ templates/partials/navbar.html | 1 + 5 files changed, 229 insertions(+) create mode 100644 handlers/preferences.go create mode 100644 models/user_preferences.go create mode 100644 templates/pages/preferences.html diff --git a/handlers/preferences.go b/handlers/preferences.go new file mode 100644 index 0000000..8ea3e9e --- /dev/null +++ b/handlers/preferences.go @@ -0,0 +1,90 @@ +package handlers + +import ( + "log" + "net/http" + "threadr/models" + + "github.com/gorilla/sessions" +) + +func PreferencesHandler(app *App) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + session := r.Context().Value("session").(*sessions.Session) + userID, ok := session.Values["user_id"].(int) + if !ok { + http.Redirect(w, r, app.Config.ThreadrDir+"/login/", http.StatusFound) + return + } + + // Handle POST request (saving preferences) + if r.Method == http.MethodPost { + // Get form values + autoSaveDrafts := r.FormValue("auto_save_drafts") == "on" + markdownPreviewDefault := r.FormValue("markdown_preview_default") + + // Validate markdown_preview_default + if markdownPreviewDefault != "edit" && markdownPreviewDefault != "preview" { + markdownPreviewDefault = "edit" + } + + // Get current preferences (or create if not exists) + prefs, err := models.GetUserPreferences(app.DB, userID) + if err != nil { + log.Printf("Error fetching preferences: %v", err) + http.Error(w, "Internal Server Error", http.StatusInternalServerError) + return + } + + // Update preferences + prefs.AutoSaveDrafts = autoSaveDrafts + prefs.MarkdownPreviewDefault = markdownPreviewDefault + + err = models.UpdateUserPreferences(app.DB, prefs) + if err != nil { + log.Printf("Error updating preferences: %v", err) + http.Error(w, "Failed to save preferences", http.StatusInternalServerError) + return + } + + // Redirect back to preferences page with success + http.Redirect(w, r, app.Config.ThreadrDir+"/preferences/?saved=true", http.StatusFound) + return + } + + // Handle GET request (displaying preferences form) + prefs, err := models.GetUserPreferences(app.DB, userID) + if err != nil { + log.Printf("Error fetching preferences: %v", err) + http.Error(w, "Internal Server Error", http.StatusInternalServerError) + return + } + + // Check if we should show success message + showSuccess := r.URL.Query().Get("saved") == "true" + + data := struct { + PageData + Preferences *models.UserPreferences + ShowSuccess bool + }{ + PageData: PageData{ + Title: "ThreadR - Preferences", + Navbar: "preferences", + LoggedIn: true, + ShowCookieBanner: false, + BasePath: app.Config.ThreadrDir, + StaticPath: app.Config.ThreadrDir + "/static", + CurrentURL: r.URL.Path, + }, + Preferences: prefs, + ShowSuccess: showSuccess, + } + + if err := app.Tmpl.ExecuteTemplate(w, "preferences", data); err != nil { + log.Printf("Error executing template in PreferencesHandler: %v", err) + http.Error(w, "Internal Server Error", http.StatusInternalServerError) + return + } + } +} diff --git a/main.go b/main.go index 5ed803c..067d3bd 100644 --- a/main.go +++ b/main.go @@ -217,6 +217,21 @@ func createTablesIfNotExist(db *sql.DB) error { return fmt.Errorf("error creating users table: %v", err) } + // Create user_preferences table + _, err = db.Exec(` + CREATE TABLE user_preferences ( + id INT AUTO_INCREMENT PRIMARY KEY, + user_id INT NOT NULL UNIQUE, + auto_save_drafts BOOLEAN DEFAULT TRUE, + markdown_preview_default VARCHAR(20) DEFAULT 'edit', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE + )`) + if err != nil { + return fmt.Errorf("error creating user_preferences table: %v", err) + } + log.Println("Database tables created.") return nil } @@ -362,6 +377,7 @@ func main() { filepath.Join(dir, "templates/pages/thread.html"), filepath.Join(dir, "templates/pages/userhome.html"), filepath.Join(dir, "templates/pages/chat.html"), + filepath.Join(dir, "templates/pages/preferences.html"), ) if err != nil { log.Fatal("Error parsing page templates:", err) @@ -399,6 +415,7 @@ func main() { http.HandleFunc(config.ThreadrDir+"/about/", app.SessionMW(handlers.AboutHandler(app))) http.HandleFunc(config.ThreadrDir+"/profile/", app.SessionMW(app.RequireLoginMW(handlers.ProfileHandler(app)))) http.HandleFunc(config.ThreadrDir+"/profile/edit/", app.SessionMW(app.RequireLoginMW(handlers.ProfileEditHandler(app)))) + http.HandleFunc(config.ThreadrDir+"/preferences/", app.SessionMW(app.RequireLoginMW(handlers.PreferencesHandler(app)))) http.HandleFunc(config.ThreadrDir+"/like/", app.SessionMW(app.RequireLoginMW(handlers.LikeHandler(app)))) http.HandleFunc(config.ThreadrDir+"/news/", app.SessionMW(handlers.NewsHandler(app))) http.HandleFunc(config.ThreadrDir+"/signup/", app.SessionMW(handlers.SignupHandler(app))) diff --git a/models/user_preferences.go b/models/user_preferences.go new file mode 100644 index 0000000..b74fe4f --- /dev/null +++ b/models/user_preferences.go @@ -0,0 +1,70 @@ +package models + +import ( + "database/sql" +) + +type UserPreferences struct { + ID int + UserID int + AutoSaveDrafts bool + MarkdownPreviewDefault string // "edit" or "preview" +} + +// GetUserPreferences retrieves preferences for a user, creating defaults if none exist +func GetUserPreferences(db *sql.DB, userID int) (*UserPreferences, error) { + query := `SELECT id, user_id, auto_save_drafts, markdown_preview_default + FROM user_preferences WHERE user_id = ?` + + prefs := &UserPreferences{} + err := db.QueryRow(query, userID).Scan( + &prefs.ID, + &prefs.UserID, + &prefs.AutoSaveDrafts, + &prefs.MarkdownPreviewDefault, + ) + + if err == sql.ErrNoRows { + // No preferences exist, create defaults + return CreateDefaultPreferences(db, userID) + } + + if err != nil { + return nil, err + } + + return prefs, nil +} + +// CreateDefaultPreferences creates default preferences for a new user +func CreateDefaultPreferences(db *sql.DB, userID int) (*UserPreferences, error) { + query := `INSERT INTO user_preferences (user_id, auto_save_drafts, markdown_preview_default) + VALUES (?, TRUE, 'edit')` + + result, err := db.Exec(query, userID) + if err != nil { + return nil, err + } + + id, err := result.LastInsertId() + if err != nil { + return nil, err + } + + return &UserPreferences{ + ID: int(id), + UserID: userID, + AutoSaveDrafts: true, + MarkdownPreviewDefault: "edit", + }, nil +} + +// UpdateUserPreferences updates user preferences +func UpdateUserPreferences(db *sql.DB, prefs *UserPreferences) error { + query := `UPDATE user_preferences + SET auto_save_drafts = ?, markdown_preview_default = ?, updated_at = NOW() + WHERE user_id = ?` + + _, err := db.Exec(query, prefs.AutoSaveDrafts, prefs.MarkdownPreviewDefault, prefs.UserID) + return err +} diff --git a/templates/pages/preferences.html b/templates/pages/preferences.html new file mode 100644 index 0000000..6e7c739 --- /dev/null +++ b/templates/pages/preferences.html @@ -0,0 +1,51 @@ +{{define "preferences"}} + + + + {{.Title}} + + + + + {{template "navbar" .}} +
+
+

Preferences

+
+ {{if .ShowSuccess}} +
+ Preferences saved successfully! +
+ {{end}} +
+
+

Draft Auto-Save

+ +

+ Drafts are saved to your browser's local storage and restored when you return to chat. +

+ +

Markdown Preview

+ + +

+ Choose which tab is shown by default when composing messages in chat. +

+ + +
+
+
+ {{template "cookie_banner" .}} + + +{{end}} diff --git a/templates/partials/navbar.html b/templates/partials/navbar.html index 0f6af1d..6cb89d0 100644 --- a/templates/partials/navbar.html +++ b/templates/partials/navbar.html @@ -4,6 +4,7 @@ {{if .LoggedIn}}
  • User Home
  • Profile
  • +
  • Preferences
  • Logout
  • {{else}}
  • Login