added dialog for editing community profile

This commit is contained in:
2025-10-17 22:32:28 -06:00
parent 8e80176022
commit 0edba20d1d
10 changed files with 585 additions and 57 deletions
+98 -24
View File
@@ -23,17 +23,26 @@ import (
"gopkg.in/yaml.v3"
)
// DialogItemChoice holds a dialog item choice (only needed in case of defined dropdowns)
type DialogItemChoice struct {
Id string `yaml:"id"`
Text string `yaml:"text"`
Default bool `yaml:"default,omitempty"`
}
// DialogItem holds the dialog item definition.
type DialogItem struct {
Type string `yaml:"type"`
Name string `yaml:"name"`
Caption string `yaml:"caption,omitempty"`
Subcaption string `yaml:"subcaption,omitempty"`
Required bool `yaml:"required,omitempty"`
Size int `yaml:"size,omitempty"`
MaxLength int `yaml:"maxlength,omitempty"`
Value string `yaml:"value,omitempty"`
Param string `yaml:"param,omitempty"`
Type string `yaml:"type"`
Name string `yaml:"name"`
Caption string `yaml:"caption,omitempty"`
Subcaption string `yaml:"subcaption,omitempty"`
Required bool `yaml:"required,omitempty"`
Disabled bool `yaml:"disabled,omitempty"`
Size int `yaml:"size,omitempty"`
MaxLength int `yaml:"maxlength,omitempty"`
Value string `yaml:"value,omitempty"`
Param string `yaml:"param,omitempty"`
Choices []DialogItemChoice `yaml:"choices,omitempty"`
AuxData any
}
@@ -44,6 +53,7 @@ type Dialog struct {
Options string `yaml:"options,omitempty"`
MenuSelector string `yaml:"menuSelector,omitempty"`
Title string `yaml:"title"`
Subtitle string `yaml:"subtitle,omitempty"`
Action string `yaml:"action"`
Instructions string `yaml:"instructions,omitempty"`
Fields []DialogItem `yaml:"fields"`
@@ -82,6 +92,9 @@ func AmLoadDialog(name string) (*Dialog, error) {
if fld.Type == "date" && fld.Param == "" {
d.Fields[i].Param = "year:-100"
}
if fld.Type == "dropdown" && len(fld.Choices) == 0 {
return nil, fmt.Errorf("dropdown field %s in dialog %s has no choices", fld.Name, name)
}
}
return &d, nil
}
@@ -184,11 +197,29 @@ func (fld *DialogItem) SetVal(p *string) {
}
}
// SetInt sets the value of a field to an integer.
func (fld *DialogItem) SetInt(v int) {
fld.Value = fmt.Sprintf("%d", v)
}
// IsEmpty returns true if the field is empty.
func (fld *DialogItem) IsEmpty() bool {
return len(fld.Value) == 0
}
// SetCommunity alters a dialog's content to reflect the community.
func (d *Dialog) SetCommunity(comm *database.Community) {
d.Title = strings.ReplaceAll(d.Title, "[CNAME]", comm.Name)
d.Subtitle = strings.ReplaceAll(d.Subtitle, "[CNAME]", comm.Name)
d.Action = strings.ReplaceAll(d.Action, "[CID]", comm.Alias)
for i, fld := range d.Fields {
switch fld.Type {
case "userphoto", "communitylogo":
d.Fields[i].Param = strings.ReplaceAll(fld.Param, "[CID]", comm.Alias)
}
}
}
/* Field returns a pointer to a dialog's field, given its name.
* Parameters:
* name - The name of the field to find.
@@ -224,6 +255,46 @@ func (d *Dialog) Render(ctxt AmContext) (string, any, error) {
if d.Fields[i].Value == "" {
d.Fields[i].Value = config.GlobalConfig.Defaults.TimeZone
}
case "dropdown":
defv := ""
for _, ch := range fld.Choices {
if ch.Default {
defv = ch.Id
break
}
}
if d.Fields[i].Value == "" {
d.Fields[i].Value = defv
} else {
ok := false
for _, ch := range fld.Choices {
if d.Fields[i].Value == ch.Id {
ok = true
break
}
}
if !ok {
d.Fields[i].Value = defv
}
}
case "rolelist":
rl := database.AmRoleList(fld.Param)
defv := rl.Default().LevelStr()
if d.Fields[i].Value == "" {
d.Fields[i].Value = defv
} else {
ok := false
for _, r := range rl.Roles() {
if d.Fields[i].Value == r.LevelStr() {
ok = true
break
}
if !ok {
d.Fields[i].Value = defv
}
}
}
// TODO: want to do something like dropdown but not sure what yet
}
}
if d.MenuSelector != "" && d.MenuSelector != "nochange" {
@@ -312,7 +383,7 @@ func (d *Dialog) LoadFromForm(ctxt AmContext) {
}
}
d.Fields[i].AuxData = dvals
case "userphoto":
case "userphoto", "communitylogo":
d.Fields[i].Value = ctxt.FormField(fmt.Sprintf("%s_data", fld.Name))
default:
d.Fields[i].Value = ctxt.FormField(fld.Name)
@@ -468,20 +539,23 @@ func validateDateField(fld *DialogItem) error {
// validators maps the field types to validator functions.
var validators = map[string]validatorFunc{
"ams_id": validateAmsIdField,
"button": nilValidator,
"checkbox": nilValidator,
"countrylist": validateCountryField,
"date": validateDateField,
"email": validateEmailField,
"header": nilValidator,
"hidden": nilValidator,
"integer": validateIntegerField,
"localelist": nilValidator, // TODO
"password": validateTextField,
"text": validateTextField,
"tzlist": nilValidator, // TODO
"userphoto": nilValidator,
"ams_id": validateAmsIdField,
"button": nilValidator,
"checkbox": nilValidator,
"communitylogo": nilValidator,
"countrylist": validateCountryField,
"date": validateDateField,
"dropdown": nilValidator,
"email": validateEmailField,
"header": nilValidator,
"hidden": nilValidator,
"integer": validateIntegerField,
"localelist": nilValidator,
"password": validateTextField,
"rolelist": nilValidator,
"text": validateTextField,
"tzlist": nilValidator,
"userphoto": nilValidator,
}
/* Validate validates the values in the dialog.
+172
View File
@@ -0,0 +1,172 @@
#
# Amsterdam Web Communities System
# Copyright (c) 2025 Erbosoft Metaverse Design Solutions, All Rights Reserved
#
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
#
name: "community.profile"
formName: "comprofform"
menuSelector: "community"
title: "Edit Community Profile:"
subtitle: "[CNAME]"
action: "/comm/[CID]/admin/profile"
fields:
- type: "hidden"
name: "cc"
value: ""
- type: "header"
name: "header1"
caption: "Basic Information"
- type: "text"
name: "name"
caption: "Community Name"
required: true
size: 32
maxLength: 128
- type: "ams_id"
name: "alias"
caption: "Community Alias"
required: true
size: 32
maxlength: 32
- type: "text"
name: "synopsis"
caption: "Synopsis"
size: 32
maxLength: 255
- type: "text"
name: "rules"
caption: "Rules"
size: 32
maxLength: 255
- type: "localelist"
name: "language"
caption: "Primary Language"
required: true
- type: "text"
name: "url"
caption: "Home Page"
subcaption: "(URL)"
size: 32
maxlength: 255
- type: "communitylogo"
name: "logo"
caption: "Community logo"
param: "/TODO/comm/[CID]/admin/logo"
- type: "header"
name: "header2"
caption: "Location"
- type: "text"
name: "company"
caption: "Company"
size: 32
maxlength: 255
- type: "text"
name: "addr1"
caption: "Address"
size: 32
maxlength: 255
- type: "text"
name: "addr2"
caption: "Address"
subcaption: "(line 2)"
size: 32
maxlength: 255
- type: "text"
name: "loc"
caption: "City"
required: true
size: 32
maxlength: 64
- type: "text"
name: "reg"
caption: "State/Province"
required: true
size: 32
maxlength: 64
- type: "text"
name: "pcode"
caption: "Zip/Postal Code"
required: true
size: 32
maxlength: 64
- type: "countrylist"
name: "country"
caption: "Country"
required: true
- type: "header"
name: "header3"
caption: "Security"
- type: "dropdown"
name: "comtype"
caption: "Community type"
required: true
choices:
- id: "0"
text: "Public"
default: true
- id: "1"
text: "Private"
- type: "text"
name: "joinkey"
caption: "Join Key"
subcaption: "(for private communities)"
size: 32
maxLength: 64
- type: "checkbox"
name: "membersonly"
caption: "Allow only members to access this community"
- type: "dropdown"
name: "hidemode"
caption: "Community visibility"
required: true
choices:
- id: "NONE"
text: "Show in both directory and search"
default: true
- id: "DIRECTORY"
text: "Hide in directory, but not in search"
- id: "BOTH"
text: "Hide in both directory and search"
- type: "rolelist"
name: "read_lvl"
caption: "Security level required to read contents"
required: true
param: "Community.Read"
- type: "rolelist"
name: "write_lvl"
caption: "Security level required to update profile"
required: true
param: "Community.Write"
- type: "rolelist"
name: "create_lvl"
caption: "Security level required to create new subobjects"
required: true
param: "Community.Create"
- type: "rolelist"
name: "delete_lvl"
caption: "Security level required to delete community"
required: true
param: "Community.Delete"
- type: "rolelist"
name: "join_lvl"
caption: "Security level required to join community"
required: true
param: "Community.Join"
- type: "header"
name: "header4"
caption: "Conferencing Options"
- type: "checkbox"
name: "pic_in_post"
caption: "Display user pictures next to posts in conferences"
subcaption: "(by default; user can override)"
- type: "button"
name: "update"
caption: "Update"
param: "blue"
- type: "button"
name: "cancel"
caption: "Cancel"
param: "red"
+5 -5
View File
@@ -61,15 +61,15 @@ menudefs:
community. Use with care and review all changes before applying them to the community.
items:
- text: "Community Profile"
link: "/TODO/comm/[CID]/admin/profile"
permission: "Community.ShowAdmin"
link: "/comm/[CID]/admin/profile"
permission: "Community.Write"
- text: "Set Community Category"
link: "/TODO/comm/[CID]/admin/category"
permission: "Community.ShowAdmin"
permission: "Community.Write"
ifdef: "USECAT"
- text: "Set Community Services"
link: "/TODO/comm/[CID]/admin/services"
permission: "Community.ShowAdmin"
permission: "Community.Write"
disabled: true
- text: "Membership Control"
link: "/TODO/comm/[CID]/admin/members"
@@ -82,5 +82,5 @@ menudefs:
permission: "Community.ShowAdmin"
- text: "Delete Community"
link: "/TODO/comm/[CID]/admin/delete"
permission: "Community.Destroy"
permission: "Community.Delete"
hazard: true
+5
View File
@@ -23,6 +23,7 @@ import (
"time"
"git.erbosoft.com/amy/amsterdam/config"
"git.erbosoft.com/amy/amsterdam/database"
"git.erbosoft.com/amy/amsterdam/util"
"github.com/CloudyKit/jet/v6"
"github.com/CloudyKit/jet/v6/loaders/embedfs"
@@ -235,6 +236,10 @@ func SetupTemplates() {
s := a.Get(0).Convert(reflect.TypeFor[string]()).String()
return reflect.ValueOf(AmMenu(s))
})
views.AddGlobalFunc("AmRoleList", func(a jet.Arguments) reflect.Value {
s := a.Get(0).Convert(reflect.TypeFor[string]()).String()
return reflect.ValueOf(database.AmRoleList(s))
})
views.AddGlobalFunc("CapitalizeString", func(a jet.Arguments) reflect.Value {
s := a.Get(0).Convert(reflect.TypeFor[string]()).String()
return reflect.ValueOf(util.CapitalizeString(s))
+85 -25
View File
@@ -10,6 +10,9 @@
<div class="p-4">
<div class="mb-6">
<h1 class="text-blue-800 text-4xl font-bold mb-2">{{ amsterdam_dialog.Title }}</h1>
{{ if amsterdam_dialog.Subtitle != "" }}
<span class="text-blue-800 text-2xl font-bold ml-2">{{ amsterdam_dialog.Subtitle }}</span>
{{ end }}
<hr class="border-2 border-gray-400 w-4/5 mb-4">
</div>
<form name="{{ amsterdam_dialog.FormName }}" method="POST" action="{{ amsterdam_dialog.Action }}" class="max-w-2xl">
@@ -58,26 +61,28 @@
{{ range amsterdam_dialog.Fields }}
{{ if .Type == "text" || .Type == "ams_id" || .Type == "email" }}
<div class="flex items-center">
<label for="{{ .Name }}" class="w-64 text-right pr-4 text-black text-sm">
<label for="{{ .Name }}"
class="w-64 text-right pr-4 {{ if .Disabled }}text-gray-400{{ else }}text-black{{ end }} text-sm">
{{ .Caption }}{{ if .Subcaption != "" }} {{ .Subcaption }}{{ end }}:
{{ if .Required }}<span class="text-red-600">*</span>{{ end }}
</label>
<input type="text" id="{{ .Name }}" name="{{ .Name }}"
{{ if .Size > 0 }}size="{{ .Size }}"{{ end }}
{{ if .MaxLength > 0 }}maxlength="{{ .MaxLength }}"{{ end }}
value="{{ .Value }}"
value="{{ .Value }}" {{ if .Disabled }}disabled{{ end }}
class="flex-1 px-3 py-2 border border-gray-300 rounded focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500" />
</div>
{{ else if .Type == "integer" }}
<div class="flex items-center">
<label for="{{ .Name }}" class="w-64 text-right pr-4 text-black text-sm">
<label for="{{ .Name }}"
class="w-64 text-right pr-4 {{ if .Disabled }}text-gray-400{{ else }}text-black{{ end }} text-sm">
{{ .Caption }}{{ if .Subcaption != "" }} {{ .Subcaption }}{{ end }}:
{{ if .Required }}<span class="text-red-600">*</span>{{ end }}
</label>
<input type="text" id="{{ .Name }}" name="{{ .Name }}"
{{ if .Size > 0 }}size="{{ .Size }}"{{ end }}
{{ if .MaxLength > 0 }}maxlength="{{ .MaxLength }}"{{ end }}
value="{{ .Value }}"
value="{{ .Value }}" {{ if .Disabled }}disabled{{ end }}
class="flex-1 px-3 py-2 border border-gray-300 rounded focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500" />
{{ vr := .ValueRange() }}
{{ if vr.Low != -1 && vr.High != -1 }}
@@ -86,36 +91,69 @@
</div>
{{ else if .Type == "password" }}
<div class="flex items-center">
<label for="{{ .Name }}" class="w-64 text-right pr-4 text-black text-sm">
<label for="{{ .Name }}"
class="w-64 text-right pr-4 {{ if .Disabled }}text-gray-400{{ else }}text-black{{ end }} text-sm">
{{ .Caption }}{{ if .Subcaption != "" }} {{ .Subcaption }}{{ end }}:
{{ if .Required }}<span class="text-red-600">*</span>{{ end }}
</label>
<input type="password" id="{{ .Name }}" name="{{ .Name }}"
{{ if .Size > 0 }}size="{{ .Size }}"{{ end }}
{{ if .MaxLength > 0 }}maxlength="{{ .MaxLength }}"{{ end }}
value="{{ .Value }}"
value="{{ .Value }}" {{ if .Disabled }}disabled{{ end }}
class="flex-1 px-3 py-2 border border-gray-300 rounded focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500" />
</div>
{{ else if .Type == "checkbox" }}
<div class="flex items-center">
<div class="w-24 text-right pr-4">
<input type="checkbox" id="{{ .Name }}" name="{{ .Name }}"
value="Y" {{ if .Value != "" }}checked{{ end }}
value="Y" {{ if .Value != "" }}checked{{ end }} {{ if .Disabled }}disabled{{ end }}
class="w-4 h-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500" />
</div>
<label for="{{ .Name }}" class="flex-1 text-black text-sm">
<label for="{{ .Name }}" class="flex-1 {{ if .Disabled }}text-gray-400{{ else }}text-black{{ end }} text-sm">
{{ .Caption }}{{ if .Subcaption != "" }} {{ .Subcaption }}{{ end }}
{{ if .Required }}<span class="text-red-600">*</span>{{ end }}
</label>
</div>
{{ else if .Type == "countrylist" }}
{{ else if .Type == "dropdown" }}
<div class="flex items-center">
<label for="{{ .Name }}" class="w-64 text-right pr-4 text-black text-sm">
<label for="{{ .Name }}"
class="w-64 text-right pr-4 {{ if .Disabled }}text-gray-400{{ else }}text-black{{ end }} text-sm">
{{ .Caption }}{{ if .Subcaption != "" }} {{ .Subcaption }}{{ end }}
{{ if .Required }}<span class="text-red-600">*</span>{{ end }}
</label>
{{ v := .Value }}
<select id="{{ .Name }}" name="{{ .Name }}" {{ if .Required }}required{{ end }}
<select id="{{ .Name }}" name="{{ .Name }}" {{ if .Required }}required{{ end }} {{ if .Disabled }}disabled{{ end }}
class="flex-1 px-3 py-2 border border-gray-300 rounded focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
{{ range .Choices }}
<option value="{{ .Id }}" {{ if .Id == v }}selected{{ end }}>{{ .Text }}</option>
{{ end }}
</select>
</div>
{{ else if .Type == "rolelist" }}
<div class="flex items-center">
<label for="{{ .Name }}"
class="w-64 text-right pr-4 {{ if .Disabled }}text-gray-400{{ else }}text-black{{ end }} text-sm">
{{ .Caption }}{{ if .Subcaption != "" }} {{ .Subcaption }}{{ end }}
{{ if .Required }}<span class="text-red-600">*</span>{{ end }}
</label>
{{ v := .Value }}
{{ rl := AmRoleList(.Param) }}
<select id="{{ .Name }}" name="{{ .Name }}" {{ if .Required }}required{{ end }} {{ if .Disabled }}disabled{{ end }}
class="flex-1 px-3 py-2 border border-gray-300 rounded focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
{{ range rl.Roles() }}
<option value="{{ .LevelStr() }}" {{ if .LevelStr() == v }}selected{{ end }}>{{ .Name() }}</option>
{{ end }}
</select>
</div>
{{ else if .Type == "countrylist" }}
<div class="flex items-center">
<label for="{{ .Name }}"
class="w-64 text-right pr-4 {{ if .Disabled }}text-gray-400{{ else }}text-black{{ end }} text-sm">
{{ .Caption }}{{ if .Subcaption != "" }} {{ .Subcaption }}{{ end }}
{{ if .Required }}<span class="text-red-600">*</span>{{ end }}
</label>
{{ v := .Value }}
<select id="{{ .Name }}" name="{{ .Name }}" {{ if .Required }}required{{ end }} {{ if .Disabled }}disabled{{ end }}
class="flex-1 px-3 py-2 border border-gray-300 rounded focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
<option value="XX" {{ if v == "XX" }}selected{{ end }}>🏳️ (unknown)</option>
{{ range GetCountryList() }}
@@ -126,12 +164,13 @@
</div>
{{ else if .Type == "localelist" }}
<div class="flex items-center">
<label for="{{ .Name }}" class="w-64 text-right pr-4 text-black text-sm">
<label for="{{ .Name }}"
class="w-64 text-right pr-4 {{ if .Disabled }}text-gray-400{{ else }}text-black{{ end }} text-sm">
{{ .Caption }}{{ if .Subcaption != "" }} {{ .Subcaption }}{{ end }}
{{ if .Required }}<span class="text-red-600">*</span>{{ end }}
</label>
{{ v := .Value }}
<select id="{{ .Name }}" name="{{ .Name }}" {{ if .Required }}required{{ end }}
<select id="{{ .Name }}" name="{{ .Name }}" {{ if .Required }}required{{ end }} {{ if .Disabled }}disabled{{ end }}
class="flex-1 max-w-md px-3 py-2 border border-gray-300 rounded focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
{{ range GetLanguageList() }}
<option value="{{ .Tag }}" {{ if .Tag == v }}selected{{ end }}>{{ .Name }}</option>
@@ -140,12 +179,13 @@
</div>
{{ else if .Type == "tzlist" }}
<div class="flex items-center">
<label for="{{ .Name }}" class="w-64 text-right pr-4 text-black text-sm">
<label for="{{ .Name }}"
class="w-64 text-right pr-4 {{ if .Disabled }}text-gray-400{{ else }}text-black{{ end }} text-sm">
{{ .Caption }}{{ if .Subcaption != "" }} {{ .Subcaption }}{{ end }}
{{ if .Required }}<span class="text-red-600">*</span>{{ end }}
</label>
{{ v := .Value }}
<select id="{{ .Name }}" name="{{ .Name }}" {{ if .Required }}required{{ end }}
<select id="{{ .Name }}" name="{{ .Name }}" {{ if .Required }}required{{ end }} {{ if .Disabled }}disabled{{ end }}
class="flex-1 max-w-md px-3 py-2 border border-gray-300 rounded focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
{{ range GetTimeZoneList() }}
<option value="{{ . }}" {{ if v == . }}selected{{ end }}>{{ . }}</option>
@@ -155,25 +195,27 @@
{{ else if .Type == "date" }}
{{ dv := .DateValues() }}
<div class="flex items-center">
<label class="w-64 text-right pr-4 text-black text-sm">
<label class="w-64 text-right pr-4 {{ if .Disabled }}text-gray-400{{ else }}text-black{{ end }} text-sm">
{{ .Caption }}{{ if .Subcaption != "" }} {{ .Subcaption }}{{ end }}
{{ if .Required }}<span class="text-red-600">*</span>{{ end }}
</label>
<div class="flex gap-2">
<select name="{{ .Name }}_month"
<select name="{{ .Name }}_month" {{ if .Disabled }}disabled{{ end }}
class="px-3 py-2 border border-gray-300 rounded focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
<option value="-1" {{ if dv[0] == -1 }}selected{{ end }}>---</option>
{{ range i := GetMonthList() }}
<option value="{{ i + 1 }}" {{ if dv[0] == i + 1 }}selected{{ end }}>{{ . }}</option>
{{ end }}
</select>
<select name="{{ .Name }}_day" class="px-3 py-2 border border-gray-300 rounded focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
<select name="{{ .Name }}_day" {{ if .Disabled }}disabled{{ end }}
class="px-3 py-2 border border-gray-300 rounded focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
<option value="-1" {{ if dv[1] == -1 }}selected{{ end }}>---</option>
{{ range MakeIntRange(1, 32, 1) }}
<option value="{{ . }}" {{ if dv[1] == . }}selected{{ end }}>{{ . }}</option>
{{ end }}
</select>
<select name="{{ .Name }}_year" class="px-3 py-2 border border-gray-300 rounded focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
<select name="{{ .Name }}_year" {{ if .Disabled }}disabled{{ end }}
class="px-3 py-2 border border-gray-300 rounded focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
<option value="-1" {{ if dv[2] == -1 }}selected{{ end }}>---</option>
{{ range MakeYearRange(.Param) }}
<option value="{{ . }}" {{ if dv[2] == . }}selected{{ end }}>{{ . }}</option>
@@ -183,13 +225,31 @@
</div>
{{ else if .Type == "userphoto" }}
<div class="flex items-start">
<label class="w-64 text-right pr-4 text-black text-sm pt-2">{{ .Caption }}
<label class="w-64 text-right pr-4 {{ if .Disabled }}text-gray-400{{ else }}text-black{{ end }} text-sm pt-2">{{ .Caption }}
{{ if .Subcaption != "" }} {{ .Subcaption }}{{ end }} (click to change):</label>
<input type="hidden" name="{{ .Name }}_data" value="{{ .Value }}"/>
<a href="/profile_photo?tgt={{ target | url }}"
class="border-2 border-gray-300 rounded hover:border-blue-500 transition-colors">
<img src="{{ .Value }}" alt="Click to upload photo" class="w-25 h-25">
</a>
{{ if .Disabled }}
<img src="{{ .Value }}" class="w-25 h-25">
{{ else }}
<a href="/profile_photo?tgt={{ target | url }}"
class="border-2 border-gray-300 rounded hover:border-blue-500 transition-colors">
<img src="{{ .Value }}" alt="Click to upload photo" class="w-25 h-25">
</a>
{{ end }}
</div>
{{ else if .Type == "communitylogo" }}
<div class="flex items-start">
<label class="w-64 text-right pr-4 {{ if .Disabled }}text-gray-400{{ else }}text-black{{ end }} text-sm pt-2">{{ .Caption }}
{{ if .Subcaption != "" }} {{ .Subcaption }}{{ end }} (click to change):</label>
<input type="hidden" name="{{ .Name }}_data" value="{{ .Value }}"/>
{{ if .Disabled }}
<img src="/img/builtin/default-community.jpg" class="w-28 h-16 rounded">
{{ else }}
<a href="{{ .Param }}"
class="border-2 border-gray-300 rounded hover:border-blue-500 transition-colors">
<img src="/img/builtin/default-community.jpg" alt="Click to upload logo" class="w-28 h-16 rounded">
</a>
{{ end }}
</div>
{{ else if .Type == "header" }}
<h2 class="text-lg font-bold text-black mb-4">{{ .Caption }}</h2>
@@ -202,7 +262,7 @@
{{ range amsterdam_dialog.Fields }}
{{ if .Type == "button" }}
{{ clstmp := "bg-" + .Param + "-600 hover:bg-" + .Param + "-700" }}
<button type="submit" name="{{ .Name }}"
<button type="submit" name="{{ .Name }}" {{ if .Disabled }}disabled{{ end }}
class="{{ clstmp }} text-white px-6 py-2 rounded font-medium transition-colors">{{ .Caption }}</button>
{{ end }}
{{ end }}