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.jocadbz
parent
83113a563a
commit
309e516480
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
17
main.go
17
main.go
|
|
@ -217,6 +217,21 @@ func createTablesIfNotExist(db *sql.DB) error {
|
||||||
return fmt.Errorf("error creating users table: %v", err)
|
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.")
|
log.Println("Database tables created.")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
@ -362,6 +377,7 @@ func main() {
|
||||||
filepath.Join(dir, "templates/pages/thread.html"),
|
filepath.Join(dir, "templates/pages/thread.html"),
|
||||||
filepath.Join(dir, "templates/pages/userhome.html"),
|
filepath.Join(dir, "templates/pages/userhome.html"),
|
||||||
filepath.Join(dir, "templates/pages/chat.html"),
|
filepath.Join(dir, "templates/pages/chat.html"),
|
||||||
|
filepath.Join(dir, "templates/pages/preferences.html"),
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal("Error parsing page templates:", err)
|
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+"/about/", app.SessionMW(handlers.AboutHandler(app)))
|
||||||
http.HandleFunc(config.ThreadrDir+"/profile/", app.SessionMW(app.RequireLoginMW(handlers.ProfileHandler(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+"/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+"/like/", app.SessionMW(app.RequireLoginMW(handlers.LikeHandler(app))))
|
||||||
http.HandleFunc(config.ThreadrDir+"/news/", app.SessionMW(handlers.NewsHandler(app)))
|
http.HandleFunc(config.ThreadrDir+"/news/", app.SessionMW(handlers.NewsHandler(app)))
|
||||||
http.HandleFunc(config.ThreadrDir+"/signup/", app.SessionMW(handlers.SignupHandler(app)))
|
http.HandleFunc(config.ThreadrDir+"/signup/", app.SessionMW(handlers.SignupHandler(app)))
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,51 @@
|
||||||
|
{{define "preferences"}}
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>{{.Title}}</title>
|
||||||
|
<link rel="stylesheet" href="{{.StaticPath}}/style.css">
|
||||||
|
<script src="{{.StaticPath}}/app.js" defer></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
{{template "navbar" .}}
|
||||||
|
<main>
|
||||||
|
<header>
|
||||||
|
<h2>Preferences</h2>
|
||||||
|
</header>
|
||||||
|
{{if .ShowSuccess}}
|
||||||
|
<div class="notification success" style="position: static; margin-bottom: 1em; animation: none;">
|
||||||
|
Preferences saved successfully!
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
<section>
|
||||||
|
<form method="post" action="{{.BasePath}}/preferences/">
|
||||||
|
<h3>Draft Auto-Save</h3>
|
||||||
|
<label for="auto_save_drafts" style="display: flex; align-items: center; gap: 0.5em; cursor: pointer;">
|
||||||
|
<input type="checkbox" id="auto_save_drafts" name="auto_save_drafts" {{if .Preferences.AutoSaveDrafts}}checked{{end}}>
|
||||||
|
<span>Automatically save drafts while typing in chat</span>
|
||||||
|
</label>
|
||||||
|
<p style="margin-left: 1.5em; margin-top: 0.25em; font-size: 0.9em; opacity: 0.8;">
|
||||||
|
Drafts are saved to your browser's local storage and restored when you return to chat.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h3 style="margin-top: 2em;">Markdown Preview</h3>
|
||||||
|
<label for="markdown_preview_edit" style="display: flex; align-items: center; gap: 0.5em; cursor: pointer; margin-bottom: 0.5em;">
|
||||||
|
<input type="radio" id="markdown_preview_edit" name="markdown_preview_default" value="edit" {{if eq .Preferences.MarkdownPreviewDefault "edit"}}checked{{end}}>
|
||||||
|
<span>Default to <strong>Edit</strong> mode (write markdown)</span>
|
||||||
|
</label>
|
||||||
|
<label for="markdown_preview_preview" style="display: flex; align-items: center; gap: 0.5em; cursor: pointer;">
|
||||||
|
<input type="radio" id="markdown_preview_preview" name="markdown_preview_default" value="preview" {{if eq .Preferences.MarkdownPreviewDefault "preview"}}checked{{end}}>
|
||||||
|
<span>Default to <strong>Preview</strong> mode (see formatted output)</span>
|
||||||
|
</label>
|
||||||
|
<p style="margin-left: 1.5em; margin-top: 0.25em; font-size: 0.9em; opacity: 0.8;">
|
||||||
|
Choose which tab is shown by default when composing messages in chat.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<input type="submit" value="Save Preferences" style="margin-top: 2em;">
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
{{template "cookie_banner" .}}
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
{{end}}
|
||||||
|
|
@ -4,6 +4,7 @@
|
||||||
{{if .LoggedIn}}
|
{{if .LoggedIn}}
|
||||||
<li><a {{if eq .Navbar "userhome"}}class="active"{{end}} href="{{.BasePath}}/userhome/">User Home</a></li>
|
<li><a {{if eq .Navbar "userhome"}}class="active"{{end}} href="{{.BasePath}}/userhome/">User Home</a></li>
|
||||||
<li><a {{if eq .Navbar "profile"}}class="active"{{end}} href="{{.BasePath}}/profile/">Profile</a></li>
|
<li><a {{if eq .Navbar "profile"}}class="active"{{end}} href="{{.BasePath}}/profile/">Profile</a></li>
|
||||||
|
<li><a {{if eq .Navbar "preferences"}}class="active"{{end}} href="{{.BasePath}}/preferences/">Preferences</a></li>
|
||||||
<li><a href="{{.BasePath}}/logout/">Logout</a></li>
|
<li><a href="{{.BasePath}}/logout/">Logout</a></li>
|
||||||
{{else}}
|
{{else}}
|
||||||
<li><a {{if eq .Navbar "login"}}class="active"{{end}} href="{{.BasePath}}/login/">Login</a></li>
|
<li><a {{if eq .Navbar "login"}}class="active"{{end}} href="{{.BasePath}}/login/">Login</a></li>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue