go-htmx-gsd #1

Merged
arthur merged 558 commits from go-htmx-gsd into main 2026-05-23 15:16:44 +00:00
8 changed files with 101 additions and 15 deletions
Showing only changes of commit c7a16dbcae - Show all commits

View file

@ -1,9 +1,11 @@
package web
import (
"log/slog"
"net/http"
"backend/internal/auth"
"backend/internal/db/sqlc"
"backend/templates"
"github.com/gorilla/csrf"
@ -34,7 +36,16 @@ func AccountProvidersHandler(deps AuthDeps) http.HandlerFunc {
}
}
sidebarTablos, err := deps.Queries.ListTablosByUser(r.Context(), user.ID)
if err != nil {
slog.Default().Error("account providers: ListTablosByUser failed", "user_id", user.ID, "err", err)
sidebarTablos = []sqlc.Tablo{}
}
if sidebarTablos == nil {
sidebarTablos = []sqlc.Tablo{}
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
_ = templates.AccountProvidersPage(user, statuses, csrf.Token(r)).Render(r.Context(), w)
_ = templates.AccountProvidersPage(user, statuses, csrf.Token(r), "/", sidebarTablos).Render(r.Context(), w)
}
}

View file

@ -64,7 +64,15 @@ func TabloDiscussionTabHandler(deps DiscussionDeps) http.HandlerFunc {
_ = templates.DiscussionTabFragment(tablo, data, templates.DiscussionForm{}, templates.DiscussionErrors{}, csrf.Token(r)).Render(r.Context(), w)
return
}
_ = templates.TabloDetailPage(user, csrf.Token(r), tablo, nil, nil, templates.EtapeTaskCounts{}, templates.EtapeFilter{}, nil, templates.EventsCalendar{}, data, "discussion").Render(r.Context(), w)
discussionSidebarTablos, sidebarErr := deps.Queries.ListTablosByUser(r.Context(), user.ID)
if sidebarErr != nil {
slog.Default().Error("discussion: ListTablosByUser failed", "user_id", user.ID, "err", sidebarErr)
discussionSidebarTablos = []sqlc.Tablo{}
}
if discussionSidebarTablos == nil {
discussionSidebarTablos = []sqlc.Tablo{}
}
_ = templates.TabloDetailPage(user, csrf.Token(r), "", discussionSidebarTablos, tablo, nil, nil, templates.EtapeTaskCounts{}, templates.EtapeFilter{}, nil, templates.EventsCalendar{}, data, "discussion").Render(r.Context(), w)
}
}

View file

@ -166,7 +166,15 @@ func TabloEventsTabHandler(deps EventsDeps) http.HandlerFunc {
_ = templates.EventsTabFragment(tablo, calendar, csrf.Token(r)).Render(r.Context(), w)
return
}
_ = templates.TabloDetailPage(user, csrf.Token(r), tablo, nil, nil, templates.EtapeTaskCounts{}, templates.EtapeFilter{}, nil, calendar, templates.DiscussionTabData{}, "events").Render(r.Context(), w)
eventsSidebarTablos, sidebarErr := deps.Queries.ListTablosByUser(r.Context(), user.ID)
if sidebarErr != nil {
slog.Default().Error("events: ListTablosByUser failed", "user_id", user.ID, "err", sidebarErr)
eventsSidebarTablos = []sqlc.Tablo{}
}
if eventsSidebarTablos == nil {
eventsSidebarTablos = []sqlc.Tablo{}
}
_ = templates.TabloDetailPage(user, csrf.Token(r), "", eventsSidebarTablos, tablo, nil, nil, templates.EtapeTaskCounts{}, templates.EtapeFilter{}, nil, calendar, templates.DiscussionTabData{}, "events").Render(r.Context(), w)
}
}

View file

@ -95,7 +95,15 @@ func TabloFilesTabHandler(deps FilesDeps) http.HandlerFunc {
_ = templates.FilesTabFragment(tablo, fileList, csrf.Token(r)).Render(r.Context(), w)
return
}
_ = templates.TabloDetailPage(user, csrf.Token(r), tablo, nil, nil, templates.EtapeTaskCounts{}, templates.EtapeFilter{}, fileList, templates.EventsCalendar{}, templates.DiscussionTabData{}, "files").Render(r.Context(), w)
filesSidebarTablos, sidebarErr := deps.Queries.ListTablosByUser(r.Context(), user.ID)
if sidebarErr != nil {
slog.Default().Error("files tab: ListTablosByUser failed", "user_id", user.ID, "err", sidebarErr)
filesSidebarTablos = []sqlc.Tablo{}
}
if filesSidebarTablos == nil {
filesSidebarTablos = []sqlc.Tablo{}
}
_ = templates.TabloDetailPage(user, csrf.Token(r), "", filesSidebarTablos, tablo, nil, nil, templates.EtapeTaskCounts{}, templates.EtapeFilter{}, fileList, templates.EventsCalendar{}, templates.DiscussionTabData{}, "files").Render(r.Context(), w)
}
}
@ -119,7 +127,15 @@ func TabloTasksTabHandler(deps FilesDeps) http.HandlerFunc {
_ = templates.TasksTabFragment(tablo, tasks, etapes, counts, filter, csrf.Token(r)).Render(r.Context(), w)
return
}
_ = templates.TabloDetailPage(user, csrf.Token(r), tablo, tasks, etapes, counts, filter, nil, templates.EventsCalendar{}, templates.DiscussionTabData{}, "tasks").Render(r.Context(), w)
tasksSidebarTablos, sidebarErr := deps.Queries.ListTablosByUser(r.Context(), user.ID)
if sidebarErr != nil {
slog.Default().Error("tasks tab: ListTablosByUser failed", "user_id", user.ID, "err", sidebarErr)
tasksSidebarTablos = []sqlc.Tablo{}
}
if tasksSidebarTablos == nil {
tasksSidebarTablos = []sqlc.Tablo{}
}
_ = templates.TabloDetailPage(user, csrf.Token(r), "", tasksSidebarTablos, tablo, tasks, etapes, counts, filter, nil, templates.EventsCalendar{}, templates.DiscussionTabData{}, "tasks").Render(r.Context(), w)
}
}

View file

@ -53,7 +53,18 @@ func PlanningPageHandler(deps PlanningDeps) http.HandlerFunc {
}
agenda := templates.NewPlanningAgenda(start, end, parsePlanningStart("", now()), rows)
sidebarTablos, err := deps.Queries.ListTablosByUser(r.Context(), user.ID)
if err != nil {
slog.Default().Error("planning: ListTablosByUser failed", "user_id", user.ID, "err", err)
http.Error(w, "internal server error", http.StatusInternalServerError)
return
}
if sidebarTablos == nil {
sidebarTablos = []sqlc.Tablo{}
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
_ = templates.PlanningPage(user, csrf.Token(r), agenda).Render(r.Context(), w)
_ = templates.PlanningPage(user, csrf.Token(r), "/planning", sidebarTablos, agenda).Render(r.Context(), w)
}
}

View file

@ -50,8 +50,14 @@ func TablosListHandler(deps TablosDeps) http.HandlerFunc {
tabloRows = []sqlc.ListTablosByUserWithDiscussionUnreadRow{}
}
cardViews := templates.TabloCardsFromUnreadRows(tabloRows)
sidebarTablos := make([]sqlc.Tablo, 0, len(cardViews))
for _, cv := range cardViews {
sidebarTablos = append(sidebarTablos, cv.Tablo)
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
_ = templates.TablosDashboard(user, csrf.Token(r), templates.TabloCardsFromUnreadRows(tabloRows)).Render(r.Context(), w)
_ = templates.TablosDashboard(user, csrf.Token(r), "/", sidebarTablos, cardViews).Render(r.Context(), w)
}
}
@ -201,8 +207,18 @@ func TabloDetailHandler(deps TablosDeps) http.HandlerFunc {
if tasks == nil {
tasks = []sqlc.Task{}
}
// Fetch sidebar tablos for AppLayout. On error, log and use empty slice —
// the page still renders, just without a populated sidebar projects list.
sidebarTablos, err := deps.Queries.ListTablosByUser(r.Context(), user.ID)
if err != nil {
slog.Default().Error("tablos detail: ListTablosByUser failed", "user_id", user.ID, "err", err)
sidebarTablos = []sqlc.Tablo{}
}
if sidebarTablos == nil {
sidebarTablos = []sqlc.Tablo{}
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
_ = templates.TabloDetailPage(user, csrf.Token(r), tablo, tasks, nil, templates.EtapeTaskCounts{}, templates.EtapeFilter{}, nil, templates.EventsCalendar{}, templates.DiscussionTabData{}, "overview").Render(r.Context(), w)
_ = templates.TabloDetailPage(user, csrf.Token(r), "", sidebarTablos, tablo, tasks, nil, templates.EtapeTaskCounts{}, templates.EtapeFilter{}, nil, templates.EventsCalendar{}, templates.DiscussionTabData{}, "overview").Render(r.Context(), w)
}
}
@ -308,7 +324,14 @@ func TabloUpdateHandler(deps TablosDeps) http.HandlerFunc {
if tasks == nil {
tasks = []sqlc.Task{}
}
_ = templates.TabloDetailPage(user, csrf.Token(r), tablo, tasks, nil, templates.EtapeTaskCounts{}, templates.EtapeFilter{}, nil, templates.EventsCalendar{}, templates.DiscussionTabData{}, "overview").Render(ctx, w)
updateSidebarTablos, sidebarErr := deps.Queries.ListTablosByUser(ctx, user.ID)
if sidebarErr != nil {
updateSidebarTablos = []sqlc.Tablo{}
}
if updateSidebarTablos == nil {
updateSidebarTablos = []sqlc.Tablo{}
}
_ = templates.TabloDetailPage(user, csrf.Token(r), "", updateSidebarTablos, tablo, tasks, nil, templates.EtapeTaskCounts{}, templates.EtapeFilter{}, nil, templates.EventsCalendar{}, templates.DiscussionTabData{}, "overview").Render(ctx, w)
return
}
@ -430,5 +453,10 @@ func renderTabloCreateError(w http.ResponseWriter, r *http.Request, form templat
// Render full page — form fragment is not embedded in the full page by default;
// for the non-HTMX error case we redirect so the user sees their list intact
// and can try again (simpler than threading form state through the full page).
_ = templates.TablosDashboard(user, csrf.Token(r), templates.TabloCardsFromUnreadRows(tabloRows)).Render(r.Context(), w)
errorCardViews := templates.TabloCardsFromUnreadRows(tabloRows)
errorSidebarTablos := make([]sqlc.Tablo, 0, len(errorCardViews))
for _, cv := range errorCardViews {
errorSidebarTablos = append(errorSidebarTablos, cv.Tablo)
}
_ = templates.TablosDashboard(user, csrf.Token(r), "/", errorSidebarTablos, errorCardViews).Render(r.Context(), w)
}

View file

@ -1,9 +1,12 @@
package templates
import "backend/internal/auth"
import (
"backend/internal/auth"
"backend/internal/db/sqlc"
)
templ AccountProvidersPage(user *auth.User, providers []LinkedProviderStatus, csrfToken string) {
@Layout("Linked providers", user, csrfToken) {
templ AccountProvidersPage(user *auth.User, providers []LinkedProviderStatus, csrfToken string, activePath string, tablos []sqlc.Tablo) {
@AppLayout("Linked providers", user, csrfToken, activePath, tablos) {
<section class="mx-auto max-w-xl">
<h1 class="mb-6 text-xl font-semibold">Linked providers</h1>
<div class="space-y-2">

View file

@ -2,10 +2,11 @@ package templates
import (
"backend/internal/auth"
"backend/internal/db/sqlc"
)
templ PlanningPage(user *auth.User, csrfToken string, agenda PlanningAgenda) {
@Layout("Planning - Xtablo", user, csrfToken) {
templ PlanningPage(user *auth.User, csrfToken string, activePath string, tablos []sqlc.Tablo, agenda PlanningAgenda) {
@AppLayout("Planning - Xtablo", user, csrfToken, activePath, tablos) {
<div class="space-y-6">
<div class="flex flex-wrap items-start justify-between gap-4">
<div>