feat: implement scroll position restoration for mobile tabs and add double-tap home navigation to refresh feed views.
This commit is contained in:
@@ -134,6 +134,50 @@ func (h *Handler) UpdatePassword(w http.ResponseWriter, r *http.Request) {
|
||||
h.writeJSON(w, http.StatusOK, map[string]string{"message": "password updated successfully"})
|
||||
}
|
||||
|
||||
// User Preferences Handlers
|
||||
func (h *Handler) GetUserPreferences(w http.ResponseWriter, r *http.Request) {
|
||||
claims, ok := r.Context().Value(auth.UserContextKey).(*auth.Claims)
|
||||
if !ok {
|
||||
h.writeError(w, http.StatusUnauthorized, "unauthorized")
|
||||
return
|
||||
}
|
||||
user, err := h.db.GetUserByUsername(claims.Username)
|
||||
if err != nil {
|
||||
h.writeError(w, http.StatusNotFound, "user not found")
|
||||
return
|
||||
}
|
||||
h.writeJSON(w, http.StatusOK, user.Preferences)
|
||||
}
|
||||
|
||||
func (h *Handler) UpdateUserPreferences(w http.ResponseWriter, r *http.Request) {
|
||||
claims, ok := r.Context().Value(auth.UserContextKey).(*auth.Claims)
|
||||
if !ok {
|
||||
h.writeError(w, http.StatusUnauthorized, "unauthorized")
|
||||
return
|
||||
}
|
||||
user, err := h.db.GetUserByUsername(claims.Username)
|
||||
if err != nil {
|
||||
h.writeError(w, http.StatusNotFound, "user not found")
|
||||
return
|
||||
}
|
||||
var newPrefs map[string]interface{}
|
||||
if err := json.NewDecoder(r.Body).Decode(&newPrefs); err != nil {
|
||||
h.writeError(w, http.StatusBadRequest, "invalid request payload")
|
||||
return
|
||||
}
|
||||
if user.Preferences == nil {
|
||||
user.Preferences = make(map[string]interface{})
|
||||
}
|
||||
for k, v := range newPrefs {
|
||||
user.Preferences[k] = v
|
||||
}
|
||||
if err := h.db.UpdateUserPreferences(user.ID, user.Preferences); err != nil {
|
||||
h.writeError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
h.writeJSON(w, http.StatusOK, user.Preferences)
|
||||
}
|
||||
|
||||
// Stats Handler
|
||||
func (h *Handler) GetStats(w http.ResponseWriter, r *http.Request) {
|
||||
stats, err := h.db.GetStats()
|
||||
|
||||
@@ -45,6 +45,8 @@ func NewRouter(h *Handler, authService *auth.AuthService, staticDir string) http
|
||||
|
||||
r.Get("/auth/me", h.GetMe)
|
||||
r.Put("/auth/password", h.UpdatePassword)
|
||||
r.Get("/user/preferences", h.GetUserPreferences)
|
||||
r.Put("/user/preferences", h.UpdateUserPreferences)
|
||||
r.Get("/stats", h.GetStats)
|
||||
|
||||
// Categories
|
||||
|
||||
+28
-4
@@ -168,6 +168,9 @@ func (d *DB) migrate() error {
|
||||
WHERE (icon_url IS NULL OR icon_url = '') AND site_url IS NOT NULL AND site_url != ''
|
||||
`)
|
||||
|
||||
// Migration for preferences column on users table if older DB exists
|
||||
_, _ = d.Exec("ALTER TABLE users ADD COLUMN preferences TEXT DEFAULT '{}'")
|
||||
|
||||
// Migration for rules table schema if older table exists
|
||||
var testCol string
|
||||
err := d.QueryRow("SELECT trigger FROM rules LIMIT 1").Scan(&testCol)
|
||||
@@ -199,13 +202,20 @@ func (d *DB) CountUsers() (int, error) {
|
||||
|
||||
func (d *DB) GetUserByUsername(username string) (*models.User, error) {
|
||||
var u models.User
|
||||
var prefsRaw sql.NullString
|
||||
err := d.QueryRow(
|
||||
"SELECT id, username, password_hash, created_at, updated_at FROM users WHERE username = ?",
|
||||
"SELECT id, username, password_hash, preferences, created_at, updated_at FROM users WHERE username = ?",
|
||||
username,
|
||||
).Scan(&u.ID, &u.Username, &u.PasswordHash, &u.CreatedAt, &u.UpdatedAt)
|
||||
).Scan(&u.ID, &u.Username, &u.PasswordHash, &prefsRaw, &u.CreatedAt, &u.UpdatedAt)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if prefsRaw.Valid && prefsRaw.String != "" {
|
||||
_ = json.Unmarshal([]byte(prefsRaw.String), &u.Preferences)
|
||||
}
|
||||
if u.Preferences == nil {
|
||||
u.Preferences = make(map[string]interface{})
|
||||
}
|
||||
return &u, nil
|
||||
}
|
||||
|
||||
@@ -215,12 +225,13 @@ func (d *DB) CreateUser(username, passwordHash string) (*models.User, error) {
|
||||
ID: uuid.New().String(),
|
||||
Username: username,
|
||||
PasswordHash: passwordHash,
|
||||
Preferences: make(map[string]interface{}),
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
_, err := d.Exec(
|
||||
"INSERT INTO users (id, username, password_hash, created_at, updated_at) VALUES (?, ?, ?, ?, ?)",
|
||||
u.ID, u.Username, u.PasswordHash, u.CreatedAt, u.UpdatedAt,
|
||||
"INSERT INTO users (id, username, password_hash, preferences, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)",
|
||||
u.ID, u.Username, u.PasswordHash, "{}", u.CreatedAt, u.UpdatedAt,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -237,6 +248,19 @@ func (d *DB) UpdateUserPassword(userID, passwordHash string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
func (d *DB) UpdateUserPreferences(userID string, prefs map[string]interface{}) error {
|
||||
raw, err := json.Marshal(prefs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
now := time.Now().UTC()
|
||||
_, err = d.Exec(
|
||||
"UPDATE users SET preferences = ?, updated_at = ? WHERE id = ?",
|
||||
string(raw), now, userID,
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
// Category operations
|
||||
func (d *DB) ListCategories() ([]models.Category, error) {
|
||||
query := `
|
||||
|
||||
@@ -6,11 +6,12 @@ import (
|
||||
|
||||
// User represents the single admin user
|
||||
type User struct {
|
||||
ID string `json:"id"`
|
||||
Username string `json:"username"`
|
||||
PasswordHash string `json:"-"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
ID string `json:"id"`
|
||||
Username string `json:"username"`
|
||||
PasswordHash string `json:"-"`
|
||||
Preferences map[string]interface{} `json:"preferences,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
// SearchHistoryItem tracks user search history for autocomplete
|
||||
|
||||
Reference in New Issue
Block a user