Files
nidus/internal/web/templates/dashboard.templ
T
arnefandCopilot 37399d1ceb Stack dashboard form fields on mobile instead of breaking layout
The new-calendar/new-address-book creation forms and the per-resource
share form used flex-row layouts that overflowed on narrow viewports.
Switch to flex-col on mobile, flex-row from sm: up, with inputs/buttons
going full-width below the sm breakpoint.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-08-19 12:57:05 +02:00

198 lines
6.4 KiB
Templ

package templates
// ShareRow is a single share grant shown in the UI, for either a calendar
// or an address book (Kind distinguishes them for form targets).
type ShareRow struct {
ResourceName string
SharedWith string
Permission string // "read" or "write"
}
// ResourceCard describes one of the user's own calendars/address books
// plus who it's currently shared with.
type ResourceCard struct {
Kind string // "calendar" or "addressbook"
Name string
Shares []ShareRow
}
// SharedWithMeItem describes a resource another user has shared with the
// current user.
type SharedWithMeItem struct {
Kind string
Owner string
Name string
Permission string
}
templ Dashboard(username string, resources []ResourceCard, sharedWithMe []SharedWithMeItem) {
@Layout("Dashboard", username) {
<h1 class="text-2xl font-semibold mb-6">Your calendars &amp; address books</h1>
<div class="flex flex-col sm:flex-row gap-4 mb-6">
<form
class="flex flex-col sm:flex-row sm:items-end gap-2 bg-white rounded-lg border border-gray-200 p-4"
hx-post="/web/resources/calendar"
hx-target="#resources"
hx-swap="outerHTML"
hx-on::after-request="if(event.detail.successful) this.reset()"
>
<div class="flex-1">
<label class="block text-xs text-gray-500 mb-1">New calendar</label>
<input name="name" type="text" required pattern="[a-zA-Z0-9_-]{1,64}"
placeholder="e.g. work"
class="w-full rounded-md border-gray-300 border px-2 py-1.5 text-sm"/>
</div>
<button type="submit"
class="w-full sm:w-auto bg-indigo-600 text-white rounded-md px-3 py-1.5 text-sm font-medium hover:bg-indigo-700">
Add
</button>
</form>
<form
class="flex flex-col sm:flex-row sm:items-end gap-2 bg-white rounded-lg border border-gray-200 p-4"
hx-post="/web/resources/addressbook"
hx-target="#resources"
hx-swap="outerHTML"
hx-on::after-request="if(event.detail.successful) this.reset()"
>
<div class="flex-1">
<label class="block text-xs text-gray-500 mb-1">New address book</label>
<input name="name" type="text" required pattern="[a-zA-Z0-9_-]{1,64}"
placeholder="e.g. contacts"
class="w-full rounded-md border-gray-300 border px-2 py-1.5 text-sm"/>
</div>
<button type="submit"
class="w-full sm:w-auto bg-indigo-600 text-white rounded-md px-3 py-1.5 text-sm font-medium hover:bg-indigo-700">
Add
</button>
</form>
</div>
@ResourceList(resources)
if len(sharedWithMe) > 0 {
<h2 class="text-xl font-semibold mt-10 mb-4">Shared with you</h2>
<ul class="divide-y divide-gray-200 bg-white rounded-lg border border-gray-200">
for _, item := range sharedWithMe {
<li class="px-4 py-3 flex items-center justify-between text-sm">
<span>
<span class="font-medium">{ item.Owner }</span> / { item.Name }
<span class="text-gray-400">({ item.Kind })</span>
</span>
<span class="text-xs uppercase tracking-wide text-gray-500">{ item.Permission }</span>
</li>
}
</ul>
}
}
}
// ResourceList renders the #resources container. It's re-rendered as a
// whole after a create/delete (which changes the set of cards), whereas a
// share update only swaps a single ResourceCardView.
templ ResourceList(resources []ResourceCard) {
<div id="resources" class="space-y-6">
for _, r := range resources {
@ResourceCardView(r)
}
if len(resources) == 0 {
<p class="text-sm text-gray-400">
You don't have any calendars or address books yet add one above.
</p>
}
</div>
}
templ ResourceCardView(r ResourceCard) {
<div id={ "resource-" + r.Kind + "-" + r.Name } class="bg-white rounded-lg border border-gray-200 p-5">
<div class="flex items-center justify-between mb-3">
<h2 class="font-medium">
{ r.Name }
<span class="text-xs uppercase tracking-wide text-gray-400 ml-2">{ r.Kind }</span>
</h2>
<button
class="text-red-600 hover:underline text-xs"
hx-delete={ resourceEndpoint(r.Kind) }
hx-vals={ resourceVals(r.Name) }
hx-target="#resources"
hx-swap="outerHTML"
hx-confirm={ "Delete " + r.Kind + " " + r.Name + "? This removes all its data and cannot be undone." }
>
Delete
</button>
</div>
<ul class="divide-y divide-gray-100 mb-4">
for _, sh := range r.Shares {
<li class="py-2 flex items-center justify-between text-sm">
<span>{ sh.SharedWith }</span>
<span class="flex items-center gap-3">
<span class="text-xs uppercase tracking-wide text-gray-500">{ sh.Permission }</span>
<button
class="text-red-600 hover:underline text-xs"
hx-delete={ shareEndpoint(r.Kind) }
hx-vals={ shareVals(r.Name, sh.SharedWith) }
hx-target={ "#resource-" + r.Kind + "-" + r.Name }
hx-swap="outerHTML"
hx-confirm={ "Remove access for " + sh.SharedWith + "?" }
>
Remove
</button>
</span>
</li>
}
if len(r.Shares) == 0 {
<li class="py-2 text-sm text-gray-400">Not shared with anyone yet.</li>
}
</ul>
<form
class="flex flex-col sm:flex-row sm:items-end gap-2"
hx-post={ shareEndpoint(r.Kind) }
hx-target={ "#resource-" + r.Kind + "-" + r.Name }
hx-swap="outerHTML"
>
<input type="hidden" name="resource" value={ r.Name }/>
<div class="flex-1">
<label class="block text-xs text-gray-500 mb-1">Username</label>
<input name="shared_with" type="text" required
class="w-full rounded-md border-gray-300 border px-2 py-1.5 text-sm"/>
</div>
<div>
<label class="block text-xs text-gray-500 mb-1">Permission</label>
<select name="permission" class="w-full rounded-md border-gray-300 border px-2 py-1.5 text-sm">
<option value="read">read</option>
<option value="write">write</option>
</select>
</div>
<button type="submit"
class="w-full sm:w-auto bg-indigo-600 text-white rounded-md px-3 py-1.5 text-sm font-medium hover:bg-indigo-700">
Share
</button>
</form>
</div>
}
func shareEndpoint(kind string) string {
if kind == "calendar" {
return "/web/shares/calendar"
}
return "/web/shares/addressbook"
}
func shareVals(resource, sharedWith string) string {
return `{"resource": "` + resource + `", "shared_with": "` + sharedWith + `"}`
}
func resourceEndpoint(kind string) string {
if kind == "calendar" {
return "/web/resources/calendar"
}
return "/web/resources/addressbook"
}
func resourceVals(name string) string {
return `{"name": "` + name + `"}`
}