package templates import "fmt" // AddressBookSummary is one of the user's own address books, listed on // the contacts home page. type AddressBookSummary struct { Name string Count int } // ContactSummary is a single contact row shown in a book's contact list. type ContactSummary struct { ID string FullName string Organization string Phone string Email string PhotoDataURL string // "data:image/...;base64,..." if the contact has a photo, else "" } // LabeledValue is one TEL/EMAIL entry with its TYPE parameter ("home", // "work", or "" for unspecified/other). type LabeledValue struct { Type string Value string } // AddressValue is one ADR entry with its TYPE parameter; the full address // is captured as a single free-form string. type AddressValue struct { Type string Value string } // ContactFormData pre-fills the create/edit contact form. Phones, Emails, // and Addresses always contain at least one (possibly blank) entry so the // form always renders at least one input row per section. type ContactFormData struct { Book string ID string // empty when creating a new contact Forename string Surname string Organization string Birthday string // "YYYY-MM-DD", empty if not set Note string PhotoDataURL string // existing photo preview, "" if none Phones []LabeledValue Emails []LabeledValue Addresses []AddressValue } templ ContactsHome(username string, books []AddressBookSummary) { @Layout("Contacts", username) {

Contacts

if len(books) == 0 {

You don't have any address books yet — create one from the dashboard first.

} else { } } } // avatar renders a contact's photo if present, otherwise a generic // initial-less placeholder circle, at a given Tailwind size class (e.g. // "w-8 h-8" for list rows, "w-24 h-24" for the form preview). templ avatar(photoDataURL, sizeClass string) { if photoDataURL != "" { } else { } } templ ContactsList(username, book string, contacts []ContactSummary) { @Layout("Contacts", username) {
← Address books

{ book }

New contact Export all (.vcf)
for _, c := range contacts { } if len(contacts) == 0 { }
Name
@avatar(c.PhotoDataURL, "w-8 h-8") { c.FullName } Export
No contacts yet — add one above.
} } // typeSelect renders the TYPE dropdown shared by email/address rows. templ typeSelect(name, selected string) { } // phoneTypeSelect renders the phone TYPE dropdown. An unspecified number is // shown as "Mobile" (the vCard "cell" type), matching how mobile clients // label a contact's main number. templ phoneTypeSelect(name, selected string) { } templ phoneRow(p LabeledValue) {
@phoneTypeSelect("phone_type", p.Type)
} templ emailRow(p LabeledValue) {
@typeSelect("email_type", p.Type)
} templ addressRow(a AddressValue) {
@typeSelect("address_type", a.Type)
} templ ContactForm(username string, data ContactFormData, errMsg string) { @Layout("Contacts", username) { ← { data.Book }

if data.ID == "" { New contact } else { Edit contact }

if errMsg != "" {

{ errMsg }

}
@avatar(data.PhotoDataURL, "w-20 h-20")
if data.PhotoDataURL != "" { }
for _, p := range data.Phones { @phoneRow(p) }
for _, e := range data.Emails { @emailRow(e) }
for _, a := range data.Addresses { @addressRow(a) }
Cancel
} } func contactFormAction(data ContactFormData) templ.SafeURL { if data.ID == "" { return templ.URL("/web/contacts/" + data.Book + "/new") } return templ.URL("/web/contacts/" + data.Book + "/" + data.ID + "/edit") }