feat(19-02): rebuild TabloProjectCard with dual card+row structure

- Rewrote TabloProjectCard with outer article.tablo-card-wrapper (display:contents)
- Added .project-card child: status badge top-left, delete button top-right
- Avatar circle now shows initial letter from card.Tablo.Title
- Added calendar icon + formatted date in .project-date-row
- Added .project-card-progress-row with "Progression: X%" label and .project-card-progress-bar
- Added .tablo-list-row sibling (hidden by default) for list view toggle
- Added strconv and strings imports for Itoa and title-casing Status
This commit is contained in:
Arthur Belleville 2026-05-17 16:30:32 +02:00
parent ae1798062e
commit 4b254e9527
No known key found for this signature in database

View file

@ -4,6 +4,8 @@ import (
"backend/internal/auth"
"backend/internal/db/sqlc"
"backend/internal/web/ui"
"strconv"
"strings"
)
// TablosDashboard renders the root authenticated dashboard with sidebar AppLayout.
@ -63,27 +65,27 @@ templ TablosEmptyState() {
})
}
// TabloProjectCard renders a single tablo as a project-card in the dashboard grid.
// Follows D-C02 design: colored avatar circle, title zone (with inline-edit support),
// creation date, and edit/delete icon buttons.
// TabloProjectCard renders a single tablo as a dual-element card+row wrapper.
// The outer article.tablo-card-wrapper contains both a .project-card (grid view)
// and a .tablo-list-row (list view, hidden by default).
// Matches production design: status badge top-left, delete button top-right,
// colored avatar with initial letter, title, date, and progress bar.
// Uses display:contents on the wrapper so it is transparent to grid/flex layout.
// Guards color rendering against null pgtype.Text values (Pitfall 6).
// Uses .Time accessor on pgtype.Timestamptz (Pitfall 6).
templ TabloProjectCard(card TabloCardView, csrfToken string) {
<article id={ "tablo-" + card.Tablo.ID.String() } class="project-card">
<div class="project-card-top">
<div class="flex items-center gap-2">
@ui.IconButton(ui.IconButtonProps{
Label: "Edit title",
Icon: "pencil",
Variant: ui.IconButtonVariantNeutral,
Tone: ui.IconButtonToneGhost,
Type: "button",
Attrs: templ.Attributes{
"hx-get": "/tablos/" + card.Tablo.ID.String() + "/edit-title",
"hx-target": "closest .tablo-title-zone",
"hx-swap": "outerHTML",
},
})
<article id={ "tablo-" + card.Tablo.ID.String() } class="tablo-card-wrapper">
<!-- Card view (default: visible in grid layout) -->
<div class="project-card">
<div class="project-card-top">
<!-- Status badge top-left -->
if len(card.Tablo.Status) > 0 {
@ui.Badge(ui.BadgeProps{
Label: strings.ToUpper(card.Tablo.Status[:1]) + card.Tablo.Status[1:],
Variant: ui.BadgeVariantPrimary,
})
}
<!-- Delete button top-right -->
<div class="tablo-delete-zone">
@ui.IconButton(ui.IconButtonProps{
Label: "Delete tablo",
@ -99,19 +101,60 @@ templ TabloProjectCard(card TabloCardView, csrfToken string) {
})
</div>
</div>
</div>
<div class="project-card-title-row">
if card.Tablo.Color.Valid && card.Tablo.Color.String != "" {
<span class="project-avatar" style={ "background-color: " + card.Tablo.Color.String }></span>
} else {
<span class="project-avatar"></span>
}
<div class="tablo-title-zone">
<h4>{ card.Tablo.Title }</h4>
<div class="project-card-title-row">
if card.Tablo.Color.Valid && card.Tablo.Color.String != "" {
<span class="project-avatar" style={ "background-color: " + card.Tablo.Color.String }>
if len(card.Tablo.Title) > 0 {
{ string([]rune(card.Tablo.Title)[0:1]) }
}
</span>
} else {
<span class="project-avatar">
if len(card.Tablo.Title) > 0 {
{ string([]rune(card.Tablo.Title)[0:1]) }
}
</span>
}
<div class="tablo-title-zone">
<h4>{ card.Tablo.Title }</h4>
</div>
</div>
<div class="project-date-row">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" style="width:1rem;height:1rem;flex-shrink:0"><rect width="18" height="18" x="3" y="4" rx="2" ry="2"></rect><line x1="16" x2="16" y1="2" y2="6"></line><line x1="8" x2="8" y1="2" y2="6"></line><line x1="3" x2="21" y1="10" y2="10"></line></svg>
{ card.Tablo.CreatedAt.Time.Format("Jan 2, 2006") }
</div>
<div class="project-card-progress-row">
<span class="project-card-progress-label">Progression: { strconv.Itoa(card.Progress) }%</span>
<div class="project-progress-track">
<div class="project-card-progress-bar" style={ "width: " + strconv.Itoa(card.Progress) + "%" }></div>
</div>
</div>
</div>
<div class="project-date-row">
{ card.Tablo.CreatedAt.Time.Format("Jan 2, 2006") }
<!-- List row (hidden by default, shown when data-view="list") -->
<div class="tablo-list-row">
if len(card.Tablo.Status) > 0 {
@ui.Badge(ui.BadgeProps{
Label: strings.ToUpper(card.Tablo.Status[:1]) + card.Tablo.Status[1:],
Variant: ui.BadgeVariantPrimary,
})
}
<span class="tablo-list-row-title">{ card.Tablo.Title }</span>
<span class="tablo-list-row-meta">{ card.Tablo.CreatedAt.Time.Format("Jan 2, 2006") }</span>
<span class="tablo-list-row-meta">{ strconv.Itoa(card.Progress) }%</span>
<div class="tablo-delete-zone">
@ui.IconButton(ui.IconButtonProps{
Label: "Delete tablo",
Icon: "trash",
Variant: ui.IconButtonVariantDanger,
Tone: ui.IconButtonToneGhost,
Type: "button",
Attrs: templ.Attributes{
"hx-get": "/tablos/" + card.Tablo.ID.String() + "/delete-confirm",
"hx-target": "closest .tablo-delete-zone",
"hx-swap": "outerHTML",
},
})
</div>
</div>
</article>
}