From a70f6406facda63a1d955d3d3177d1d5c5e71e82 Mon Sep 17 00:00:00 2001 From: Arne Brasseur Date: Sat, 14 Sep 2024 15:49:47 +0200 Subject: [PATCH] Bring back and rework the profile form --- src/co/gaiwan/compass/db/queries.clj | 30 +++-- src/co/gaiwan/compass/db/schema.clj | 2 +- src/co/gaiwan/compass/html/graphics.clj | 11 ++ src/co/gaiwan/compass/html/navigation.clj | 45 +++++-- src/co/gaiwan/compass/html/profiles.clj | 145 +++++++++++----------- src/co/gaiwan/compass/routes/profiles.clj | 87 ++++++++++--- 6 files changed, 204 insertions(+), 116 deletions(-) diff --git a/src/co/gaiwan/compass/db/queries.clj b/src/co/gaiwan/compass/db/queries.clj index 25dfed6..157d894 100644 --- a/src/co/gaiwan/compass/db/queries.clj +++ b/src/co/gaiwan/compass/db/queries.clj @@ -45,17 +45,25 @@ [?e :public-profile/name]] (db/db)))) -(defn all-links [user-eid] - (sort-by - :db/id - (db/q - '[:find [(pull ?l [* - {:public-profile/_links [:db/id]} - {:private-profile/_links [:db/id]}]) ...] - :in $ ?u - :where - [?l :profile-link/user ?u]] - (db/db) user-eid))) +(defn public-links [user-eid] + (map db/entity + (db/q + '[:find [?l ...] + :in $ ?u + :where + [?u :public-profile/links ?l]] + (db/db) + user-eid))) + +(defn private-links [user-eid] + (map db/entity + (db/q + '[:find [?l ...] + :in $ ?u + :where + [?u :public-profile/links ?l]] + (db/db) + user-eid))) (defn all-session-types [] (db/q diff --git a/src/co/gaiwan/compass/db/schema.clj b/src/co/gaiwan/compass/db/schema.clj index adffa8c..a882474 100644 --- a/src/co/gaiwan/compass/db/schema.clj +++ b/src/co/gaiwan/compass/db/schema.clj @@ -29,7 +29,7 @@ [:private-profile/bio :string "Free-form Markdown field"] ;; End user entity - [:profile-link/user :ref "User this link belongs too"] + #_[:profile-link/user :ref "User this link belongs too"] [:profile-link/type :string "`mastodon`, `linkedin`, `personal-site`, etc."] [:profile-link/href :string "http/mailto URL"] diff --git a/src/co/gaiwan/compass/html/graphics.clj b/src/co/gaiwan/compass/html/graphics.clj index 6644037..13bf582 100644 --- a/src/co/gaiwan/compass/html/graphics.clj +++ b/src/co/gaiwan/compass/html/graphics.clj @@ -80,3 +80,14 @@ {:d "M823.931359 27.629267l-131.187855 0 0 0.002047c-33.268708 0-60.241013 26.970258-60.241013 60.237943 0 33.270754 26.972305 60.238966 60.241013 60.238966 0.026606 0 0.054235-0.002047 0.082888-0.002047l116.879987 0c35.20992 0 63.754023 28.54308 63.754023 63.751976l0 116.966968 0 0c0 33.271778 26.969235 60.237943 60.238966 60.237943s60.238966-26.966165 60.238966-60.237943l0 0L993.938334 197.636243C993.938334 103.744829 917.823795 27.629267 823.931359 27.629267z"}] [:path {:d "M963.820386 449.299983c-0.026606 0-0.056282 0.002047-0.080841 0.002047L60.321854 449.302029c-0.028653 0-0.056282-0.002047-0.082888-0.002047-33.270754 0-60.238966 26.970258-60.238966 60.241013 0 33.266661 26.968212 60.237943 60.238966 60.237943l903.579373 0 0 0 0.002047 0c33.267685 0 60.234873-26.970258 60.234873-60.237943C1024.055259 476.270241 997.087047 449.299983 963.820386 449.299983z"}]])) + +(o/defstyled github-icon :svg + ([] + [:<> {:viewbox "0 0 24 24"} + [:path {:d "M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"}]])) + +(o/defstyled cog-icon :svg + ([] + [:<> {:viewbox "0 -256 1536 1536"} + [:path {:d "m 1024,512 q 0,106 -75,181 -75,75 -181,75 -106,0 -181,-75 -75,-75 -75,-181 0,-106 75,-181 75,-75 181,-75 106,0 181,75 75,75 75,181 z m 512,109 V 399 q 0,-12 -8,-23 -8,-11 -20,-13 l -185,-28 q -19,-54 -39,-91 35,-50 107,-138 10,-12 10,-25 0,-13 -9,-23 -27,-37 -99,-108 -72,-71 -94,-71 -12,0 -26,9 L 1035,-4 q -44,-23 -91,-38 -16,-136 -29,-186 -7,-28 -36,-28 H 657 q -14,0 -24.5,8.5 Q 622,-239 621,-226 l -28,184 q -49,16 -90,37 L 362,-112 q -10,-9 -25,-9 -14,0 -25,11 Q 186,4 147,58 q -7,10 -7,23 0,12 8,23 15,21 51,66.5 36,45.5 54,70.5 -27,50 -41,99 L 29,367 Q 16,369 8,379.5 0,390 0,403 v 222 q 0,12 8,23 8,11 19,13 l 186,28 q 14,46 39,92 -40,57 -107,138 -10,12 -10,24 0,10 9,23 26,36 98.5,107.5 72.5,71.5 94.5,71.5 13,0 26,-10 l 138,-107 q 44,23 91,38 16,136 29,186 7,28 36,28 h 222 q 14,0 24.5,-8.5 Q 914,1263 915,1250 l 28,-184 q 49,-16 90,-37 l 142,107 q 9,9 24,9 13,0 25,-10 129,-119 165,-170 7,-8 7,-22 0,-12 -8,-23 -15,-21 -51,-66.5 -36,-45.5 -54,-70.5 26,-50 41,-98 l 183,-28 q 13,-2 21,-12.5 8,-10.5 8,-23.5 z"}]] + )) diff --git a/src/co/gaiwan/compass/html/navigation.clj b/src/co/gaiwan/compass/html/navigation.clj index 87db363..ebbde7f 100644 --- a/src/co/gaiwan/compass/html/navigation.clj +++ b/src/co/gaiwan/compass/html/navigation.clj @@ -72,6 +72,13 @@ [:a {:text-decoration "underline"}]] [:a:visited {:color t/--link}] ["li:has(.notifier-dot)" :flex :gap-1] + [:svg :inline :mr-1 {:height "1em" :margin-bottom "-0.15em"} + [:path {:fill t/--text-1}]] + [:.bottom :fixed :bottom-0 :left-0 + :w-full :mb-3 + :text-center + + [:p :m-2]] ([user] [:<> [:div.bar @@ -95,18 +102,32 @@ :on-click "document.body.classList.toggle('menu-open')"} [:strong "Claim your Ti.to ticket for full access"]]] [:div.notifier-dot]])) - (for [[href caption] {(url-for :sessions/index) "Sessions & Activities" - ;; (url-for :attendees/index) "Attendees" - ;; (url-for :profile/index) "Profile & Settings" - (url-for :session/new) "Create Activity" - (url-for :contacts/index) "Contacts"}] - [:li [:a {:href href - :on-click "document.body.classList.toggle('menu-open')"} - caption]]) - [:li [:a {:href (url-for :contact/qr) - :hx-target "#modal" - :on-click "document.body.classList.toggle('menu-open')"} - "Add Contact"]]]])) + + [:li + [:a + {:href (url-for :sessions/index), :on-click "document.body.classList.toggle('menu-open')"} + "Sessions & Activities"]] + [:li + [:a + {:href (url-for :profile/edit), :on-click "document.body.classList.toggle('menu-open')"} + [graphics/cog-icon] "Profile & Settings"]] + [:li + [:a + {:href (url-for :contacts/index), :on-click "document.body.classList.toggle('menu-open')"} + "Contacts"]] + [:li + [:a {:href (url-for :contact/qr) + :hx-target "#modal" + :on-click "document.body.classList.toggle('menu-open')"} + [graphics/scan-icon] "Add Contact"]] + [:li + [:a + {:href (url-for :session/new), :on-click "document.body.classList.toggle('menu-open')"} + "Create Activity"]] + [:div.bottom + [:p "Proudly made by the " [:a {:href "https://gaiwan.co" :target "_blank"} "Gaiwan Team"] " and contributors."] + [:p [:a {:href "https://github.com/GaiwanTeam/compass" :target "_blank"} [graphics/github-icon] "GaiwanTeam/compass"]]] + ]])) (o/defrules toggle-menu-button) diff --git a/src/co/gaiwan/compass/html/profiles.clj b/src/co/gaiwan/compass/html/profiles.clj index d4dbcbb..757da03 100644 --- a/src/co/gaiwan/compass/html/profiles.clj +++ b/src/co/gaiwan/compass/html/profiles.clj @@ -11,14 +11,10 @@ ;; UI of profile detail -(o/defstyled edit-profile-btn :button +(o/defstyled edit-profile-btn :a.btn ([user] [:<> - {:hx-get (url-for :profile/edit) - :hx-select "#form" - :hx-target "#detail" - :hx-swap "outerHTML"} - "Edit Profile"])) + {:href (url-for :profile/edit)} "Edit Profile"])) (o/defstyled profile-detail :div#detail [c/image-frame :w-100px {t/--arc-thickness "7%"}] @@ -52,36 +48,51 @@ [:div#private-name-block]))) (o/defstyled row :tr.link-row - ([link {:keys [row-index] :as params}] + ([{:keys [variant] :as link}] [:<> [:td - ;; (pr-str link) (when (:db/id link) - [:input {:type "hidden" :name (str "link-id-" row-index) :value (:db/id link)}]) + [:input {:type "hidden" :name (str variant "-link-id[]") :value (:db/id link)}]) (let [link-type (:profile-link/type link)] - [:select {:name (str "link-type-" row-index)} + [:select {:name (str variant "-link-type[]")} [:option {:value "email" :selected (= link-type "email")} "Email"] - [:option {:value "twitter" :selected (= link-type "twitter")} "Twitter"] [:option {:value "mastodon" :selected (= link-type "mastodon")} "Mastodon"] [:option {:value "linkedin" :selected (= link-type "linkedin")} "LinkedIn"] [:option {:value "personal-site" :selected (= link-type "personal-site")} "Personal Site"] [:option {:value "other" :selected (= link-type "other")} "Other"]])] [:td - [:input (cond-> {:name (str "link-ref-" row-index) :type "text" :required true - :min-length 2} - (:db/id link) - (assoc :value (:profile-link/href link)))]] - [:td - [:input {:name (str "public-" row-index) :type "checkbox" - :checked (:public-link link)}]] - [:td - [:input {:name (str "private-" row-index) :type "checkbox" - :checked (:private-link link)}]]])) + [:input + {:type "text" + :name (str variant "-link-ref[]") + :value (str (:profile-link/href link))}]]])) + +(def always-show ["email" "mastodon"]) (o/defstyled links-table :div - ([link {:keys [row-index] :as params}] - [:table - [row link params]])) + ([links {:keys [caption variant]}] + (let [link-map (into {} (map (juxt :profile-link/type :profile-link/href)) links) + link-vals (concat + (for [t always-show] + [t (get link-map t)]) + (apply dissoc link-map always-show))] + [:<> + [:template + [row {:profile-link/type "other" + :profile-link/href "" + :variant variant}]] + [:table + [:thead + [:tr + [:th {:colspan 2} caption]]] + [:tbody + (for [[t h] link-vals] + [row {:profile-link/type t + :profile-link/href h + :variant variant}])]] + [:input#add-link + {:value "+ Add Link" + :type "button" + :on-click "let form = this.parentElement; form.querySelector('tbody').append(form.querySelector('template').content.cloneNode(true))"}]]))) (o/defstyled profile-form :div#form [c/image-frame :w-100px {t/--arc-thickness "7%"}] @@ -98,7 +109,15 @@ :flex :gap-3]] [:table :w-full] + [:.contact-card + :shadow-3 + :my-6 + {:background-color t/--surface-2 + :padding t/--size-3 + :border-radius t/--size-2} + [#{:textarea "input[type='text']"} {:background-color t/--surface-3}]] ([user] + (def user user) [:<> [:h2 "Edit Profile"] [:form {:method "POST" :action "/profile/save" :enctype "multipart/form-data"} @@ -107,63 +126,43 @@ [:input {:id "hidding" :name "hidden?" :type "checkbox" :checked (:public-profile/hidden? user)}] "Hide profile from public listings?"] - [:label {:for "name"} "Name (public)"] + [:label {:for "name"} "Display Name"] [:input {:id "name" :name "name_public" :type "text" - :required true :min-length 2 + :required true :value (:public-profile/name user)}] - [:label {:for "show-another-name"} - [:input {:id "show-another-name" :name "private-name-switch" :type "checkbox" - :hx-get (url-for :profile/private-name) - :hx-target "#private-name-block" - :hx-select "#private-name-block" - :hx-trigger "change" - :hx-swap "outerHTML"}] - "Show different name to confidantes?"] - [:div.input-block {:id "private-name-block"}] - [:div - (when user - [c/image-frame {:profile/image (user/avatar-css-value user)}]) - [:label {:for "image"} "Avatar"] - [:input {:id "image" :name "image" :type "file" :accept "image/png, image/jpeg"}]] [:div [:label {:for "bio_public"} - "Bio (public, markdown)"] + "Bio (accepts markdown)"] [:textarea {:id "bio_public" :name "bio_public"} (when (:public-profile/bio user) (:public-profile/bio user))]] - [:div - [:label {:for "bio_private"} - "Bio (confidential, markdown)"] - [:textarea {:id "bio_private" :name "bio_private"} - (when (:private-profile/bio user) - (:private-profile/bio user))]] - [:div - [:table - [:thead - [:tr - [:th {:colspan 2} "Links"] - [:th "public"] - [:th "confidential"]]] - [:tbody#links-block - (let [links (map (fn [link] - (cond-> link - (:public-profile/_links link) - (assoc :public-link true) - (:private-profile/_links link) - (assoc :private-link true))) - (queries/all-links (:db/id user)))] - (map-indexed - (fn [idx itm] - [row itm {:row-index idx}]) links))]] - [:input#rows-count {:type "hidden" :name "rows-count" :value (count (queries/all-links (:db/id user)))}] - [:input#add-link {:type "button" :value "Add Links" - :hx-get (url-for :profile/add-link) - :hx-target "#links-block" - :hx-select ".link-row" - :hx-trigger "click" - :hx-swap "beforeend"}]] - [:input {:type "submit" :value "Save"}]] + (when user + [c/image-frame {:profile/image (user/avatar-css-value user)}]) + [:label {:for "image"} "Avatar"] + [:input {:id "image" :name "image" :type "file" :accept "image/png, image/jpeg"}]] + [links-table (:public-profile/links user) + {:variant "public" + :caption "Public Profile Links"}] + + [:div.contact-card + [:h3 "Contact Card"] + [:p.info "This information is only shown to people you add as a contact."] + [:label {:for "name"} "Name"] + [:input {:id "name" :name "name_private" :type "text" + :value (:private-profile/name user)}] + [:div + [:label {:for "bio_private"} + "Private Bio (accepts markdown)"] + [:textarea {:id "bio_private" :name "bio_private"} + (when (:private-profile/bio user) + (:private-profile/bio user))]] + [links-table (:private-profile/links user) + {:variant "private" + :caption "Links Visible to Contacts"}] + ] + + [:input {:type "submit" :value "Save Profile"}]] [:script "document.getElementById('add-link').addEventListener('htmx:configRequest', function(evt) { const url = new URL(evt.detail.path, window.location.origin); diff --git a/src/co/gaiwan/compass/routes/profiles.clj b/src/co/gaiwan/compass/routes/profiles.clj index c9d76e1..8df7399 100644 --- a/src/co/gaiwan/compass/routes/profiles.clj +++ b/src/co/gaiwan/compass/routes/profiles.clj @@ -14,9 +14,12 @@ [co.gaiwan.compass.model.attendees :as attendees] [ring.util.response :as ring-response])) -(defn GET-profile [req] - {:html/body [h/profile-detail - (:identity req)]}) +(defn GET-profile [{:keys [params] :as req}] + {:html/body + [h/profile-detail + (if-let [profile-id (:profile-id params)] + (db/entity profile-id) + (:identity req))]}) (defn GET-profile-form [req] {:html/body [h/profile-form @@ -81,6 +84,30 @@ (conj [:db/retract user-id :public-profile/links link-id-val]) (and link-id-val (nil? (public-profile-links link-id-val)) public-link-val) (conj [:db/add user-id :public-profile/links link-id-val])))) +;; (params->profile-data params) +;; (parse-link-data params "public") + +(defn parse-link-data [params variant] + (map vector + (get params (keyword (str variant "-link-type"))) + (get params (keyword (str variant "-link-ref")))) ) + +(defn reconcile-links [user-id variant old-links new-links] + (let [existing-pairs (map (juxt :profile-link/type :profile-link/href) old-links) + del (remove (set new-links) existing-pairs) + add (remove (set existing-pairs) new-links)] + (concat + (for [[k v] del + :when (not (str/blank? v)) + :let [id (some #(when (= [k v] ((juxt :profile-link/type :profile-link/href) %)) + (:db/id %)) old-links)] + :when id] + [:db/retractEntity id]) + (for [[k v] add + :when (not (str/blank? v))] + {(keyword (str variant "-profile/_links")) user-id + :profile-link/type k + :profile-link/href v})))) (defn params->profile-data [{:keys [user-id hidden? @@ -89,26 +116,41 @@ bio_private name_private rows-count image] :as params}] - (tap> params) - (let [user-id (parse-long user-id)] - (cond-> (into [{:db/id user-id - :public-profile/bio bio_public - :public-profile/name name_public - :public-profile/hidden? (= "on" hidden?) - :private-profile/bio bio_private}] - (mapcat #(index->link-data params %)) - (range (parse-long rows-count))) + (let [user-id (parse-long user-id) + user (db/entity user-id) + public-links (reconcile-links + user-id + "public" + (:public-profile/links user) + (parse-link-data params "public")) + private-links (reconcile-links + user-id + "private" + (:private-profile/links user) + (parse-link-data params "private"))] + (cond-> (concat + [{:db/id user-id + :public-profile/bio bio_public + :public-profile/name name_public + :private-profile/bio bio_private}] + public-links + private-links) image (conj [:db/add user-id :public-profile/avatar-url (assets/add-to-content-addressed-storage (:content-type image) (:tempfile image))]) - (and (= "on" private-name-switch) - (not (str/blank? name_private))) - (conj [:db/add user-id :private-profile/name name_private]) + (= "on" private-name-switch) + (conj [:db/add user-id :public-profile/hidden? true]) + + (not= "on" private-name-switch) + (conj [:db/retract user-id :public-profile/hidden? true]) - (or (not= "on" private-name-switch) - (str/blank? name_private)) - (conj [:db/retract user-id :private-profile/name])))) + (and (str/blank? name_private) (:private-profile/name user)) + (conj [:db/retract user-id :private-profile/name (:private-profile/name user)]) + + (not (str/blank? name_private)) + (conj [:db/add user-id :private-profile/name name_private]) + ))) (defn POST-save-profile "Save profile to DB @@ -149,4 +191,11 @@ ["/uploads/:filename" {:middleware [[response/wrap-requires-auth]] :get {:handler file-handler}}] - ]) + ["/user/:profile-id" + {:name :profile/show + :middleware [[response/wrap-requires-auth]] + :get {:handler GET-profile}}] + ["/me" + {:name :profile/me + :middleware [[response/wrap-requires-auth]] + :get {:handler GET-profile}}]])