Project edit - editing owners with preview

This commit is contained in:
Asaf Gartner 2024-07-04 06:32:15 +03:00
parent f706709c34
commit b591e5f6bd
3 changed files with 51 additions and 44 deletions

View File

@ -184,11 +184,12 @@ func ThreadToTemplate(t *models.Thread) Thread {
} }
func UserAvatarDefaultUrl(theme string) string { func UserAvatarDefaultUrl(theme string) string {
// TODO(redesign): Get rid of theme here
return hmnurl.BuildTheme("empty-avatar.svg", theme, true) return hmnurl.BuildTheme("empty-avatar.svg", theme, true)
} }
func UserAvatarUrl(u *models.User) string { func UserAvatarUrl(u *models.User) string {
avatar := "" avatar := UserAvatarDefaultUrl("light")
if u != nil && u.AvatarAsset != nil { if u != nil && u.AvatarAsset != nil {
avatar = hmnurl.BuildS3Asset(u.AvatarAsset.S3Key) avatar = hmnurl.BuildS3Asset(u.AvatarAsset.S3Key)
} }

View File

@ -145,18 +145,24 @@
<div id="owners_error" class="f6"></div> <div id="owners_error" class="f6"></div>
<div id="owner_list" class="pt3 flex flex-wrap g3"> <div id="owner_list" class="pt3 flex flex-wrap g3">
<template id="owner_row"> <template id="owner_row">
<div class="owner_row flex flex-row items-center bg2 pa2" data-tmpl="root"> <div class="owner_row flex flex-row items-center g2 bg3 pa2" data-tmpl="root">
<input type="hidden" name="owners" data-tmpl="input" /> <input type="hidden" name="owners" data-tmpl="input" />
<div class="flex g1 items-center b">
<img data-tmpl="avatar" class="avatar avatar-user avatar-small" src="" />
<span data-tmpl="name"></span> <span data-tmpl="name"></span>
<a class="remove_owner svgicon f7 link-normal pl2" href="javascript:;">{{ svg "close" }}</a> </div>
<a class="remove_owner svgicon f7 link-normal" href="javascript:;">{{ svg "close" }}</a>
</div> </div>
</template> </template>
{{ range .ProjectSettings.Owners }} {{ range .ProjectSettings.Owners }}
<div class="owner_row flex flex-row items-center bg2 pa2"> <div class="owner_row flex flex-row items-center g2 bg3 pa2">
<input type="hidden" name="owners" value="{{ .Username }}" /> <input type="hidden" name="owners" value="{{ .Username }}" />
<span>{{ .Username }}</span> <div class="flex g1 items-center b">
<img class="avatar avatar-user avatar-small" src="{{ .AvatarUrl }}" />
<span title="{{ .Username }}">{{ .Name }}</span>
</div>
{{ if (or $.User.IsStaff (ne .ID $.User.ID)) }} {{ if (or $.User.IsStaff (ne .ID $.User.ID)) }}
<a class="remove_owner svgicon f7 link-normal pl2" href="javascript:;">{{ svg "close" }}</a> <a class="remove_owner svgicon f7 link-normal" href="javascript:;">{{ svg "close" }}</a>
{{ end }} {{ end }}
</div> </div>
{{ end }} {{ end }}
@ -267,11 +273,6 @@
<div id="owners_preview_container"> <div id="owners_preview_container">
<hr class="mv3"> <hr class="mv3">
<div id="owners_preview" class="flex flex-wrap g2"> <div id="owners_preview" class="flex flex-wrap g2">
<!-- TODO(redesign): Actually preview owners -->
<div class="flex g1 items-center b">
<div class="avatar avatar-user avatar-small"></div>
<span>Example User</span>
</div>
</div> </div>
</div> </div>
<!-- TODO(redesign): Preview badges --> <!-- TODO(redesign): Preview badges -->
@ -340,6 +341,7 @@
let ownerList = document.querySelector("#owner_list"); let ownerList = document.querySelector("#owner_list");
let ownerTemplate = makeTemplateCloner("owner_row"); let ownerTemplate = makeTemplateCloner("owner_row");
let ownerPreviewTemplate = makeTemplateCloner("owner_preview"); let ownerPreviewTemplate = makeTemplateCloner("owner_preview");
let ownersPreviewContainer = document.querySelector("#owners_preview");
addOwnerInput.addEventListener("keypress", function(ev) { addOwnerInput.addEventListener("keypress", function(ev) {
if (ev.which == 13) { if (ev.which == 13) {
@ -385,7 +387,7 @@
let result = xhr.response; let result = xhr.response;
if (result) { if (result) {
if (result.found) { if (result.found) {
addOwner(result.canonical); addOwner(result.username, result.name, result.avatarUrl);
addOwnerInput.value = ""; addOwnerInput.value = "";
} else { } else {
ownersError.textContent = "Username not found"; ownersError.textContent = "Username not found";
@ -417,21 +419,41 @@
updateAddOwnerStyles(); updateAddOwnerStyles();
} }
function addOwner(username) { function addOwner(username, bestName, avatarUrl) {
let ownerEl = ownerTemplate(); let ownerEl = ownerTemplate();
ownerEl.input.value = username; ownerEl.input.value = username;
ownerEl.name.textContent = username; ownerEl.name.textContent = bestName;
ownerEl.title = username;
ownerEl.avatar.src = avatarUrl;
ownerList.appendChild(ownerEl.root); ownerList.appendChild(ownerEl.root);
updateAddOwnerStyles(); updateAddOwnerStyles();
updateOwnersPreview();
} }
ownerList.addEventListener("click", function(ev) { ownerList.addEventListener("click", function(ev) {
if (ev.target.classList.contains("remove_owner")) { if (ev.target.closest(".remove_owner")) {
ev.target.parentElement.remove(); ev.target.closest(".owner_row").remove();
} }
updateAddOwnerStyles(); updateAddOwnerStyles();
updateOwnersPreview();
}); });
function updateOwnersPreview() {
let ownerEls = ownerList.querySelectorAll(".owner_row");
ownersPreviewContainer.innerHTML = "";
for (let i = 0; i < ownerEls.length; ++i) {
let avatarUrl = ownerEls[i].querySelector("img").src;
let name = ownerEls[i].querySelector("span").textContent;
let previewEl = ownerPreviewTemplate();
previewEl.avatar.src = avatarUrl;
previewEl.name.textContent = name;
ownersPreviewContainer.appendChild(previewEl.root);
}
}
updateOwnersPreview();
////////////////////////////// //////////////////////////////
// Logo / header management // // Logo / header management //
////////////////////////////// //////////////////////////////

View File

@ -8,50 +8,34 @@ import (
"strings" "strings"
"git.handmade.network/hmn/hmn/src/db" "git.handmade.network/hmn/hmn/src/db"
"git.handmade.network/hmn/hmn/src/hmndata"
"git.handmade.network/hmn/hmn/src/models" "git.handmade.network/hmn/hmn/src/models"
"git.handmade.network/hmn/hmn/src/oops" "git.handmade.network/hmn/hmn/src/oops"
"git.handmade.network/hmn/hmn/src/templates"
"git.handmade.network/hmn/hmn/src/utils" "git.handmade.network/hmn/hmn/src/utils"
) )
func APICheckUsername(c *RequestContext) ResponseData { func APICheckUsername(c *RequestContext) ResponseData {
c.Req.ParseForm() c.Req.ParseForm()
usernameArgs, hasUsername := c.Req.Form["username"] usernameArgs, hasUsername := c.Req.Form["username"]
found := false var user *models.User
canonicalUsername := "" var err error
if hasUsername { if hasUsername {
requestedUsername := usernameArgs[0] requestedUsername := usernameArgs[0]
found = true user, err = hmndata.FetchUserByUsername(c, c.Conn, c.CurrentUser, requestedUsername, hmndata.UsersQuery{})
c.Perf.StartBlock("SQL", "Fetch user") if err != nil && !errors.Is(err, db.NotFound) {
user, err := db.QueryOne[models.User](c, c.Conn,
`
SELECT $columns
FROM
hmn_user
WHERE
LOWER(hmn_user.username) = LOWER($1)
AND status = ANY ($2)
`,
requestedUsername,
[]models.UserStatus{models.UserStatusConfirmed, models.UserStatusApproved},
)
c.Perf.EndBlock()
if err != nil {
if errors.Is(err, db.NotFound) {
found = false
} else {
return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to fetch user: %s", requestedUsername)) return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to fetch user: %s", requestedUsername))
} }
} else {
canonicalUsername = user.Username
}
} }
var res ResponseData var res ResponseData
addCORSHeaders(c, &res) addCORSHeaders(c, &res)
if found { if user != nil {
res.WriteJson(map[string]any{ res.WriteJson(map[string]any{
"found": true, "found": true,
"canonical": canonicalUsername, "username": user.Username,
"name": user.BestName(),
"avatarUrl": templates.UserAvatarUrl(user),
}, nil) }, nil)
} else { } else {
res.WriteJson(map[string]any{ res.WriteJson(map[string]any{