xtablo-source/deprecated/internal/web/views/tablos.templ
Arthur Belleville 5d0c201e86
Some checks failed
backend-ci / Backend tests (pull_request) Failing after 53s
backend-ci / Backend tests (push) Failing after 1s
Some work
2026-05-23 17:26:01 +02:00

467 lines
14 KiB
Text

package views
import "xtablo-backend/internal/web/ui"
templ TablosPageContent(vm TablosPageViewModel) {
<div class="px-4 pt-8 pb-6" data-project-filter-root>
<div class="flex flex-col md:flex-row md:items-center md:justify-between mb-8 gap-4">
<h1 class="text-2xl font-bold text-gray-900 dark:text-gray-100">Mes Projets</h1>
@ui.Button(ui.ButtonProps{
Label: "Nouveau projet",
Variant: ui.ButtonVariantDefault,
Size: ui.SizeMD,
Type: "button",
Icon: "plus",
Attrs: templ.Attributes{
"hx-get": vm.CreateModalHref(),
"hx-target": "#app-main-content",
"hx-swap": "outerHTML",
"hx-push-url": "true",
},
})
</div>
<div class="flex items-center gap-6 mb-6 border-b border-[#EAECF0] dark:border-gray-700">
<a
href={ templ.SafeURL(vm.ViewHref("grid")) }
hx-get={ vm.ViewHref("grid") }
hx-target="#app-main-content"
hx-swap="outerHTML"
hx-push-url="true"
class={ gridToggleClass(vm.IsGridView()) }
>
<span class="w-5 h-5">
@ActionIcon("grid3x3")
</span>
<span class="font-medium">Vue en grille</span>
</a>
<a
href={ templ.SafeURL(vm.ViewHref("list")) }
hx-get={ vm.ViewHref("list") }
hx-target="#app-main-content"
hx-swap="outerHTML"
hx-push-url="true"
class={ listToggleClass(vm.IsGridView()) }
>
<span class="w-5 h-5">
@ActionIcon("list")
</span>
<span class="font-medium">Vue en liste</span>
</a>
</div>
<div class="flex flex-col md:flex-row gap-4 mb-6">
<div class="relative md:w-[350px]">
<span class="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400 w-5 h-5 pointer-events-none">
@ActionIcon("search")
</span>
<input
data-project-filter-input
placeholder="Rechercher..."
autocomplete="off"
class="w-full pl-10 pr-4 py-3 border border-[#EAECF0] dark:border-gray-700 rounded-[8px] focus:outline-none focus:ring-2 focus:ring-purple-500 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 placeholder-gray-400"
type="text"
/>
</div>
<div class="flex items-center gap-2 flex-wrap">
@StatusPill(vm, "all", "Tous")
@StatusPill(vm, "todo", "Pas commencé")
@StatusPill(vm, "in_progress", "En cours")
@StatusPill(vm, "done", "Terminé")
</div>
</div>
if vm.HasTablos() {
if vm.IsGridView() {
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4 sm:gap-6" data-project-filter-results>
for _, tablo := range vm.Tablos {
@TabloGridCard(tablo)
}
</div>
} else {
<div class="bg-white dark:bg-gray-800 rounded-xl border border-[#EAECF0] dark:border-gray-700 overflow-x-auto -mx-4 sm:mx-0" data-project-filter-results>
@ui.Table(ui.TableProps{
Head: TabloListHead(),
Body: TabloListBody(vm.Tablos),
})
</div>
}
<div
data-project-filter-empty
hidden
class="rounded-xl border border-dashed border-[#EAECF0] bg-white/80 px-6 py-10 text-center text-sm text-gray-500 dark:border-gray-700 dark:bg-gray-800/60 dark:text-gray-400"
>
Aucun projet ne correspond à votre recherche.
</div>
@InitProjectFilterScript()
} else {
@ui.EmptyState(ui.EmptyStateProps{
Title: "Aucun projet trouvé",
Description: "Créez votre premier projet",
Icon: ui.UIIcon("grid3x3"),
Action: ui.Button(ui.ButtonProps{
Label: "Nouveau projet",
Variant: ui.ButtonVariantDefault,
Size: ui.SizeMD,
Type: "button",
Icon: "plus",
Attrs: templ.Attributes{
"hx-get": vm.CreateModalHref(),
"hx-target": "#app-main-content",
"hx-swap": "outerHTML",
"hx-push-url": "true",
},
}),
})
}
if vm.HasModal() {
if vm.IsCreateModal() {
@CreateTabloModal(vm)
} else if vm.IsEditModal() {
@EditTabloModal(vm)
}
}
</div>
}
templ StatusPill(vm TablosPageViewModel, status string, label string) {
<a
href={ templ.SafeURL(vm.StatusHref(status)) }
hx-get={ vm.StatusHref(status) }
hx-target="#app-main-content"
hx-swap="outerHTML"
hx-push-url="true"
class={ statusPillClass(vm.Status == status) }
>
if status == "all" {
<span class="w-4 h-4">
@ActionIcon("filter")
</span>
}
{ label }
</a>
}
templ BorderlessDeleteButton(deleteRequestURL string) {
@ui.IconButton(ui.IconButtonProps{
Label: "Supprimer le projet",
Icon: "trash",
Variant: ui.IconButtonVariantDanger,
Tone: ui.IconButtonToneGhost,
Type: "button",
Attrs: templ.Attributes{
"hx-delete": deleteRequestURL,
"hx-target": "#app-main-content",
"hx-swap": "outerHTML",
"hx-confirm": "Supprimer ce projet ?",
},
})
}
templ EditTabloButton(editRequestURL string) {
@ui.IconButton(ui.IconButtonProps{
Label: "Modifier le projet",
Icon: "pencil",
Variant: ui.IconButtonVariantNeutral,
Tone: ui.IconButtonToneGhost,
Type: "button",
Attrs: templ.Attributes{
"hx-get": editRequestURL,
"hx-target": "#app-main-content",
"hx-swap": "outerHTML",
"hx-push-url": "true",
},
})
}
templ TabloGridCard(tablo TabloCardView) {
@TabloGridCardWithAttrs(tablo, nil)
}
templ TabloGridCardWithAttrs(tablo TabloCardView, attrs templ.Attributes) {
<article class="project-card" style={ projectColorVariableStyle(tablo.Color) } data-project-filter-item data-project-name={ tablo.Name } { attrs... }>
<div class="project-card-top">
@ui.Badge(ui.BadgeProps{
Label: tablo.StatusLabel,
Variant: badgeVariantForTone(tablo.StatusTone),
})
<div class="flex items-center gap-3">
@EditTabloButton(tablo.EditRequestURL)
@BorderlessDeleteButton(tablo.DeleteRequestURL)
</div>
</div>
<div class="project-card-title-row">
<div class="project-avatar">
if tablo.IconKind != "" {
@ActionIcon(tablo.IconKind)
} else {
<span>{ tablo.Initial }</span>
}
</div>
<h4>{ tablo.Name }</h4>
</div>
<div class="project-date-row">
@ActionIcon("calendar")
<span>{ tablo.CardDateLabel }</span>
</div>
<div class="project-progress">
<div class="project-progress-label">
<span>Progression:</span>
<strong>{ tablo.ProgressLabel }</strong>
</div>
<div class="project-progress-track">
<div class="project-progress-bar" style={ progressInlineStyle(tablo.Progress) }></div>
</div>
</div>
</article>
}
templ TabloListRow(tablo TabloCardView) {
<tr class="border-t border-[#EAECF0] dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors cursor-pointer" style={ projectColorVariableStyle(tablo.Color) } data-project-filter-item data-project-name={ tablo.Name }>
<td class="px-6 py-4">
<div class="flex items-center gap-3">
<div class="project-list-icon w-8 h-8 rounded-lg flex items-center justify-center shrink-0 overflow-hidden [&>svg]:w-4 [&>svg]:h-4">
@ActionIcon(tablo.IconKind)
</div>
<span class="font-medium text-gray-900 dark:text-gray-100 truncate">{ tablo.Name }</span>
</div>
</td>
<td class="px-6 py-4 whitespace-nowrap">
@ui.Badge(ui.BadgeProps{
Label: tablo.StatusLabel,
Variant: badgeVariantForTone(tablo.StatusTone),
})
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-600 dark:text-gray-400">
<div class="flex items-center gap-1.5 [&>svg]:w-4 [&>svg]:h-4 [&>svg]:shrink-0">
@ActionIcon("calendar")
{ tablo.CreatedAtLabel }
</div>
</td>
<td class="px-6 py-4">
<div class="flex items-center gap-3">
<div class="flex-1 bg-gray-200 dark:bg-gray-700 rounded-full h-2 min-w-[80px]">
<div class="project-progress-bar h-2 rounded-full transition-all" style={ progressInlineStyle(tablo.Progress) }></div>
</div>
<span class="text-sm font-semibold text-gray-900 dark:text-gray-100 w-8 text-right">{ tablo.ProgressLabel }</span>
</div>
</td>
<td class="px-6 py-4 text-right">
<div class="flex items-center justify-end gap-1">
@EditTabloButton(tablo.EditRequestURL)
@BorderlessDeleteButton(tablo.DeleteRequestURL)
</div>
</td>
</tr>
}
templ CreateTabloModal(vm TablosPageViewModel) {
@ui.Modal(ui.ModalProps{
Title: "Nouveau projet",
Body: CreateTabloModalBody(vm),
})
}
templ InitProjectFilterScript() {
<script>
(function () {
function normalizeProjectFilterValue(value) {
return (value || "").toLowerCase().trim();
}
function applyProjectFilter(root) {
if (!root) return;
var input = root.querySelector("[data-project-filter-input]");
var results = root.querySelector("[data-project-filter-results]");
var empty = root.querySelector("[data-project-filter-empty]");
if (!input || !results || !empty) return;
var query = normalizeProjectFilterValue(input.value);
var visibleCount = 0;
root.querySelectorAll("[data-project-filter-item]").forEach(function (item) {
var name = normalizeProjectFilterValue(item.getAttribute("data-project-name") || item.textContent);
var matches = query === "" || name.indexOf(query) !== -1;
item.hidden = !matches;
if (matches) {
item.removeAttribute("hidden");
visibleCount += 1;
} else {
item.setAttribute("hidden", "");
}
});
var hasVisibleResults = visibleCount > 0;
results.hidden = !hasVisibleResults;
empty.hidden = hasVisibleResults;
if (hasVisibleResults) {
results.removeAttribute("hidden");
empty.setAttribute("hidden", "");
} else {
results.setAttribute("hidden", "");
empty.removeAttribute("hidden");
}
}
function handleProjectFilterInput(event) {
var input = event.target.closest("[data-project-filter-input]");
if (!input) return;
applyProjectFilter(input.closest("[data-project-filter-root]"));
}
if (!window.xtabloProjectFilterInitialized) {
document.addEventListener("input", handleProjectFilterInput);
window.xtabloProjectFilterInitialized = true;
}
document.querySelectorAll("[data-project-filter-root]").forEach(applyProjectFilter);
})();
</script>
}
templ TabloListHead() {
<tr class="bg-gray-50 dark:bg-gray-800/80 border-b border-[#EAECF0] dark:border-gray-700">
<th class="px-6 py-3 text-left text-xs font-semibold text-gray-600 dark:text-gray-400 uppercase tracking-wider">Projet</th>
<th class="px-6 py-3 text-left text-xs font-semibold text-gray-600 dark:text-gray-400 uppercase tracking-wider">Statut</th>
<th class="px-6 py-3 text-left text-xs font-semibold text-gray-600 dark:text-gray-400 uppercase tracking-wider">Créé le</th>
<th class="px-6 py-3 text-left text-xs font-semibold text-gray-600 dark:text-gray-400 uppercase tracking-wider">Progression</th>
<th class="px-6 py-3 w-12"></th>
</tr>
}
templ TabloListBody(tablos []TabloCardView) {
for _, tablo := range tablos {
@TabloListRow(tablo)
}
}
templ CreateTabloModalBody(vm TablosPageViewModel) {
<form
hx-post="/tablos"
hx-target="#app-main-content"
hx-swap="outerHTML"
class="flex flex-col gap-4"
>
<input type="hidden" name="view" value={ vm.View }/>
<input type="hidden" name="status" value={ vm.Status }/>
<input type="hidden" name="modal" value="create"/>
if vm.ErrorMessage != "" {
<div class="mb-1 rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700">{ vm.ErrorMessage }</div>
}
@ui.FormField(ui.FormFieldProps{
Label: "Nom du projet",
For: "tablo-name",
Field: ui.Input(ui.InputProps{
ID: "tablo-name",
Name: "name",
Value: vm.FormName,
Placeholder: "Nom du projet",
Type: "text",
}),
})
@ui.FormField(ui.FormFieldProps{
Label: "Couleur",
For: "tablo-color",
Field: ui.Input(ui.InputProps{
ID: "tablo-color",
Name: "color",
Value: vm.FormColor,
Placeholder: "#3B82F6",
Type: "text",
Attrs: templ.Attributes{
"pattern": "^#[0-9A-Fa-f]{6}$",
"autocomplete": "off",
},
}),
})
<div class="flex items-center justify-end gap-3">
<a
href={ templ.SafeURL(vm.CloseModalHref()) }
hx-get={ vm.CloseModalHref() }
hx-target="#app-main-content"
hx-swap="outerHTML"
hx-push-url="true"
class="ui-button ui-button-solid ui-button-neutral ui-button-md"
>
Annuler
</a>
@ui.Button(ui.ButtonProps{
Label: "Créer le projet",
Variant: ui.ButtonVariantDefault,
Size: ui.SizeMD,
Type: "submit",
})
</div>
</form>
}
templ EditTabloModal(vm TablosPageViewModel) {
@ui.Modal(ui.ModalProps{
Title: "Modifier le projet",
Body: EditTabloModalBody(vm),
})
}
templ EditTabloColorField(vm TablosPageViewModel) {
<div class="flex items-center gap-3">
@ui.Input(ui.InputProps{
ID: "edit-tablo-color",
Name: "color",
Value: vm.FormColor,
Placeholder: "#3B82F6",
Type: "text",
Attrs: templ.Attributes{
"pattern": "^#[0-9A-Fa-f]{6}$",
"autocomplete": "off",
"oninput": "document.getElementById('edit-tablo-color-picker').value=this.value",
},
})
<input
id="edit-tablo-color-picker"
type="color"
value={ vm.FormColor }
class="ui-input tablo-color-picker"
oninput="document.getElementById('edit-tablo-color').value=this.value"
/>
</div>
}
templ EditTabloModalBody(vm TablosPageViewModel) {
<form
hx-post={ vm.EditSubmitHref() }
hx-target="#app-main-content"
hx-swap="outerHTML"
class="flex flex-col gap-4"
>
<input type="hidden" name="view" value={ vm.View }/>
<input type="hidden" name="status" value={ vm.Status }/>
if vm.ErrorMessage != "" {
<div class="mb-1 rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700">{ vm.ErrorMessage }</div>
}
@ui.FormField(ui.FormFieldProps{
Label: "Nom du projet",
For: "edit-tablo-name",
Field: ui.Input(ui.InputProps{
ID: "edit-tablo-name",
Name: "name",
Value: vm.FormName,
Placeholder: "Nom du projet",
Type: "text",
}),
})
@ui.FormField(ui.FormFieldProps{
Label: "Couleur",
For: "edit-tablo-color",
Field: EditTabloColorField(vm),
Hint: "Utilisez le champ hex ou le sélecteur pour choisir une couleur au format #RRGGBB.",
})
<div class="flex items-center justify-end gap-3">
<a
href={ templ.SafeURL(vm.CloseModalHref()) }
hx-get={ vm.CloseModalHref() }
hx-target="#app-main-content"
hx-swap="outerHTML"
hx-push-url="true"
class="ui-button ui-button-solid ui-button-neutral ui-button-md"
>
Annuler
</a>
@ui.Button(ui.ButtonProps{
Label: "Enregistrer",
Variant: ui.ButtonVariantDefault,
Size: ui.SizeMD,
Type: "submit",
})
</div>
</form>
}