From 6fa0c5ceaa81a3d4e3b6a171500a2f0982f8787f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marina=20L=C3=B3pez?= Date: Wed, 25 Mar 2026 11:08:07 +0100 Subject: [PATCH] :sparkles: Add organization avatar --- backend/src/app/nitrate.clj | 4 +- .../app/main/ui/components/org_avatar.cljs | 47 +++++++++++++++ .../app/main/ui/components/org_avatar.scss | 58 +++++++++++++++++++ .../src/app/main/ui/dashboard/sidebar.cljs | 14 ++--- 4 files changed, 112 insertions(+), 11 deletions(-) create mode 100644 frontend/src/app/main/ui/components/org_avatar.cljs create mode 100644 frontend/src/app/main/ui/components/org_avatar.scss diff --git a/backend/src/app/nitrate.clj b/backend/src/app/nitrate.clj index 32e2b7ce5e..ba052d4477 100644 --- a/backend/src/app/nitrate.clj +++ b/backend/src/app/nitrate.clj @@ -98,7 +98,8 @@ [:name ::sm/text] [:slug ::sm/text] [:is-your-penpot :boolean] - [:owner-id ::sm/uuid]]) + [:owner-id ::sm/uuid] + [:avatar-bg-url [::sm/text]]]) (def ^:private schema:team [:map @@ -261,6 +262,7 @@ :organization-name (:name org) :organization-slug (:slug org) :organization-owner-id (:owner-id org) + :organization-avatar-bg-url (:avatar-bg-url org) :is-default (or (:is-default team) (true? (:is-your-penpot org)))) team)) (catch Throwable cause diff --git a/frontend/src/app/main/ui/components/org_avatar.cljs b/frontend/src/app/main/ui/components/org_avatar.cljs new file mode 100644 index 0000000000..45095ec770 --- /dev/null +++ b/frontend/src/app/main/ui/components/org_avatar.cljs @@ -0,0 +1,47 @@ +;; 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 http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) KALEIDOS INC + +(ns app.main.ui.components.org-avatar + (:require-macros [app.main.style :as stl]) + (:require + [cuerdas.core :as str] + [rumext.v2 :as mf])) + +(defn- get-org-initials + [name] + (->> (str/split (str/trim (or name "")) #"\s+") + (keep #(first (re-seq #"[a-zA-Z]" %))) + (take 2) + (map str/upper) + (apply str))) + +(mf/defc org-avatar* + {::mf/props :obj} + [{:keys [org size]}] + (let [name (:name org) + custom-photo (:organization-custom-photo org) + avatar-bg (:organization-avatar-bg-url org) + initials (get-org-initials name)] + + (if custom-photo + [:img {:src custom-photo + :class (stl/css-case :org-avatar true + :org-avatar-custom true + :org-avatar-xxxl (= size "xxxl") + :org-avatar-xxl (= size "xxl")) + :alt name}] + [:div {:class (stl/css-case :org-avatar true + :org-avatar-xxxl (= size "xxxl") + :org-avatar-xxl (= size "xxl")) + :aria-hidden "true"} + [:img {:src avatar-bg + :class (stl/css :org-avatar-bg) + :alt ""}] + (when (seq initials) + [:span {:class (stl/css-case :org-avatar-initials true + :size-initials-xxxl (= size "xxxl") + :size-initials-xxl (= size "xxl"))} + initials])]))) diff --git a/frontend/src/app/main/ui/components/org_avatar.scss b/frontend/src/app/main/ui/components/org_avatar.scss new file mode 100644 index 0000000000..ab7ee242a1 --- /dev/null +++ b/frontend/src/app/main/ui/components/org_avatar.scss @@ -0,0 +1,58 @@ +// 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 http://mozilla.org/MPL/2.0/. +// +// Copyright (c) KALEIDOS INC + +@use "ds/typography.scss" as t; +@use "ds/colors.scss" as *; + +.org-avatar { + position: relative; + border-radius: 50%; + overflow: hidden; + flex-shrink: 0; +} + +.org-avatar-custom { + object-fit: cover; +} + +.org-avatar-xxxl { + width: var(--sp-xxxl); + height: var(--sp-xxxl); +} + +.org-avatar-xxl { + width: var(--sp-xxl); + height: var(--sp-xxl); +} + +.org-avatar-bg { + position: absolute; + inset: 0; + width: 100%; + height: 100%; + object-fit: cover; +} + +.org-avatar-initials { + display: flex; + justify-content: center; + align-items: center; + position: absolute; + inset: 0; + color: #{$gray-950}; +} + +.size-initials-xxl { + @include t.use-typography("headline-small"); + + font-weight: 600; +} + +.size-initials-xxxl { + @include t.use-typography("headline-medium"); + + font-weight: 600; +} diff --git a/frontend/src/app/main/ui/dashboard/sidebar.cljs b/frontend/src/app/main/ui/dashboard/sidebar.cljs index 4b9a1aaaa4..f944e07e94 100644 --- a/frontend/src/app/main/ui/dashboard/sidebar.cljs +++ b/frontend/src/app/main/ui/dashboard/sidebar.cljs @@ -25,6 +25,7 @@ [app.main.ui.components.dropdown-menu :refer [dropdown-menu* dropdown-menu-item*]] [app.main.ui.components.link :refer [link]] + [app.main.ui.components.org-avatar :refer [org-avatar*]] [app.main.ui.dashboard.comments :refer [comments-icon* comments-section]] [app.main.ui.dashboard.inline-edition :refer [inline-edition]] [app.main.ui.dashboard.project-menu :refer [project-menu*]] @@ -346,10 +347,7 @@ :data-value (:id org-item) :class (stl/css :org-dropdown-item) :key (str (:id org-item))} - ;; TODO org pictures - [:img {:src (cf/resolve-team-photo-url org-item) - :class (stl/css :team-picture) - :alt (:name org-item)}] + [:> org-avatar* {:org org-item :size "xxl"}] [:span {:class (stl/css :team-text) :title (:name org-item)} (:name org-item)] (when (= (:id org-item) (:id organization)) @@ -580,7 +578,7 @@ (defn- team->org [team] - (assoc (dm/select-keys team [:id :organization-id :organization-slug :organization-owner-id]) + (assoc (dm/select-keys team [:id :organization-id :organization-slug :organization-owner-id :organization-avatar-bg-url]) :name (:organization-name team))) (mf/defc sidebar-org-switch* @@ -659,11 +657,7 @@ [:span {:class (stl/css :team-text)} "Penpot"]] [:* - [:span {:class (stl/css :nitrate-penpot-icon)} - ;; TODO org pictures - [:img {:src (cf/resolve-team-photo-url current-org) - :class (stl/css :team-picture) - :alt (:name current-org)}]] + [:> org-avatar* {:org current-org :size "xxxl"}] [:span {:class (stl/css :team-text)} (:name current-org)]])] arrow-icon]