Profiles, Giphy, uploads, settings UI

Backend:
- Auth: split routes into RegisterPublicRoutes + RegisterProtectedRoutes
- Auth: PATCH /me for profile updates (display_name, bio, accent_color, status_text)
- Auth: Gravatar fallback for avatars on register
- DB: users table now has bio, accent_color, status_text columns
- giphy/client.go: Search() and GetTrending() against Giphy API
- upload/handlers.go: MinIO file upload + serve
- config: added GiphyConfig and MinIO config
- cmd/server: wired giphy (/gifs/search, /gifs/trending), upload (/upload), files (/files/*) routes

Frontend:
- auth store: updated User interface with new profile fields, added updateProfile()
- UserSettings.tsx: terminal-styled profile editor (display_name, bio, accent_color, status_text, avatar upload)
- GiphyPicker.tsx: terminal-styled GIF picker with search + trending
- ChatArea.tsx: integrated [GIF] button into message input
- App.tsx: imports UserSettings, added /settings route
This commit is contained in:
2026-06-28 16:06:08 -04:00
parent bb5a56816b
commit e69553af02
18 changed files with 1422 additions and 352 deletions
+57 -2
View File
@@ -1,6 +1,7 @@
package main
import (
"encoding/json"
"fmt"
"log/slog"
"net/http"
@@ -11,9 +12,11 @@ import (
"git.dustin.coffee/hobokenchicken/dumpsterChat/internal/config"
"git.dustin.coffee/hobokenchicken/dumpsterChat/internal/db"
"git.dustin.coffee/hobokenchicken/dumpsterChat/internal/gateway"
"git.dustin.coffee/hobokenchicken/dumpsterChat/internal/giphy"
"git.dustin.coffee/hobokenchicken/dumpsterChat/internal/message"
"git.dustin.coffee/hobokenchicken/dumpsterChat/internal/middleware"
"git.dustin.coffee/hobokenchicken/dumpsterChat/internal/server"
"git.dustin.coffee/hobokenchicken/dumpsterChat/internal/upload"
"github.com/go-chi/chi/v5"
chimw "github.com/go-chi/chi/v5/middleware"
)
@@ -42,6 +45,18 @@ func main() {
hub := gateway.NewHub(logger)
go hub.Run()
// Giphy client (nil if no API key)
giphyClient := giphy.NewClient(cfg.Giphy.APIKey)
// Upload handler (nil if no MinIO config)
uploadHandler, err := upload.NewHandler(cfg)
if err != nil {
logger.Warn("upload handler not available", "error", err)
}
// Auth handler
authHandler := auth.NewHandler(database.DB, cfg)
r := chi.NewRouter()
r.Use(chimw.Logger)
r.Use(chimw.Recoverer)
@@ -60,9 +75,9 @@ func main() {
// API routes
r.Route("/api/v1", func(r chi.Router) {
// Auth (public)
// Auth (public: register, login, logout)
r.Route("/auth", func(r chi.Router) {
auth.NewHandler(database.DB, cfg).RegisterRoutes(r)
authHandler.RegisterPublicRoutes(r)
})
// Protected routes
@@ -70,6 +85,9 @@ func main() {
r.Use(middleware.Session(sessionStore, cfg))
r.Use(middleware.RequireAuth)
// Auth (protected: me, update profile)
authHandler.RegisterProtectedRoutes(r)
// Servers
r.Route("/servers", func(r chi.Router) {
server.NewHandler(database.DB).RegisterRoutes(r)
@@ -84,9 +102,46 @@ func main() {
r.Route("/channels/{channelID}/messages", func(r chi.Router) {
message.NewHandler(database.DB, hub).RegisterRoutes(r)
})
// Giphy search
if giphyClient != nil {
r.Get("/gifs/search", func(w http.ResponseWriter, r *http.Request) {
query := r.URL.Query().Get("q")
if query == "" {
http.Error(w, `{"error":"missing query"}`, http.StatusBadRequest)
return
}
gifs, err := giphyClient.Search(query, 20)
if err != nil {
http.Error(w, `{"error":"search failed"}`, http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(gifs)
})
r.Get("/gifs/trending", func(w http.ResponseWriter, r *http.Request) {
gifs, err := giphyClient.GetTrending(20)
if err != nil {
http.Error(w, `{"error":"trending failed"}`, http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(gifs)
})
}
// File upload
if uploadHandler != nil {
r.Post("/upload", uploadHandler.Upload)
}
})
})
// File serving (public, for viewing uploaded files)
if uploadHandler != nil {
r.Get("/files/*", uploadHandler.Serve)
}
// Static file serving for production (SPA)
staticDir := "/srv/web"
if _, err := os.Stat(staticDir); err == nil {