467 lines
14 KiB
Text
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>
|
|
}
|