From 908c8b02f8a9168239dcc47cecb24a3d6cd41aeb Mon Sep 17 00:00:00 2001 From: Asaf Gartner Date: Mon, 1 Jul 2024 02:55:21 +0300 Subject: [PATCH] Link editor --- .../2024-06-30T233630Z_AddPrimaryToLinks.go | 48 +++++ src/models/link.go | 1 + src/templates/mapping.go | 1 + src/templates/src/include/link_editor.html | 177 ++++++++++++++++++ src/templates/src/project_edit.html | 149 +-------------- src/templates/src/user_settings.html | 6 +- src/templates/types.go | 1 + src/website/links_helper.go | 5 +- src/website/projects.go | 5 +- src/website/user.go | 12 +- 10 files changed, 244 insertions(+), 161 deletions(-) create mode 100644 src/migration/migrations/2024-06-30T233630Z_AddPrimaryToLinks.go create mode 100644 src/templates/src/include/link_editor.html diff --git a/src/migration/migrations/2024-06-30T233630Z_AddPrimaryToLinks.go b/src/migration/migrations/2024-06-30T233630Z_AddPrimaryToLinks.go new file mode 100644 index 00000000..c0dae0e7 --- /dev/null +++ b/src/migration/migrations/2024-06-30T233630Z_AddPrimaryToLinks.go @@ -0,0 +1,48 @@ +package migrations + +import ( + "context" + "time" + + "git.handmade.network/hmn/hmn/src/migration/types" + "git.handmade.network/hmn/hmn/src/utils" + "github.com/jackc/pgx/v5" +) + +func init() { + registerMigration(AddPrimaryToLinks{}) +} + +type AddPrimaryToLinks struct{} + +func (m AddPrimaryToLinks) Version() types.MigrationVersion { + return types.MigrationVersion(time.Date(2024, 6, 30, 23, 36, 30, 0, time.UTC)) +} + +func (m AddPrimaryToLinks) Name() string { + return "AddPrimaryToLinks" +} + +func (m AddPrimaryToLinks) Description() string { + return "Adds 'primary_link' field to links" +} + +func (m AddPrimaryToLinks) Up(ctx context.Context, tx pgx.Tx) error { + utils.Must1(tx.Exec(ctx, + ` + ALTER TABLE link + ADD COLUMN primary_link BOOLEAN NOT NULL DEFAULT false; + `, + )) + return nil +} + +func (m AddPrimaryToLinks) Down(ctx context.Context, tx pgx.Tx) error { + utils.Must1(tx.Exec(ctx, + ` + ALTER TABLE link + DROP COLUMN primary_link; + `, + )) + return nil +} diff --git a/src/models/link.go b/src/models/link.go index 5f9ea617..274347d9 100644 --- a/src/models/link.go +++ b/src/models/link.go @@ -5,6 +5,7 @@ type Link struct { Name string `db:"name"` URL string `db:"url"` Ordering int `db:"ordering"` + Primary bool `db:"primary_link"` UserID *int `db:"user_id"` ProjectID *int `db:"project_id"` } diff --git a/src/templates/mapping.go b/src/templates/mapping.go index b68ca178..c513a52b 100644 --- a/src/templates/mapping.go +++ b/src/templates/mapping.go @@ -335,6 +335,7 @@ func LinkToTemplate(link *models.Link) Link { ServiceName: service.Name, Icon: service.IconName, Username: username, + Primary: link.Primary, } } diff --git a/src/templates/src/include/link_editor.html b/src/templates/src/include/link_editor.html new file mode 100644 index 00000000..acb4baf7 --- /dev/null +++ b/src/templates/src/include/link_editor.html @@ -0,0 +1,177 @@ +
+ + Links + + Add Link + +
+ + + +
+ + +
+ diff --git a/src/templates/src/project_edit.html b/src/templates/src/project_edit.html index 8c7c25f4..9ec86e09 100644 --- a/src/templates/src/project_edit.html +++ b/src/templates/src/project_edit.html @@ -155,32 +155,7 @@ {{ end }} -
- - Links - + Add Link - -
- - - -
- -
+ {{ template "link_editor.html" .ProjectSettings.LinksJSON }} {{ if and .Editing .User.IsStaff }}
@@ -422,127 +397,5 @@ {{ .TextEditor.UploadUrl }} ); - /////////// - // Links // - /////////// - - const linksContainer = document.querySelector("#links"); - const linksJSONInput = document.querySelector("#links_json"); - const linkTemplate = makeTemplateCloner("link_row"); - const dummyLinkTemplate = makeTemplateCloner("link_row_dummy"); - - const initialLinks = JSON.parse("{{ .ProjectSettings.LinksJSON }}"); - for (const link of initialLinks) { - const l = linkTemplate(); - l.nameInput.value = link.name; - l.urlInput.value = link.url; - linksContainer.appendChild(l.root) - } - ensureLinksEmptyState(); - - projectForm.addEventListener("submit", () => updateLinksJSON()); - - function addLink(e) { - e.preventDefault(); - linksContainer.appendChild(linkTemplate().root); - } - - function deleteLink(e) { - e.preventDefault(); - const l = e.target.closest(".link_row"); - l.remove(); - - ensureLinksEmptyState(); - } - - function ensureLinksEmptyState() { - if (!linksContainer.querySelector(".link_row")) { - // Empty state is a single row - linksContainer.appendChild(linkTemplate().root); - } - } - - function updateLinksJSON() { - const links = []; - for (const l of linksContainer.querySelectorAll(".link_row")) { - const value = l.querySelector(".link_name").value; - const url = l.querySelector(".link_url").value; - if (!url) { - continue; - } - - links.push({ - "name": name, - "url": url, - }); - } - linksJSONInput.value = JSON.stringify(links); - } - - let draggingLink = null; - let linkDragStartY = 0; - let linkDragStartMouseY = 0; - - function startLinkDrag(e) { - e.preventDefault(); - const l = e.target.closest(".link_row"); - - const top = l.offsetTop; - - l.insertAdjacentElement("beforebegin", dummyLinkTemplate().root); - document.querySelector("body").classList.add("grabbing"); - - l.style.position = "absolute"; - l.style.top = `${top}px`; - l.classList.add("link_dragging"); - - draggingLink = l; - linkDragStartY = top; - linkDragStartMouseY = e.pageY; - } - - function doLinkDrag(e) { - if (!draggingLink) { - return; - } - - const maxTop = linksContainer.offsetHeight - draggingLink.offsetHeight; - - const delta = e.pageY - linkDragStartMouseY; - const top = Math.max(0, Math.min(maxTop, linkDragStartY + delta)); - const middle = top + draggingLink.offsetHeight/2; - - draggingLink.style.top = `${top}px`; - - const numLinks = linksContainer.querySelectorAll(".link_row").length; - const itemHeight = linksContainer.offsetHeight / numLinks; - const index = Math.floor(middle / itemHeight); - - const links = linksContainer.querySelectorAll(".link_row:not(.link_dragging)"); - const dummy = linksContainer.querySelector(".link_row_dummy"); - dummy.remove(); - linksContainer.insertBefore(dummy, links[index]); - } - - function endLinkDrag(e) { - if (!draggingLink) { - return; - } - - const dummy = linksContainer.querySelector(".link_row_dummy"); - draggingLink.remove(); - linksContainer.insertBefore(draggingLink, dummy); - dummy.remove(); - - draggingLink.style.position = null; - draggingLink.style.top = null; - draggingLink.classList.remove("link_dragging"); - - document.querySelector("body").classList.remove("grabbing"); - draggingLink = null; - } - window.addEventListener("mouseup", endLinkDrag); - window.addEventListener("mousemove", doLinkDrag); - {{ end }} diff --git a/src/templates/src/user_settings.html b/src/templates/src/user_settings.html index 0d94f14e..372e6973 100644 --- a/src/templates/src/user_settings.html +++ b/src/templates/src/user_settings.html @@ -2,6 +2,7 @@ {{ define "extrahead" }} + {{ end }} {{ define "content" }} @@ -63,9 +64,7 @@
-
- Links -
+ {{ template "link_editor.html" .LinksJSON }}
Relevant links to put on your profile.
diff --git a/src/templates/types.go b/src/templates/types.go index 572b951f..d20886f2 100644 --- a/src/templates/types.go +++ b/src/templates/types.go @@ -225,6 +225,7 @@ type Link struct { ServiceName string `json:"serviceName"` Username string `json:"text"` Icon string `json:"icon"` + Primary bool `json:"primary"` } type Podcast struct { diff --git a/src/website/links_helper.go b/src/website/links_helper.go index f8f92b70..4aaff92e 100644 --- a/src/website/links_helper.go +++ b/src/website/links_helper.go @@ -8,8 +8,9 @@ import ( ) type ParsedLink struct { - Name string `json:"name"` - Url string `json:"url"` + Name string `json:"name"` + Url string `json:"url"` + Primary bool `json:"primary"` } func ParseLinks(text string) []ParsedLink { diff --git a/src/website/projects.go b/src/website/projects.go index 80e54e73..a4d47d31 100644 --- a/src/website/projects.go +++ b/src/website/projects.go @@ -935,12 +935,13 @@ func updateProject(ctx context.Context, tx pgx.Tx, user *models.User, payload *P for i, link := range payload.Links { _, err = tx.Exec(ctx, ` - INSERT INTO link (name, url, ordering, project_id) - VALUES ($1, $2, $3, $4) + INSERT INTO link (name, url, ordering, primary_link, project_id) + VALUES ($1, $2, $3, $4, $5) `, link.Name, link.Url, i, + link.Primary, payload.ProjectID, ) if err != nil { diff --git a/src/website/user.go b/src/website/user.go index be69f31e..3ad6bbfe 100644 --- a/src/website/user.go +++ b/src/website/user.go @@ -1,6 +1,7 @@ package website import ( + "encoding/json" "errors" "net/http" "strconv" @@ -193,7 +194,7 @@ func UserSettings(c *RequestContext) ResponseData { Avatar *templates.Asset Email string // these fields are handled specially on templates.User ShowEmail bool - LinksText string + LinksJSON string HasPassword bool SubmitUrl string @@ -219,7 +220,7 @@ func UserSettings(c *RequestContext) ResponseData { return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to fetch user links")) } - linksText := LinksToText(links) + linksJSON := string(utils.Must1(json.Marshal(templates.LinksToTemplate(links)))) var tduser *templates.DiscordUser var numUnsavedMessages int @@ -269,7 +270,7 @@ func UserSettings(c *RequestContext) ResponseData { Avatar: templates.AssetToTemplate(c.CurrentUser.AvatarAsset), Email: c.CurrentUser.Email, ShowEmail: c.CurrentUser.ShowEmail, - LinksText: linksText, + LinksJSON: linksJSON, HasPassword: c.CurrentUser.Password != "", SubmitUrl: hmnurl.BuildUserSettings(""), @@ -378,12 +379,13 @@ func UserSettingsSave(c *RequestContext) ResponseData { for i, link := range links { _, err := tx.Exec(c, ` - INSERT INTO link (name, url, ordering, user_id) - VALUES ($1, $2, $3, $4) + INSERT INTO link (name, url, ordering, primary_link, user_id) + VALUES ($1, $2, $3, $4, $5) `, link.Name, link.Url, i, + link.Primary, c.CurrentUser.ID, ) if err != nil {