diff --git a/go-backend/.air.toml b/go-backend/.air.toml index 7d3d4e4..d8c1aee 100644 --- a/go-backend/.air.toml +++ b/go-backend/.air.toml @@ -4,11 +4,11 @@ root = "." tmp_dir = "tmp" [build] - cmd = "go run github.com/a-h/templ/cmd/templ@latest generate && go run github.com/sqlc-dev/sqlc/cmd/sqlc@v1.31.1 generate && go build -o ./tmp/main ." + cmd = "go run ./cmd/buildstyles && pnpm exec tailwindcss -i tailwind.input.css -o static/tailwind.css --cwd . && go run github.com/a-h/templ/cmd/templ@v0.3.1020 generate && go run github.com/sqlc-dev/sqlc/cmd/sqlc@v1.31.1 generate && go build -o ./tmp/main ." entrypoint = ["./tmp/main"] include_ext = ["go", "templ", "sql", "css", "html", "png", "svg", "webmanifest", "json"] exclude_dir = ["tmp", "vendor", ".git", "internal/db/sqlc"] - exclude_regex = ["_templ\\.go$"] + exclude_regex = ["_templ\\.go$", "static/(styles|tailwind)\\.css$"] delay = 200 stop_on_error = true send_interrupt = true diff --git a/go-backend/internal/db/repository.go b/go-backend/internal/db/repository.go index ae50ac0..5c72c3b 100644 --- a/go-backend/internal/db/repository.go +++ b/go-backend/internal/db/repository.go @@ -164,7 +164,6 @@ func (r *PostgresAuthRepository) CreateTablo(ctx context.Context, input tablomod func (r *PostgresAuthRepository) ListTablos(ctx context.Context, input tablomodel.ListInput) ([]tablomodel.Record, error) { params := sqlcdb.ListTablosParams{ OwnerID: input.OwnerID, - Query: nullableText(strings.TrimSpace(input.Query)), Status: nullableStatus(input.Status), } diff --git a/go-backend/internal/tablos/model.go b/go-backend/internal/tablos/model.go index c15bec6..35714d6 100644 --- a/go-backend/internal/tablos/model.go +++ b/go-backend/internal/tablos/model.go @@ -44,6 +44,5 @@ type UpdateInput struct { type ListInput struct { OwnerID uuid.UUID - Query string Status *Status } diff --git a/go-backend/internal/web/dates/french.go b/go-backend/internal/web/dates/french.go new file mode 100644 index 0000000..b8b748f --- /dev/null +++ b/go-backend/internal/web/dates/french.go @@ -0,0 +1,13 @@ +package dates + +import ( + "fmt" + "time" +) + +var frenchMonths = [...]string{"janv.", "fevr.", "mars", "avr.", "mai", "juin", "juil.", "aout", "sept.", "oct.", "nov.", "dec."} + +func FormatFrenchDate(value time.Time) string { + month := frenchMonths[int(value.Month())-1] + return fmt.Sprintf("%02d %s %d", value.Day(), month, value.Year()) +} diff --git a/go-backend/internal/web/handlers/tablos.go b/go-backend/internal/web/handlers/tablos.go index 9dc6884..a1cc9a4 100644 --- a/go-backend/internal/web/handlers/tablos.go +++ b/go-backend/internal/web/handlers/tablos.go @@ -12,6 +12,7 @@ import ( "github.com/google/uuid" tablomodel "xtablo-backend/internal/tablos" + "xtablo-backend/internal/web/dates" "xtablo-backend/internal/web/views" ) @@ -37,16 +38,11 @@ type ListTablosInput = tablomodel.ListInput type TablosPageState struct { View string - Query string Status string ModalKind string EditingTabloID string } -func normalizeTabloQuery(query string) string { - return strings.ToLower(strings.TrimSpace(query)) -} - func parseTablosPageState(values interface { Get(string) string }) TablosPageState { @@ -64,7 +60,6 @@ func parseTablosPageState(values interface { return TablosPageState{ View: view, - Query: strings.TrimSpace(values.Get("q")), Status: status, ModalKind: normalizedModalKind(strings.TrimSpace(values.Get("modal"))), } @@ -329,7 +324,6 @@ func tablosPageViewModel(user PublicUser, state TablosPageState, tablos []TabloR return views.NewTablosPageViewModel( user.DisplayName, state.View, - state.Query, state.Status, state.ModalKind, state.EditingTabloID, @@ -343,7 +337,6 @@ func tablosPageViewModel(user PublicUser, state TablosPageState, tablos []TabloR func listTablosForState(ctx context.Context, repo AuthRepository, ownerID uuid.UUID, state TablosPageState) ([]TabloRecord, error) { return repo.ListTablos(ctx, ListTablosInput{ OwnerID: ownerID, - Query: state.Query, Status: state.statusFilter(), }) } @@ -396,7 +389,6 @@ func (r *InMemoryAuthRepository) ListTablos(_ context.Context, input ListTablosI r.mu.RLock() defer r.mu.RUnlock() - query := normalizeTabloQuery(input.Query) var tablos []TabloRecord for _, tablo := range r.tablos { @@ -409,9 +401,6 @@ func (r *InMemoryAuthRepository) ListTablos(_ context.Context, input ListTablosI if input.Status != nil && tablo.Status != *input.Status { continue } - if query != "" && !strings.Contains(strings.ToLower(tablo.Name), query) { - continue - } tablos = append(tablos, tablo) } @@ -505,9 +494,6 @@ func buildStatefulRequestURL(path string, state TablosPageState) string { values := url.Values{} values.Set("view", state.View) values.Set("status", state.Status) - if strings.TrimSpace(state.Query) != "" { - values.Set("q", strings.TrimSpace(state.Query)) - } encoded := values.Encode() if encoded == "" { return path @@ -538,13 +524,11 @@ func tabloIconPresentation(name string) (string, string, string, string) { } func formatFrenchDate(value time.Time) string { - months := []string{"janv.", "fevr.", "mars", "avr.", "mai", "juin", "juil.", "aout", "sept.", "oct.", "nov.", "dec."} - month := months[int(value.Month())-1] - return fmt.Sprintf("%02d %s %d", value.Day(), month, value.Year()) + return dates.FormatFrenchDate(value) } func formatCardDate(value time.Time) string { - return value.Format("Jan 02, 2006") + return dates.FormatFrenchDate(value) } func projectInitial(name string) string { diff --git a/go-backend/internal/web/handlers/tablos_test.go b/go-backend/internal/web/handlers/tablos_test.go index 03fd88b..928d560 100644 --- a/go-backend/internal/web/handlers/tablos_test.go +++ b/go-backend/internal/web/handlers/tablos_test.go @@ -7,6 +7,7 @@ import ( "net/url" "strings" "testing" + "time" ) func TestInMemoryTablosListExcludesSoftDeletedRows(t *testing.T) { @@ -54,7 +55,7 @@ func TestInMemoryTablosListExcludesSoftDeletedRows(t *testing.T) { } } -func TestInMemoryTablosListFiltersBySearchAndStatus(t *testing.T) { +func TestInMemoryTablosListFiltersByStatus(t *testing.T) { repo := NewInMemoryAuthRepository() user, err := repo.GetAuthUserByEmail(context.Background(), "demo@xtablo.com") if err != nil { @@ -81,15 +82,14 @@ func TestInMemoryTablosListFiltersBySearchAndStatus(t *testing.T) { tablos, err := repo.ListTablos(context.Background(), ListTablosInput{ OwnerID: user.ID, - Query: "delivery", Status: &[]TabloStatus{TabloStatusInProgress}[0], }) if err != nil { - t.Fatalf("list filtered tablos: %v", err) + t.Fatalf("list status-filtered tablos: %v", err) } if len(tablos) != 1 { - t.Fatalf("expected 1 filtered tablo, got %d", len(tablos)) + t.Fatalf("expected 1 status-filtered tablo, got %d", len(tablos)) } if tablos[0].ID != expectedTablo.ID { @@ -177,7 +177,7 @@ func TestGetTablosPageDefaultsToGridAndAllStatus(t *testing.T) { } } -func TestGetTablosPageHonorsSearchAndStatus(t *testing.T) { +func TestGetTablosPageIgnoresSearchQueryParamAndLeavesFilteringToClient(t *testing.T) { repo := NewInMemoryAuthRepository() handler := NewAuthHandler(repo) sessionCookie := loginTestUser(t, handler, "demo@xtablo.com", "xtablo-demo") @@ -192,10 +192,10 @@ func TestGetTablosPageHonorsSearchAndStatus(t *testing.T) { _, err := repo.CreateTablo(context.Background(), CreateTabloInput{ OwnerID: userID, Name: "Alpha Draft", - Status: TabloStatusTodo, + Status: TabloStatusInProgress, }) if err != nil { - t.Fatalf("create todo tablo: %v", err) + t.Fatalf("create first in-progress tablo: %v", err) } _, err = repo.CreateTablo(context.Background(), CreateTabloInput{ @@ -218,11 +218,54 @@ func TestGetTablosPageHonorsSearchAndStatus(t *testing.T) { } body := rec.Body.String() - if !strings.Contains(body, "Beta Delivery") { - t.Fatalf("expected filtered tablo to be visible, got %q", body) + if !strings.Contains(body, "Alpha Draft") { + t.Fatalf("expected non-matching tablo to stay visible for client filtering, got %q", body) } - if strings.Contains(body, "Alpha Draft") { - t.Fatalf("expected non-matching tablo to be filtered out, got %q", body) + if !strings.Contains(body, "Beta Delivery") { + t.Fatalf("expected matching tablo to be visible, got %q", body) + } +} + +func TestGetTablosPageRendersClientSideProjectFilterHooks(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") + } + + _, err := repo.CreateTablo(context.Background(), CreateTabloInput{ + OwnerID: userID, + Name: "Searchable Project", + Status: TabloStatusTodo, + }) + if err != nil { + t.Fatalf("create searchable tablo: %v", err) + } + + pageReq := httptest.NewRequest(http.MethodGet, "/tablos", nil) + pageReq.AddCookie(sessionCookie) + rec := httptest.NewRecorder() + + handler.GetTablosPage().ServeHTTP(rec, pageReq) + + body := rec.Body.String() + for _, want := range []string{ + `data-project-filter-root`, + `data-project-filter-input`, + `data-project-filter-item`, + `window.xtabloProjectFilterInitialized`, + } { + if !strings.Contains(body, want) { + t.Fatalf("expected client-side filter markup %q, got %q", want, body) + } + } + if strings.Contains(body, `name="q"`) { + t.Fatalf("expected search query field to be removed from markup, got %q", body) } } @@ -443,6 +486,13 @@ func TestGetTablosPageGridUsesProjectDateRowMarkup(t *testing.T) { } } +func TestFormatCardDateUsesFrenchMonthNames(t *testing.T) { + got := formatCardDate(time.Date(2026, time.May, 10, 9, 0, 0, 0, time.UTC)) + if got != "10 mai 2026" { + t.Fatalf("expected French card date label, got %q", got) + } +} + func TestGetTablosPageGridUsesProjectCardMarkup(t *testing.T) { repo := NewInMemoryAuthRepository() handler := NewAuthHandler(repo) diff --git a/go-backend/internal/web/ui/button.css b/go-backend/internal/web/ui/button.css index a7e912c..07f045b 100644 --- a/go-backend/internal/web/ui/button.css +++ b/go-backend/internal/web/ui/button.css @@ -1,7 +1,7 @@ .ui-button { align-items: center; border: 0; - border-radius: 0rem; + border-radius: 0.7rem; cursor: pointer; display: inline-flex; font-weight: 600; diff --git a/go-backend/internal/web/views/dashboard_components_test.go b/go-backend/internal/web/views/dashboard_components_test.go index 57bc770..3be1984 100644 --- a/go-backend/internal/web/views/dashboard_components_test.go +++ b/go-backend/internal/web/views/dashboard_components_test.go @@ -33,6 +33,9 @@ func TestOverviewProjectsFromTablosCarriesColorAndEditURL(t *testing.T) { if project.EditRequestURL != "/tablos/11111111-1111-1111-1111-111111111111/edit" { t.Fatalf("expected edit request url to be set, got %q", project.EditRequestURL) } + if project.CardDateLabel != "10 mai 2026" { + t.Fatalf("expected French card date label, got %q", project.CardDateLabel) + } } func TestOverviewProjectsSectionRendersColorAndEditAction(t *testing.T) { diff --git a/go-backend/internal/web/views/home.go b/go-backend/internal/web/views/home.go index de089b9..2818aa3 100644 --- a/go-backend/internal/web/views/home.go +++ b/go-backend/internal/web/views/home.go @@ -7,6 +7,7 @@ import ( "github.com/a-h/templ" tablomodel "xtablo-backend/internal/tablos" + "xtablo-backend/internal/web/dates" ) const overviewProjectsPreviewLimit = 6 @@ -119,7 +120,7 @@ func OverviewProjectsFromTablos(tablos []tablomodel.Record) []TabloCardView { StatusTone: statusTone, Initial: projectInitial(tablo.Name), Accent: overviewProjectAccent(tablo.Name), - CardDateLabel: tablo.CreatedAt.Format("Jan 02, 2006"), + CardDateLabel: dates.FormatFrenchDate(tablo.CreatedAt), Progress: progress, ProgressLabel: progressPercentLabel(progress), DeleteRequestURL: "/tablos/" + tablo.ID.String(), diff --git a/go-backend/internal/web/views/tablos.templ b/go-backend/internal/web/views/tablos.templ index 19eb1a3..589ea28 100644 --- a/go-backend/internal/web/views/tablos.templ +++ b/go-backend/internal/web/views/tablos.templ @@ -3,7 +3,7 @@ package views import "xtablo-backend/internal/web/ui" templ TablosPageContent(vm TablosPageViewModel) { -