feat(admin): split-pane user management with list and detail form

This commit is contained in:
Claus Lohmar 2026-06-15 05:23:46 +00:00
parent 56acb3b763
commit b6e4345316
5 changed files with 215 additions and 161 deletions

View file

@ -113,5 +113,24 @@ templ adminStyles() {
.password-box{font-family:monospace;background:var(--bg);padding:10px;border-radius:6px;margin:8px 0;font-size:0.85rem;user-select:all;word-break:break-all} .password-box{font-family:monospace;background:var(--bg);padding:10px;border-radius:6px;margin:8px 0;font-size:0.85rem;user-select:all;word-break:break-all}
.password-hint{font-size:0.75rem;color:var(--orange)} .password-hint{font-size:0.75rem;color:var(--orange)}
@media(max-width:640px){.sidebar{display:none}.content{margin-left:0}.stats-grid{grid-template-columns:1fr 1fr}.form-row{grid-template-columns:1fr}} @media(max-width:640px){.sidebar{display:none}.content{margin-left:0}.stats-grid{grid-template-columns:1fr 1fr}.form-row{grid-template-columns:1fr}}
/* Split pane */
.split-pane{display:flex;flex-direction:column;gap:16px;height:calc(100vh - 100px)}
.pane-top{flex:1;overflow-y:auto;min-height:200px}
.pane-bottom{flex:1;overflow-y:auto;border-top:1px solid var(--border);padding-top:16px}
.user-list{background:var(--surface);border:1px solid var(--border);border-radius:12px;overflow:hidden}
.list-body{max-height:300px;overflow-y:auto}
.user-row{display:flex;justify-content:space-between;align-items:center;padding:10px 14px;border-bottom:1px solid var(--border);cursor:pointer;transition:background 0.1s}
.user-row:hover{background:var(--surface2)}
.user-row:last-child{border-bottom:none}
.user-row-left{display:flex;align-items:center;gap:10px}
.user-avatar{width:32px;height:32px;border-radius:50%;background:var(--surface2);display:flex;align-items:center;justify-content:center;font-size:0.8rem;font-weight:600;color:var(--text2)}
.user-sub{font-size:0.75rem;color:var(--text2);margin-top:1px}
.user-row-right{display:flex;align-items:center;gap:8px}
.empty-row{padding:20px;text-align:center;color:var(--text2)}
.loading{padding:20px;text-align:center;color:var(--text2)}
.empty-state{display:flex;flex-direction:column;align-items:center;justify-content:center;padding:40px;color:var(--text2);gap:10px}
.empty-state p{font-size:0.85rem}
.detail-card{background:var(--surface);border:1px solid var(--border);border-radius:12px;padding:16px}
</style> </style>
} }

File diff suppressed because one or more lines are too long

View file

@ -1,83 +1,92 @@
package templates package templates
templ UserDashboard() { templ UserDashboard() {
<div class="section-header"> @Layout("users") {
<h2>User Management</h2> <div class="split-pane">
<button class="btn" hx-get="/admin/users/create-form" hx-target="#form-container" hx-swap="innerHTML">+ Add User</button> <div class="pane-top">
</div> <div class="section-header">
<h2>Users</h2>
<div id="form-container" style="margin-bottom:16px"></div> <button class="btn" hx-get="/admin/users/create-form" hx-target="#user-detail" hx-swap="innerHTML">+ Add</button>
</div>
<div class="user-table-wrap"> <div class="user-list">
<div class="table-toolbar"> <div class="table-toolbar">
<span class="table-title">All Users</span> <span class="count">{ 0 } users</span>
<button class="btn-sm" hx-get="/admin/api/users" hx-target="#user-table-body" hx-swap="innerHTML" hx-trigger="load, click">Refresh</button> <button class="btn-sm" hx-get="/admin/users/list" hx-target="#user-list-body" hx-swap="innerHTML">Refresh</button>
</div>
<div class="list-body" id="user-list-body" hx-get="/admin/users/list" hx-trigger="load" hx-swap="innerHTML">
<div class="loading">Loading users...</div>
</div>
</div>
</div>
<div class="pane-bottom" id="user-detail">
<div class="empty-state">
<svg width="40" height="40" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M20 21v-2a4 4 0 00-4-4H8a4 4 0 00-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>
<p>Select a user or click + Add</p>
</div>
</div>
</div> </div>
<table> }
<thead><tr>
<th>Username</th><th>Name</th><th>Email</th><th>Role</th><th>Status</th><th></th>
</tr></thead>
<tbody id="user-table-body" hx-get="/admin/api/users" hx-trigger="load" hx-swap="innerHTML">
<tr><td colspan="6" class="loading-row">Loading users...</td></tr>
</tbody>
</table>
</div>
} }
templ UserRows(users []UserRow) { templ UserList(users []UserRow) {
for _, u := range users { if len(users) == 0 {
<tr> <div class="empty-row">No users found</div>
<td><strong>{ u.Username }</strong></td> } else {
<td>{ u.DisplayName }</td> for _, u := range users {
<td>{ u.Email }</td> <div class="user-row" onclick="selectUser('{ u.Username }')">
<td><span class="role-tag">{ u.Role }</span></td> <div class="user-row-left">
<td> <div class="user-avatar">{ initials(u.DisplayName) }</div>
if u.Disabled { <div>
<span class="status-dot off"></span> Disabled <strong>{ u.Username }</strong>
} else { <div class="user-sub">{ u.Email }</div>
<span class="status-dot on"></span> Active </div>
} </div>
</td> <div class="user-row-right">
<td> <span class="role-tag">{ u.Role }</span>
<button class="btn-sm danger" if u.Disabled {
hx-delete={ "/admin/api/users/" + u.Username } <span class="status-dot off"></span>
hx-confirm={ "Delete " + u.Username + "?" } } else {
hx-target="closest tr" <span class="status-dot on"></span>
hx-swap="delete">Delete</button> }
</td> </div>
</tr> </div>
}
} }
} }
templ CreateUserForm() { templ CreateUserForm() {
<div class="form-card" id="create-form"> <div class="detail-card">
<div class="section-header"> <div class="section-header">
<h3>New User</h3> <h3>New User</h3>
<button class="btn-sm" <button class="btn-sm" hx-get="/admin/users/cancel-form" hx-target="#user-detail" hx-swap="innerHTML">Cancel</button>
hx-get="/admin/users/cancel-form" hx-target="#form-container" hx-swap="innerHTML">Cancel</button>
</div> </div>
<form hx-post="/admin/api/users" hx-target="#form-container" hx-swap="innerHTML"> <form hx-post="/admin/users/create" hx-target="#user-detail" hx-swap="innerHTML">
<div class="form-row"> <div class="form-group">
<div class="form-group"> <label>Username *</label>
<label>Username *</label> <input type="text" name="username" class="input" required placeholder="jdoe"/>
<input type="text" name="username" class="input" required placeholder="jdoe"/> </div>
</div> <div class="form-group">
<div class="form-group"> <label>Display Name</label>
<label>Display Name</label> <input type="text" name="display_name" class="input" placeholder="John Doe"/>
<input type="text" name="display_name" class="input" placeholder="John Doe"/> </div>
</div> <div class="form-group">
<label>Email</label>
<input type="email" name="email" class="input" placeholder="john@example.com"/>
</div> </div>
<div class="form-row"> <div class="form-row">
<div class="form-group"> <div class="form-group">
<label>Email</label> <label>Role</label>
<input type="email" name="email" class="input" placeholder="john@example.com"/> <select name="role" class="input">
<option value="user">User</option>
<option value="admin">Admin</option>
</select>
</div> </div>
<div class="form-group"> <div class="form-group">
<label>Groups</label> <label>Groups</label>
<input type="text" name="groups" class="input" placeholder="admins,users"/> <input type="text" name="groups" class="input" placeholder="admins,users"/>
</div> </div>
</div> </div>
<button type="submit" class="btn" style="margin-top:8px;width:100%">Create User</button> <button type="submit" class="btn" style="width:100%;margin-top:8px">Create User</button>
</form> </form>
</div> </div>
} }
@ -90,15 +99,20 @@ templ CreateUserSuccess(results []CreateUserResultRow) {
<div class="alert success"> <div class="alert success">
<strong>{ r.Username }</strong> created <strong>{ r.Username }</strong> created
<div class="password-box">{ r.GeneratedPassword }</div> <div class="password-box">{ r.GeneratedPassword }</div>
<div class="password-hint">Save this — it won't be shown again</div> <div class="password-hint">Save this password — it won't be shown again</div>
</div> </div>
} }
} }
<script type="text/javascript"> <script type="text/javascript">
setTimeout(function() { htmx.trigger('#user-table-body', 'click'); }, 500); setTimeout(function() { htmx.trigger('#user-list-body', 'click'); }, 500);
</script> </script>
} }
func initials(s string) string {
if len(s) == 0 { return "?" }
return string(s[0])
}
type UserRow struct { type UserRow struct {
Username string Username string
DisplayName string DisplayName string
@ -106,6 +120,7 @@ type UserRow struct {
Role string Role string
Groups string Groups string
Disabled bool Disabled bool
Password string
} }
type CreateUserResultRow struct { type CreateUserResultRow struct {
Username string Username string

View file

@ -29,7 +29,38 @@ func UserDashboard() templ.Component {
templ_7745c5c3_Var1 = templ.NopComponent templ_7745c5c3_Var1 = templ.NopComponent
} }
ctx = templ.ClearChildren(ctx) ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div class=\"section-header\"><h2>User Management</h2><button class=\"btn\" hx-get=\"/admin/users/create-form\" hx-target=\"#form-container\" hx-swap=\"innerHTML\">+ Add User</button></div><div id=\"form-container\" style=\"margin-bottom:16px\"></div><div class=\"user-table-wrap\"><div class=\"table-toolbar\"><span class=\"table-title\">All Users</span> <button class=\"btn-sm\" hx-get=\"/admin/api/users\" hx-target=\"#user-table-body\" hx-swap=\"innerHTML\" hx-trigger=\"load, click\">Refresh</button></div><table><thead><tr><th>Username</th><th>Name</th><th>Email</th><th>Role</th><th>Status</th><th></th></tr></thead> <tbody id=\"user-table-body\" hx-get=\"/admin/api/users\" hx-trigger=\"load\" hx-swap=\"innerHTML\"><tr><td colspan=\"6\" class=\"loading-row\">Loading users...</td></tr></tbody></table></div>") templ_7745c5c3_Var2 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div class=\"split-pane\"><div class=\"pane-top\"><div class=\"section-header\"><h2>Users</h2><button class=\"btn\" hx-get=\"/admin/users/create-form\" hx-target=\"#user-detail\" hx-swap=\"innerHTML\">+ Add</button></div><div class=\"user-list\"><div class=\"table-toolbar\"><span class=\"count\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(0)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `core/admin/templates/user-dashboard.templ`, Line: 13, Col: 29}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, " users</span> <button class=\"btn-sm\" hx-get=\"/admin/users/list\" hx-target=\"#user-list-body\" hx-swap=\"innerHTML\">Refresh</button></div><div class=\"list-body\" id=\"user-list-body\" hx-get=\"/admin/users/list\" hx-trigger=\"load\" hx-swap=\"innerHTML\"><div class=\"loading\">Loading users...</div></div></div></div><div class=\"pane-bottom\" id=\"user-detail\"><div class=\"empty-state\"><svg width=\"40\" height=\"40\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\"><path d=\"M20 21v-2a4 4 0 00-4-4H8a4 4 0 00-4 4v2\"></path><circle cx=\"12\" cy=\"7\" r=\"4\"></circle></svg><p>Select a user or click + Add</p></div></div></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
templ_7745c5c3_Err = Layout("users").Render(templ.WithChildren(ctx, templ_7745c5c3_Var2), templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@ -37,7 +68,7 @@ func UserDashboard() templ.Component {
}) })
} }
func UserRows(users []UserRow) templ.Component { func UserList(users []UserRow) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
@ -53,108 +84,89 @@ func UserRows(users []UserRow) templ.Component {
}() }()
} }
ctx = templ.InitializeContext(ctx) ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var2 := templ.GetChildren(ctx) templ_7745c5c3_Var4 := templ.GetChildren(ctx)
if templ_7745c5c3_Var2 == nil { if templ_7745c5c3_Var4 == nil {
templ_7745c5c3_Var2 = templ.NopComponent templ_7745c5c3_Var4 = templ.NopComponent
} }
ctx = templ.ClearChildren(ctx) ctx = templ.ClearChildren(ctx)
for _, u := range users { if len(users) == 0 {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<tr><td><strong>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "<div class=\"empty-row\">No users found</div>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var3 string } else {
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(u.Username) for _, u := range users {
if templ_7745c5c3_Err != nil { templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "<div class=\"user-row\" onclick=\"selectUser('{ u.Username }')\"><div class=\"user-row-left\"><div class=\"user-avatar\">")
return templ.Error{Err: templ_7745c5c3_Err, FileName: `core/admin/templates/user-dashboard.templ`, Line: 30, Col: 27}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "</strong></td><td>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var4 string
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(u.DisplayName)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `core/admin/templates/user-dashboard.templ`, Line: 31, Col: 22}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "</td><td>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var5 string
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(u.Email)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `core/admin/templates/user-dashboard.templ`, Line: 32, Col: 16}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "</td><td><span class=\"role-tag\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var6 string
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(u.Role)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `core/admin/templates/user-dashboard.templ`, Line: 33, Col: 38}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "</span></td><td>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if u.Disabled {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "<span class=\"status-dot off\"></span> Disabled")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
} else { var templ_7745c5c3_Var5 string
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "<span class=\"status-dot on\"></span> Active") templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(initials(u.DisplayName))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `core/admin/templates/user-dashboard.templ`, Line: 38, Col: 55}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "</div><div><strong>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var6 string
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(u.Username)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `core/admin/templates/user-dashboard.templ`, Line: 40, Col: 26}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "</strong><div class=\"user-sub\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var7 string
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(u.Email)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `core/admin/templates/user-dashboard.templ`, Line: 41, Col: 37}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "</div></div></div><div class=\"user-row-right\"><span class=\"role-tag\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var8 string
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(u.Role)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `core/admin/templates/user-dashboard.templ`, Line: 45, Col: 36}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "</span> ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if u.Disabled {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "<span class=\"status-dot off\"></span>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
} else {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "<span class=\"status-dot on\"></span>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "</div></div>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "</td><td><button class=\"btn-sm danger\" hx-delete=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var7 string
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.ResolveAttributeValue("/admin/api/users/" + u.Username)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `core/admin/templates/user-dashboard.templ`, Line: 43, Col: 49}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var7)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "\" hx-confirm=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var8 string
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.ResolveAttributeValue("Delete " + u.Username + "?")
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `core/admin/templates/user-dashboard.templ`, Line: 44, Col: 46}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var8)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "\" hx-target=\"closest tr\" hx-swap=\"delete\">Delete</button></td></tr>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
} }
} }
return nil return nil
@ -182,7 +194,7 @@ func CreateUserForm() templ.Component {
templ_7745c5c3_Var9 = templ.NopComponent templ_7745c5c3_Var9 = templ.NopComponent
} }
ctx = templ.ClearChildren(ctx) ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "<div class=\"form-card\" id=\"create-form\"><div class=\"section-header\"><h3>New User</h3><button class=\"btn-sm\" hx-get=\"/admin/users/cancel-form\" hx-target=\"#form-container\" hx-swap=\"innerHTML\">Cancel</button></div><form hx-post=\"/admin/api/users\" hx-target=\"#form-container\" hx-swap=\"innerHTML\"><div class=\"form-row\"><div class=\"form-group\"><label>Username *</label> <input type=\"text\" name=\"username\" class=\"input\" required placeholder=\"jdoe\"></div><div class=\"form-group\"><label>Display Name</label> <input type=\"text\" name=\"display_name\" class=\"input\" placeholder=\"John Doe\"></div></div><div class=\"form-row\"><div class=\"form-group\"><label>Email</label> <input type=\"email\" name=\"email\" class=\"input\" placeholder=\"john@example.com\"></div><div class=\"form-group\"><label>Groups</label> <input type=\"text\" name=\"groups\" class=\"input\" placeholder=\"admins,users\"></div></div><button type=\"submit\" class=\"btn\" style=\"margin-top:8px;width:100%\">Create User</button></form></div>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "<div class=\"detail-card\"><div class=\"section-header\"><h3>New User</h3><button class=\"btn-sm\" hx-get=\"/admin/users/cancel-form\" hx-target=\"#user-detail\" hx-swap=\"innerHTML\">Cancel</button></div><form hx-post=\"/admin/users/create\" hx-target=\"#user-detail\" hx-swap=\"innerHTML\"><div class=\"form-group\"><label>Username *</label> <input type=\"text\" name=\"username\" class=\"input\" required placeholder=\"jdoe\"></div><div class=\"form-group\"><label>Display Name</label> <input type=\"text\" name=\"display_name\" class=\"input\" placeholder=\"John Doe\"></div><div class=\"form-group\"><label>Email</label> <input type=\"email\" name=\"email\" class=\"input\" placeholder=\"john@example.com\"></div><div class=\"form-row\"><div class=\"form-group\"><label>Role</label> <select name=\"role\" class=\"input\"><option value=\"user\">User</option> <option value=\"admin\">Admin</option></select></div><div class=\"form-group\"><label>Groups</label> <input type=\"text\" name=\"groups\" class=\"input\" placeholder=\"admins,users\"></div></div><button type=\"submit\" class=\"btn\" style=\"width:100%;margin-top:8px\">Create User</button></form></div>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@ -220,7 +232,7 @@ func CreateUserSuccess(results []CreateUserResultRow) templ.Component {
var templ_7745c5c3_Var11 string var templ_7745c5c3_Var11 string
templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(r.Error) templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(r.Error)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `core/admin/templates/user-dashboard.templ`, Line: 88, Col: 37} return templ.Error{Err: templ_7745c5c3_Err, FileName: `core/admin/templates/user-dashboard.templ`, Line: 97, Col: 37}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
@ -238,7 +250,7 @@ func CreateUserSuccess(results []CreateUserResultRow) templ.Component {
var templ_7745c5c3_Var12 string var templ_7745c5c3_Var12 string
templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(r.Username) templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(r.Username)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `core/admin/templates/user-dashboard.templ`, Line: 91, Col: 24} return templ.Error{Err: templ_7745c5c3_Err, FileName: `core/admin/templates/user-dashboard.templ`, Line: 100, Col: 24}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
@ -251,19 +263,19 @@ func CreateUserSuccess(results []CreateUserResultRow) templ.Component {
var templ_7745c5c3_Var13 string var templ_7745c5c3_Var13 string
templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(r.GeneratedPassword) templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(r.GeneratedPassword)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `core/admin/templates/user-dashboard.templ`, Line: 92, Col: 51} return templ.Error{Err: templ_7745c5c3_Err, FileName: `core/admin/templates/user-dashboard.templ`, Line: 101, Col: 51}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "</div><div class=\"password-hint\">Save this — it won't be shown again</div></div>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "</div><div class=\"password-hint\">Save this password — it won't be shown again</div></div>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
} }
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "<script type=\"text/javascript\">\n\t\tsetTimeout(function() { htmx.trigger('#user-table-body', 'click'); }, 500);\n\t</script>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "<script type=\"text/javascript\">\n\t\tsetTimeout(function() { htmx.trigger('#user-list-body', 'click'); }, 500);\n\t</script>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@ -271,6 +283,13 @@ func CreateUserSuccess(results []CreateUserResultRow) templ.Component {
}) })
} }
func initials(s string) string {
if len(s) == 0 {
return "?"
}
return string(s[0])
}
type UserRow struct { type UserRow struct {
Username string Username string
DisplayName string DisplayName string
@ -278,6 +297,7 @@ type UserRow struct {
Role string Role string
Groups string Groups string
Disabled bool Disabled bool
Password string
} }
type CreateUserResultRow struct { type CreateUserResultRow struct {
Username string Username string

View file

@ -224,7 +224,7 @@ func (h *Handler) userRowsHandler(w http.ResponseWriter, r *http.Request) {
rows = append(rows, UserToRow(u)) rows = append(rows, UserToRow(u))
} }
component := templates.UserRows(rows) component := templates.UserList(rows)
component.Render(r.Context(), w) component.Render(r.Context(), w)
} }