diff --git a/go-backend/internal/web/handlers/auth.go b/go-backend/internal/web/handlers/auth.go index bd8dbab..7b7df0c 100644 --- a/go-backend/internal/web/handlers/auth.go +++ b/go-backend/internal/web/handlers/auth.go @@ -98,9 +98,9 @@ func (h *AuthHandler) GetHome() http.HandlerFunc { content := views.OverviewMainContent(user.DisplayName, user.Email, projects) var renderErr error if isHXRequest(r) { - renderErr = views.DashboardContentSwap("/", content).Render(r.Context(), w) + renderErr = views.DashboardContentSwap("/", tablos, content).Render(r.Context(), w) } else { - renderErr = views.DashboardPage("/", content).Render(r.Context(), w) + renderErr = views.DashboardPage("/", tablos, content).Render(r.Context(), w) } if renderErr != nil { http.Error(w, "failed to render app page", http.StatusInternalServerError) @@ -152,16 +152,24 @@ func (h *AuthHandler) GetNotFound() http.HandlerFunc { return } + tablos, err := h.repo.ListTablos(r.Context(), ListTablosInput{ + OwnerID: user.ID, + }) + if err != nil { + http.Error(w, "failed to load projects", http.StatusInternalServerError) + return + } + w.Header().Set("Content-Type", "text/html; charset=utf-8") w.WriteHeader(http.StatusNotFound) content := views.NotFoundContent(user.DisplayName) - var err error + var renderErr error if isHXRequest(r) { - err = views.DashboardContentSwap("", content).Render(r.Context(), w) + renderErr = views.DashboardContentSwap("", tablos, content).Render(r.Context(), w) } else { - err = views.DashboardPage("", content).Render(r.Context(), w) + renderErr = views.DashboardPage("", tablos, content).Render(r.Context(), w) } - if err != nil { + if renderErr != nil { http.Error(w, "failed to render not found page", http.StatusInternalServerError) } } @@ -175,15 +183,23 @@ func (h *AuthHandler) renderAppPage(activePath string, content func(user PublicU return } + tablos, err := h.repo.ListTablos(r.Context(), ListTablosInput{ + OwnerID: user.ID, + }) + if err != nil { + http.Error(w, "failed to load projects", http.StatusInternalServerError) + return + } + w.Header().Set("Content-Type", "text/html; charset=utf-8") pageContent := content(user) - var err error + var renderErr error if isHXRequest(r) { - err = views.DashboardContentSwap(activePath, pageContent).Render(r.Context(), w) + renderErr = views.DashboardContentSwap(activePath, tablos, pageContent).Render(r.Context(), w) } else { - err = views.DashboardPage(activePath, pageContent).Render(r.Context(), w) + renderErr = views.DashboardPage(activePath, tablos, pageContent).Render(r.Context(), w) } - if err != nil { + if renderErr != nil { http.Error(w, "failed to render app page", http.StatusInternalServerError) } } diff --git a/go-backend/internal/web/handlers/auth_test.go b/go-backend/internal/web/handlers/auth_test.go index ad27bbd..43f5641 100644 --- a/go-backend/internal/web/handlers/auth_test.go +++ b/go-backend/internal/web/handlers/auth_test.go @@ -1,6 +1,7 @@ package handlers import ( + "context" "bytes" "net/http" "net/http/httptest" @@ -114,6 +115,59 @@ func TestLogoutLogsSessionDeletion(t *testing.T) { } } +func TestTasksPageSidebarShowsRealTablos(t *testing.T) { + repo := NewInMemoryAuthRepository() + handler := NewAuthHandler(repo) + sessionCookie := loginTestUser(t, handler, "demo@xtablo.com", "xtablo-demo") + + req := httptest.NewRequest(http.MethodGet, "/", nil) + req.AddCookie(sessionCookie) + userID, ok := handler.currentUserID(req.Context(), req) + if !ok { + t.Fatal("expected user session") + } + + for _, tablo := range []CreateTabloInput{ + {OwnerID: userID, Name: "Blue", Color: "#3B82F6", Status: TabloStatusTodo}, + {OwnerID: userID, Name: "Green", Color: "#22C55E", Status: TabloStatusTodo}, + {OwnerID: userID, Name: "Purple", Color: "#A855F7", Status: TabloStatusTodo}, + {OwnerID: userID, Name: "Red", Color: "#EF4444", Status: TabloStatusTodo}, + {OwnerID: userID, Name: "Hidden Fifth", Color: "#EAB308", Status: TabloStatusTodo}, + } { + if _, err := repo.CreateTablo(context.Background(), tablo); err != nil { + t.Fatalf("create tablo %q: %v", tablo.Name, err) + } + } + + pageReq := httptest.NewRequest(http.MethodGet, "/tasks", nil) + pageReq.AddCookie(sessionCookie) + rec := httptest.NewRecorder() + + handler.GetTasksPage().ServeHTTP(rec, pageReq) + + if rec.Code != http.StatusOK { + t.Fatalf("expected status 200, got %d", rec.Code) + } + + body := rec.Body.String() + for _, want := range []string{ + "Green", + "Purple", + "Red", + "Hidden Fifth", + } { + if !strings.Contains(body, want) { + t.Fatalf("expected sidebar to contain %q, got %q", want, body) + } + } + if strings.Contains(body, "Blue") { + t.Fatalf("expected sidebar to limit to the four most recent tablos, got %q", body) + } + if !strings.Contains(body, `href="/tablos/`) { + t.Fatalf("expected sidebar project links, got %q", body) + } +} + func newTestAuthHandler(t *testing.T) *AuthHandler { t.Helper() return NewAuthHandler(NewInMemoryAuthRepository()) diff --git a/go-backend/internal/web/handlers/tablos.go b/go-backend/internal/web/handlers/tablos.go index 3d1811c..e724e4b 100644 --- a/go-backend/internal/web/handlers/tablos.go +++ b/go-backend/internal/web/handlers/tablos.go @@ -7,13 +7,13 @@ import ( "net/http" "net/url" "regexp" - "strconv" "strings" "time" "github.com/google/uuid" tablomodel "xtablo-backend/internal/tablos" "xtablo-backend/internal/web/dates" + "xtablo-backend/internal/web/tabloicons" "xtablo-backend/internal/web/views" ) @@ -24,29 +24,6 @@ var tabloColorPattern = regexp.MustCompile(`^#[0-9A-Fa-f]{6}$`) const defaultTabloColor = "#3B82F6" const tabloColorValidationMessage = "La couleur du projet doit être un code hexadécimal au format #RRGGBB" -type tabloPaletteEntry struct { - icon string - bg string - fg string - accent string - r int - g int - b int -} - -var tabloIconPalette = []tabloPaletteEntry{ - {icon: "bolt", bg: "bg-blue-500", fg: "text-white", accent: "blue", r: 59, g: 130, b: 246}, - {icon: "leaf", bg: "bg-green-500", fg: "text-white", accent: "green", r: 34, g: 197, b: 94}, - {icon: "gem", bg: "bg-purple-500", fg: "text-white", accent: "purple", r: 168, g: 85, b: 247}, - {icon: "flame", bg: "bg-red-500", fg: "text-white", accent: "red", r: 239, g: 68, b: 68}, - {icon: "star", bg: "bg-yellow-500", fg: "text-gray-700", accent: "yellow", r: 234, g: 179, b: 8}, - {icon: "compass", bg: "bg-indigo-500", fg: "text-white", accent: "indigo", r: 99, g: 102, b: 241}, - {icon: "heart", bg: "bg-pink-500", fg: "text-white", accent: "pink", r: 236, g: 72, b: 153}, - {icon: "waves", bg: "bg-teal-500", fg: "text-white", accent: "teal", r: 20, g: 184, b: 166}, - {icon: "sun", bg: "bg-orange-500", fg: "text-white", accent: "orange", r: 249, g: 115, b: 22}, - {icon: "sparkles", bg: "bg-cyan-500", fg: "text-gray-700", accent: "cyan", r: 6, g: 182, b: 212}, -} - type TabloStatus = tablomodel.Status const ( @@ -152,7 +129,7 @@ func (h *AuthHandler) PostTablos() http.HandlerFunc { http.Error(w, "failed to list tablos", http.StatusInternalServerError) return } - renderTablosResponse(w, r, "/tablos", tablosPageViewModel(user, state, tablos, name, r.FormValue("color"), "Le nom du projet est requis"), http.StatusUnprocessableEntity) + renderTablosResponse(w, r, "/tablos", tablos, tablosPageViewModel(user, state, tablos, name, r.FormValue("color"), "Le nom du projet est requis"), http.StatusUnprocessableEntity) return } @@ -163,7 +140,7 @@ func (h *AuthHandler) PostTablos() http.HandlerFunc { http.Error(w, "failed to list tablos", http.StatusInternalServerError) return } - renderTablosResponse(w, r, "/tablos", tablosPageViewModel(user, state, tablos, name, r.FormValue("color"), tabloColorValidationMessage), http.StatusUnprocessableEntity) + renderTablosResponse(w, r, "/tablos", tablos, tablosPageViewModel(user, state, tablos, name, r.FormValue("color"), tabloColorValidationMessage), http.StatusUnprocessableEntity) return } @@ -184,7 +161,7 @@ func (h *AuthHandler) PostTablos() http.HandlerFunc { return } - renderTablosResponse(w, r, "/tablos", tablosPageViewModel(user, state, tablos, "", "", ""), http.StatusOK) + renderTablosResponse(w, r, "/tablos", tablos, tablosPageViewModel(user, state, tablos, "", "", ""), http.StatusOK) } } @@ -218,7 +195,7 @@ func (h *AuthHandler) GetEditTabloModal() http.HandlerFunc { return } - renderTablosResponse(w, r, "/tablos", tablosPageViewModel(user, state, tablos, tablo.Name, tablo.Color, ""), http.StatusOK) + renderTablosResponse(w, r, "/tablos", tablos, tablosPageViewModel(user, state, tablos, tablo.Name, tablo.Color, ""), http.StatusOK) } } @@ -252,7 +229,7 @@ func (h *AuthHandler) PostTabloUpdate() http.HandlerFunc { http.Error(w, "failed to list tablos", http.StatusInternalServerError) return } - renderTablosResponse(w, r, "/tablos", tablosPageViewModel(user, state, tablos, name, r.FormValue("color"), "Le nom du projet est requis"), http.StatusUnprocessableEntity) + renderTablosResponse(w, r, "/tablos", tablos, tablosPageViewModel(user, state, tablos, name, r.FormValue("color"), "Le nom du projet est requis"), http.StatusUnprocessableEntity) return } @@ -263,7 +240,7 @@ func (h *AuthHandler) PostTabloUpdate() http.HandlerFunc { http.Error(w, "failed to list tablos", http.StatusInternalServerError) return } - renderTablosResponse(w, r, "/tablos", tablosPageViewModel(user, state, tablos, name, r.FormValue("color"), tabloColorValidationMessage), http.StatusUnprocessableEntity) + renderTablosResponse(w, r, "/tablos", tablos, tablosPageViewModel(user, state, tablos, name, r.FormValue("color"), tabloColorValidationMessage), http.StatusUnprocessableEntity) return } @@ -289,7 +266,7 @@ func (h *AuthHandler) PostTabloUpdate() http.HandlerFunc { return } - renderTablosResponse(w, r, "/tablos", tablosPageViewModel(user, state, tablos, "", "", ""), http.StatusOK) + renderTablosResponse(w, r, "/tablos", tablos, tablosPageViewModel(user, state, tablos, "", "", ""), http.StatusOK) } } @@ -323,7 +300,7 @@ func (h *AuthHandler) DeleteTablo() http.HandlerFunc { return } - renderTablosResponse(w, r, "/tablos", tablosPageViewModel(user, state, tablos, "", "", ""), http.StatusOK) + renderTablosResponse(w, r, "/tablos", tablos, tablosPageViewModel(user, state, tablos, "", "", ""), http.StatusOK) } } @@ -341,7 +318,7 @@ func (h *AuthHandler) renderTablosPage(w http.ResponseWriter, r *http.Request) { return } - renderTablosResponse(w, r, "/tablos", tablosPageViewModel(user, state, tablos, "", "", ""), http.StatusOK) + renderTablosResponse(w, r, "/tablos", tablos, tablosPageViewModel(user, state, tablos, "", "", ""), http.StatusOK) } func tablosPageViewModel(user PublicUser, state TablosPageState, tablos []TabloRecord, formName string, formColor string, errorMessage string) views.TablosPageViewModel { @@ -374,16 +351,16 @@ func findTabloByID(tablos []TabloRecord, targetID uuid.UUID) (TabloRecord, bool) return TabloRecord{}, false } -func renderTablosResponse(w http.ResponseWriter, r *http.Request, activePath string, vm views.TablosPageViewModel, statusCode int) { +func renderTablosResponse(w http.ResponseWriter, r *http.Request, activePath string, tablos []TabloRecord, vm views.TablosPageViewModel, statusCode int) { w.Header().Set("Content-Type", "text/html; charset=utf-8") w.WriteHeader(statusCode) var err error content := views.TablosPageContent(vm) if isHXRequest(r) { - err = views.DashboardContentSwapWithMainClass(activePath, "flex-1 overflow-auto", content).Render(r.Context(), w) + err = views.DashboardContentSwapWithMainClass(activePath, tablos, "flex-1 overflow-auto", content).Render(r.Context(), w) } else { - err = views.DashboardPageWithMainClass(activePath, "flex-1 overflow-auto", content).Render(r.Context(), w) + err = views.DashboardPageWithMainClass(activePath, tablos, "flex-1 overflow-auto", content).Render(r.Context(), w) } if err != nil { http.Error(w, "failed to render tablos page", http.StatusInternalServerError) @@ -538,52 +515,8 @@ func tabloStatusPresentation(status TabloStatus) (string, string, int, string) { } func tabloIconPresentation(color string) (string, string, string, string) { - r, g, b, ok := parseHexColor(color) - if !ok { - fallback := tabloIconPalette[0] - return fallback.icon, fallback.bg, fallback.fg, fallback.accent - } - - best := tabloIconPalette[0] - bestDistance := colorDistanceSquared(r, g, b, best.r, best.g, best.b) - for _, entry := range tabloIconPalette[1:] { - distance := colorDistanceSquared(r, g, b, entry.r, entry.g, entry.b) - if distance < bestDistance { - best = entry - bestDistance = distance - } - } - - return best.icon, best.bg, best.fg, best.accent -} - -func parseHexColor(color string) (int, int, int, bool) { - trimmed := strings.TrimSpace(color) - if len(trimmed) != 7 || trimmed[0] != '#' { - return 0, 0, 0, false - } - - r, err := strconv.ParseInt(trimmed[1:3], 16, 0) - if err != nil { - return 0, 0, 0, false - } - g, err := strconv.ParseInt(trimmed[3:5], 16, 0) - if err != nil { - return 0, 0, 0, false - } - b, err := strconv.ParseInt(trimmed[5:7], 16, 0) - if err != nil { - return 0, 0, 0, false - } - - return int(r), int(g), int(b), true -} - -func colorDistanceSquared(r1 int, g1 int, b1 int, r2 int, g2 int, b2 int) int { - dr := r1 - r2 - dg := g1 - g2 - db := b1 - b2 - return dr*dr + dg*dg + db*db + presentation := tabloicons.ForColor(color) + return presentation.Icon, presentation.Bg, presentation.Fg, presentation.Accent } func formatFrenchDate(value time.Time) string { diff --git a/go-backend/internal/web/tabloicons/presentation.go b/go-backend/internal/web/tabloicons/presentation.go new file mode 100644 index 0000000..90871f8 --- /dev/null +++ b/go-backend/internal/web/tabloicons/presentation.go @@ -0,0 +1,81 @@ +package tabloicons + +import ( + "strconv" + "strings" +) + +type Presentation struct { + Icon string + Bg string + Fg string + Accent string +} + +type paletteEntry struct { + Presentation + r int + g int + b int +} + +var palette = []paletteEntry{ + {Presentation: Presentation{Icon: "bolt", Bg: "bg-blue-500", Fg: "text-white", Accent: "blue"}, r: 59, g: 130, b: 246}, + {Presentation: Presentation{Icon: "leaf", Bg: "bg-green-500", Fg: "text-white", Accent: "green"}, r: 34, g: 197, b: 94}, + {Presentation: Presentation{Icon: "gem", Bg: "bg-purple-500", Fg: "text-white", Accent: "purple"}, r: 168, g: 85, b: 247}, + {Presentation: Presentation{Icon: "flame", Bg: "bg-red-500", Fg: "text-white", Accent: "red"}, r: 239, g: 68, b: 68}, + {Presentation: Presentation{Icon: "star", Bg: "bg-yellow-500", Fg: "text-gray-700", Accent: "yellow"}, r: 234, g: 179, b: 8}, + {Presentation: Presentation{Icon: "compass", Bg: "bg-indigo-500", Fg: "text-white", Accent: "indigo"}, r: 99, g: 102, b: 241}, + {Presentation: Presentation{Icon: "heart", Bg: "bg-pink-500", Fg: "text-white", Accent: "pink"}, r: 236, g: 72, b: 153}, + {Presentation: Presentation{Icon: "waves", Bg: "bg-teal-500", Fg: "text-white", Accent: "teal"}, r: 20, g: 184, b: 166}, + {Presentation: Presentation{Icon: "sun", Bg: "bg-orange-500", Fg: "text-white", Accent: "orange"}, r: 249, g: 115, b: 22}, + {Presentation: Presentation{Icon: "sparkles", Bg: "bg-cyan-500", Fg: "text-gray-700", Accent: "cyan"}, r: 6, g: 182, b: 212}, +} + +func ForColor(color string) Presentation { + r, g, b, ok := parseHexColor(color) + if !ok { + return palette[0].Presentation + } + + best := palette[0] + bestDistance := colorDistanceSquared(r, g, b, best.r, best.g, best.b) + for _, entry := range palette[1:] { + distance := colorDistanceSquared(r, g, b, entry.r, entry.g, entry.b) + if distance < bestDistance { + best = entry + bestDistance = distance + } + } + + return best.Presentation +} + +func parseHexColor(color string) (int, int, int, bool) { + trimmed := strings.TrimSpace(color) + if len(trimmed) != 7 || trimmed[0] != '#' { + return 0, 0, 0, false + } + + r, err := strconv.ParseInt(trimmed[1:3], 16, 0) + if err != nil { + return 0, 0, 0, false + } + g, err := strconv.ParseInt(trimmed[3:5], 16, 0) + if err != nil { + return 0, 0, 0, false + } + b, err := strconv.ParseInt(trimmed[5:7], 16, 0) + if err != nil { + return 0, 0, 0, false + } + + return int(r), int(g), int(b), true +} + +func colorDistanceSquared(r1 int, g1 int, b1 int, r2 int, g2 int, b2 int) int { + dr := r1 - r2 + dg := g1 - g2 + db := b1 - b2 + return dr*dr + dg*dg + db*db +} diff --git a/go-backend/internal/web/ui/app.css b/go-backend/internal/web/ui/app.css index 026863b..9056499 100644 --- a/go-backend/internal/web/ui/app.css +++ b/go-backend/internal/web/ui/app.css @@ -1004,6 +1004,11 @@ td.text-right .borderless-icon-button.ui-icon-button-ghost.ui-icon-button-danger width: 3rem; } +.project-avatar > svg { + height: 1.25rem; + width: 1.25rem; +} + .project-list-icon { background: var(--project-color, var(--color-project-fallback)); color: var(--color-text-inverse); diff --git a/go-backend/internal/web/views/dashboard_components.templ b/go-backend/internal/web/views/dashboard_components.templ index 2734eae..b93466c 100644 --- a/go-backend/internal/web/views/dashboard_components.templ +++ b/go-backend/internal/web/views/dashboard_components.templ @@ -1,13 +1,14 @@ package views import "strconv" +import tablomodel "xtablo-backend/internal/tablos" import "xtablo-backend/internal/web/ui" -templ DashboardPage(activePath string, content templ.Component) { - @DashboardPageWithMainClass(activePath, "dashboard-main flex-1 overflow-auto", content) +templ DashboardPage(activePath string, tablos []tablomodel.Record, content templ.Component) { + @DashboardPageWithMainClass(activePath, tablos, "dashboard-main flex-1 overflow-auto", content) } -templ DashboardPageWithMainClass(activePath string, mainClass string, content templ.Component) { +templ DashboardPageWithMainClass(activePath string, tablos []tablomodel.Record, mainClass string, content templ.Component) {
@@ -20,7 +21,7 @@ templ DashboardPageWithMainClass(activePath string, mainClass string, content te") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var22 string - templ_7745c5c3_Var22, templ_7745c5c3_Err = templ.JoinStringErrs(description) + var templ_7745c5c3_Var24 string + templ_7745c5c3_Var24, templ_7745c5c3_Err = templ.JoinStringErrs(description) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views/dashboard_components.templ`, Line: 163, Col: 19} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views/dashboard_components.templ`, Line: 183, Col: 19} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var22)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var24)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "
Cette page n'existe pas ou n'est plus disponible.
")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, " ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
- var templ_7745c5c3_Var26 string
- templ_7745c5c3_Var26, templ_7745c5c3_Err = templ.JoinStringErrs(dashboardTodayLabel())
+ var templ_7745c5c3_Var28 string
+ templ_7745c5c3_Var28, templ_7745c5c3_Err = templ.JoinStringErrs(dashboardTodayLabel())
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views/dashboard_components.templ`, Line: 191, Col: 50}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views/dashboard_components.templ`, Line: 211, Col: 50}
}
- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var26))
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var28))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "Bonjour, ")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "
Bonjour, ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
- var templ_7745c5c3_Var27 string
- templ_7745c5c3_Var27, templ_7745c5c3_Err = templ.JoinStringErrs(dashboardGreetingName(displayName))
+ var templ_7745c5c3_Var29 string
+ templ_7745c5c3_Var29, templ_7745c5c3_Err = templ.JoinStringErrs(dashboardGreetingName(displayName))
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views/dashboard_components.templ`, Line: 193, Col: 84}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views/dashboard_components.templ`, Line: 213, Col: 84}
}
- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var27))
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var29))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "!
Mes Projets
Mes Projets
") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - var templ_7745c5c3_Var43 string - templ_7745c5c3_Var43, templ_7745c5c3_Err = templ.JoinStringErrs(task.Title) - if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/views/dashboard_components.templ`, Line: 338, Col: 18} - } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var43)) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 50, "