Files
nidus/internal/web/templates/files.templ
T
arnefandCopilot 3fa0297715 Add web UI for WebDAV files and CardDAV contacts
Files browser (web/files):
- Alphabetical listing grouped folders-then-files
- Upload via button or drag-and-drop, including whole folders
- Folder creation
- Fixed layout for long filenames without spaces (table-fixed + break-all)

Contacts (web/contacts):
- Full CRUD for CardDAV contacts (create/edit/delete)
- VCF import (multi-card files) and export (single/all)
- Photo upload with preview, birthday field
- TYPE labels (private/business) for phone, email, address
- Repeatable multi-input rows for phones/emails/addresses instead of textareas
- Sanitizes a known malformed TYPE parameter pattern from some vCard
  exporters (e.g. Nextcloud Contacts) that otherwise caused phone numbers
  to be silently dropped on import

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-08-20 15:06:39 +02:00

99 lines
3.6 KiB
Templ

package templates
// Breadcrumb is one segment of the current directory path shown above the
// file listing, e.g. "docs" linking to "/web/files/docs/".
type Breadcrumb struct {
Name string
Path string // relative path (no leading slash) used to build the link
}
// FileEntry is a single row (directory or file) in the file browser.
type FileEntry struct {
Name string
IsDir bool
Size string // human-readable, empty for directories
ModTime string
RelPath string // relative path (no leading slash) used to build the link
}
templ FilesPage(username string, breadcrumbs []Breadcrumb, entries []FileEntry, currentPath string) {
@Layout("Files", username) {
<div class="flex items-center justify-between mb-6 gap-4 flex-wrap">
<h1 class="text-2xl font-semibold">Files</h1>
<div class="flex gap-2">
<button id="new-folder-button" type="button"
class="bg-white border border-gray-300 rounded-md px-3 py-1.5 text-sm font-medium text-gray-700 hover:bg-gray-50">
New folder
</button>
<label class="cursor-pointer bg-indigo-600 text-white rounded-md px-3 py-1.5 text-sm font-medium hover:bg-indigo-700">
Upload files
<input id="upload-files-input" type="file" multiple class="hidden"/>
</label>
<label class="cursor-pointer bg-indigo-600 text-white rounded-md px-3 py-1.5 text-sm font-medium hover:bg-indigo-700">
Upload folder
<input id="upload-folder-input" type="file" webkitdirectory multiple class="hidden"/>
</label>
</div>
</div>
<nav class="text-sm text-gray-500 mb-4 flex flex-wrap items-center gap-1">
<a href="/web/files/" class="hover:underline text-indigo-600">home</a>
for _, bc := range breadcrumbs {
<span>/</span>
<a href={ templ.URL("/web/files/" + bc.Path + "/") } class="hover:underline text-indigo-600">{ bc.Name }</a>
}
</nav>
<div
id="file-drop-zone"
data-current-path={ currentPath }
data-upload-url={ "/web/files/" + currentPath }
class="bg-white rounded-lg border-2 border-dashed border-gray-200 p-2 transition-colors"
>
<table class="w-full text-sm table-fixed">
<colgroup>
<col class="w-auto"/>
<col class="w-20"/>
<col class="w-36"/>
</colgroup>
<thead>
<tr class="text-left text-gray-400 border-b border-gray-100">
<th class="py-2 px-3 font-medium">Name</th>
<th class="py-2 px-3 font-medium">Size</th>
<th class="py-2 px-3 font-medium">Modified</th>
</tr>
</thead>
<tbody>
for _, e := range entries {
<tr class="border-b border-gray-50 hover:bg-gray-50">
<td class="py-2 px-3 break-all">
if e.IsDir {
<a href={ templ.URL("/web/files/" + e.RelPath + "/") } class="flex items-center gap-2 text-indigo-600 hover:underline">
<span aria-hidden="true">📁</span>{ e.Name }
</a>
} else {
<a href={ templ.URL("/web/files/" + e.RelPath) } download class="flex items-center gap-2 text-gray-700 hover:underline">
<span aria-hidden="true">📄</span>{ e.Name }
</a>
}
</td>
<td class="py-2 px-3 text-gray-500 whitespace-nowrap">{ e.Size }</td>
<td class="py-2 px-3 text-gray-500 whitespace-nowrap">{ e.ModTime }</td>
</tr>
}
if len(entries) == 0 {
<tr>
<td colspan="3" class="py-6 px-3 text-center text-gray-400">
This folder is empty drag &amp; drop files or folders here, or use the upload buttons above.
</td>
</tr>
}
</tbody>
</table>
</div>
<p id="upload-status" class="mt-3 text-sm text-gray-500"></p>
<script type="module" src="/web/static/files.js"></script>
}
}