@planningcenter/organization-avatars 1.6.2 → 1.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -2,15 +2,42 @@
2
2
 
3
3
  This package provides an `OrganizationAvatars` React component which encapsulates displaying and updating an organization's avatar.
4
4
 
5
- See [the component's type signature](https://github.com/planningcenter/organization-avatars/blob/main/src/types.ts) for up-to-date props.
6
-
7
5
  ## Usage
8
6
 
9
- If no `darkModeAvatarUrl` is provided, the `avatarUrl` will be rendered with dark styles in the dark mode avatar's UI slot.
7
+ ```tsx
8
+ import { OrganizationAvatars } from "@planningcenter/organization-avatars"
9
+ import "@planningcenter/organization-avatars/style.css"
10
+
11
+ <OrganizationAvatars
12
+ orgName="Demo Church"
13
+ avatarUrl="https://example.com/avatar.png"
14
+ darkModeAvatarUrl="https://example.com/avatar-dark.png"
15
+ showDarkModeAvatar
16
+ onAvatarUpdate={(mode, newUrl) => console.log(mode, newUrl)}
17
+ />
18
+ ```
19
+
20
+ To see a working example, see the [`/organization` page in Accounts](https://accounts.planningcenteronline.com/organization). The `OrganizationAvatars` component is mounted in [the `_church_information` partial](https://github.com/planningcenter/accounts/blob/main/app/views/organization/show/_church_information.html.erb).
21
+
22
+ ## Props
23
+
24
+ | Prop | Type | Default | Description |
25
+ | --- | --- | --- | --- |
26
+ | `orgName` | `string` | **required** | The organization name, used for avatar alt text. |
27
+ | `avatarUrl` | `string` | `undefined` | URL of the current light mode avatar. When no `darkModeAvatarUrl` is provided, this is also used for the dark mode slot. |
28
+ | `darkModeAvatarUrl` | `string` | `undefined` | URL of the current dark mode avatar. Falls back to `avatarUrl` if not set. |
29
+ | `showDarkModeAvatar` | `boolean` | `false` | Whether to render the dark mode avatar UI alongside the light mode avatar. Intended to be controlled by a feature flag. |
30
+ | `onAvatarUpdate` | `(mode: AvatarMode, newUrl: string) => void` | `undefined` | Callback fired after a successful upload or delete. `mode` is `"avatar"` or `"dark_mode_avatar"`. `newUrl` is the new URL (or `""` on delete). |
31
+ | `pcoEnv` | `Environment` | `undefined` | Planning Center environment override (`"production"`, `"staging"`, `"development"`, `"test"`, `"prototype"`). When omitted, the environment is inferred automatically. |
32
+ | `readOnly` | `boolean` | `false` | Disables the edit button, preventing uploads and deletes. |
33
+
34
+ ### Types
10
35
 
11
- The `showDarkModeAvatar` prop gates the rendering of the dark mode avatar UI. It is intended to be controlled by a feature flag, and will eventually be removed when dark mode avatars have been rolled out.
36
+ ```ts
37
+ type AvatarMode = "avatar" | "dark_mode_avatar"
12
38
 
13
- To see an example of how this package can be used, see the [`/organization` page in Accounts](https://accounts.planningcenteronline.com/organization). The `OrganizationAvatars` component is mounted in [the `_church_information` partial](https://github.com/planningcenter/accounts/blob/main/app/views/organization/show/_church_information.html.erb).
39
+ type Environment = "production" | "staging" | "development" | "test" | "prototype"
40
+ ```
14
41
 
15
42
  ## Development
16
43
 
package/dist/index.cjs CHANGED
@@ -1 +1 @@
1
- "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const e=require("react/jsx-runtime"),l=require("react"),B=require("react-dom"),M=require("@planningcenter/url"),C=require("@planningcenter/tapestry"),j=require("@planningcenter/icons/paths/general"),O=require("@planningcenter/icons/paths/services"),P=require("react-dropzone");function z(a){var o,t,r="";if(typeof a=="string"||typeof a=="number")r+=a;else if(typeof a=="object")if(Array.isArray(a)){var c=a.length;for(o=0;o<c;o++)a[o]&&(t=z(a[o]))&&(r&&(r+=" "),r+=t)}else for(t in a)a[t]&&(r&&(r+=" "),r+=t);return r}function T(){for(var a,o,t=0,r="",c=arguments.length;t<c;t++)(a=arguments[t])&&(o=z(a))&&(r&&(r+=" "),r+=o);return r}function $(){const a=document.querySelector('meta[name="csrf-token"]');if(!a||!a.content)throw new Error('CSRF token not found. Ensure your Rails application includes <meta name="csrf-token" content="..."> in the document head.');return a.content}function L(a){const o=a.split("");return[o.shift()?.toUpperCase(),o.join("")].join("")}const I=a=>a==="dark_mode_avatar"?"dark":"light";function S({avatarUrl:a,organization:o,mode:t,pcoEnv:r,readOnly:c,onAvatarUpdate:v}){const[s,p]=l.useState(null),[d,x]=l.useState(!1),[u,f]=l.useState(!1),[_,h]=l.useState(""),i=l.useId(),k=l.useRef(null),b=I(t),F=g=>{const[n]=g;n&&p(Object.assign(n,{preview:URL.createObjectURL(n)}))};l.useEffect(()=>()=>{s?.preview&&(URL.revokeObjectURL(s.preview),h(""))},[s]);const N=async g=>{const n=await fetch(M.pcoApiUrl("accounts",{env:r}),{method:"PATCH",headers:{"Content-Type":"application/json","X-CSRF-Token":$(),Accept:"application/json"},body:JSON.stringify({data:{attributes:g}})}),m=await n.json();if(!n.ok)throw m;return m},R=async g=>{if(g.preventDefault(),!(!s||d||u)){x(!0);try{const n=new FileReader,m=await new Promise((y,w)=>{n.onloadend=()=>y(n.result),n.onerror=w,n.readAsDataURL(s)}),A=await N({[t]:m,[`${t}_cache`]:""});p(null),v(t,A.data.attributes[t].url),k.current?.close()}catch{h("We were unable to save your avatar.")}finally{x(!1)}}},U=async()=>{if(!(d||u)){f(!0);try{await N({[`remove_${t}`]:"1"}),v(t,""),k.current?.close()}catch{h("We were unable to delete your avatar.")}finally{f(!1)}}},q=()=>{p(null),h("")};return e.jsxs(e.Fragment,{children:[e.jsxs("div",{className:"pco-org-avatar",children:[e.jsx("button",{type:"button",commandfor:i,command:"show-modal",disabled:c,className:`pco-org-avatar__button pco-org-avatar__button--${b}`,children:a?e.jsxs(e.Fragment,{children:[e.jsx("img",{src:a,alt:`Church logo for ${o}`}),e.jsx("div",{className:"tds-btn tds-btn--interaction tds-btn--icononly",children:e.jsx("svg",{width:"14",height:"14",viewBox:"0 0 16 16",fill:"currentColor","aria-hidden":"true",children:e.jsx("path",{d:j.pencil})})})]}):e.jsxs(e.Fragment,{children:[e.jsx("svg",{width:"48",height:"48",viewBox:"0 0 16 16",fill:"currentColor","aria-hidden":"true",children:e.jsx("path",{d:O.image})}),e.jsx("div",{className:"tds-btn tds-btn--interaction tds-btn--icononly",children:e.jsx("svg",{width:"14",height:"14",viewBox:"0 0 16 16",fill:"currentColor","aria-hidden":"true",children:e.jsx("path",{d:j.pencil})})})]})}),e.jsxs("div",{className:"pco-org-avatar__label",children:[L(b)," mode"]})]}),B.createPortal(e.jsx("dialog",{id:i,ref:k,className:"pco-org-avatar__dialog",onClose:q,children:e.jsxs("form",{onSubmit:R,children:[e.jsxs("div",{className:"pco-org-avatar__dialog-header",children:[e.jsxs("h1",{className:"pco-org-avatar__dialog-title",children:["Church logo — ",b," mode"]}),e.jsx("button",{type:"button",commandfor:i,command:"close",className:"pco-org-avatar__dialog-close","aria-label":"Close modal",children:e.jsx("svg",{width:"16",height:"16",viewBox:"0 0 16 16",fill:"currentColor","aria-hidden":"true",children:e.jsx("path",{d:j.smallX})})})]}),e.jsxs("div",{className:"pco-org-avatar__dialog-body",children:[_&&e.jsxs("div",{role:"alert",className:"pco-org-avatar__dialog-alert",children:[e.jsx("svg",{width:"16",height:"16",viewBox:"0 0 16 16",className:"symbol","aria-hidden":"true",children:e.jsx("path",{d:j.exclamationTriangle})}),e.jsx("p",{children:_})]}),e.jsx(P,{accept:{"image/*":[".jpg",".jpeg",".png"]},multiple:!1,onDropAccepted:F,children:({getRootProps:g,getInputProps:n,isFocused:m,isDragAccept:A,isDragReject:y,isDragActive:w})=>{const D=T("pco-org-avatar__dialog-dropzone",`pco-org-avatar__dialog-dropzone--${b}`,{"pco-org-avatar__dialog-dropzone--image":s||a,"pco-org-avatar__dialog-dropzone--focused":m,"pco-org-avatar__dialog-dropzone--accepted":A,"pco-org-avatar__dialog-dropzone--rejected":w&&y});return e.jsxs("div",{...g({className:D}),children:[e.jsx("input",{...n()}),s?e.jsx("img",{src:s.preview,alt:`Church logo for ${o}`}):a?e.jsx("img",{src:a,alt:`Church logo for ${o}`}):e.jsxs(e.Fragment,{children:[e.jsx("svg",{width:"64",height:"64",viewBox:"0 0 16 16","aria-hidden":"true",children:e.jsx("path",{d:j.toCloudArrow})}),w&&y?e.jsxs(e.Fragment,{children:[e.jsx("h2",{children:"Wrong file type"}),e.jsx("p",{children:"Please upload a PNG or JPG file."})]}):e.jsxs(e.Fragment,{children:[e.jsx("h2",{children:"Add church logo"}),e.jsx("p",{children:"Use a high-quality image (up to 20mb) with a transparent background."})]})]})]})}})]}),e.jsxs("div",{className:"pco-org-avatar__dialog-actions",children:[a&&e.jsx(C.Button,{label:"Delete logo",kind:"delete",onClick:U,loading:u,disabled:u||d,className:"pco-org-avatar__dialog-delete"}),e.jsx(C.Button,{type:"button",label:"Cancel",commandfor:i,command:"close",disabled:d||u}),e.jsx(C.Button,{type:"submit",label:"Update logo",kind:"primary",loading:d,disabled:!s||d||u})]})]})}),document.body)]})}function E({avatarUrl:a,darkModeAvatarUrl:o,orgName:t,onAvatarUpdate:r,pcoEnv:c,readOnly:v,showDarkModeAvatar:s=!1}){const[p,d]=l.useState(a),[x,u]=l.useState(o),f=(h,i)=>{h==="avatar"?(d(i),r?.("avatar",i)):h==="dark_mode_avatar"&&(u(i),r?.("dark_mode_avatar",i))},_=x||p;return e.jsxs("div",{className:"pco-org-avatars",children:[e.jsx(S,{avatarUrl:p,organization:t,mode:"avatar",pcoEnv:c,readOnly:v,onAvatarUpdate:f}),s&&e.jsx(S,{avatarUrl:_,organization:t,mode:"dark_mode_avatar",pcoEnv:c,readOnly:v,onAvatarUpdate:f})]})}exports.OrganizationAvatars=E;
1
+ "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const e=require("react/jsx-runtime"),c=require("react"),T=require("react-dom"),$=require("@planningcenter/url"),b=require("@planningcenter/tapestry"),I=require("@planningcenter/sweetest-alert"),_=require("@planningcenter/icons/paths/general"),L=require("@planningcenter/icons/paths/services"),E=require("react-dropzone");function B(a){var r,t,o="";if(typeof a=="string"||typeof a=="number")o+=a;else if(typeof a=="object")if(Array.isArray(a)){var d=a.length;for(r=0;r<d;r++)a[r]&&(t=B(a[r]))&&(o&&(o+=" "),o+=t)}else for(t in a)a[t]&&(o&&(o+=" "),o+=t);return o}function W(){for(var a,r,t=0,o="",d=arguments.length;t<d;t++)(a=arguments[t])&&(r=B(a))&&(o&&(o+=" "),o+=r);return o}function G(){const a=document.querySelector('meta[name="csrf-token"]');if(!a||!a.content)throw new Error('CSRF token not found. Ensure your Rails application includes <meta name="csrf-token" content="..."> in the document head.');return a.content}function J(a){const r=a.split("");return[r.shift()?.toUpperCase(),r.join("")].join("")}const X=a=>a==="dark_mode_avatar"?"dark":"light",z=({open:a})=>e.jsxs("h2",{className:"pco-org-avatar__instructions",children:["Drag & drop your logo file to upload, or"," ",e.jsx(b.Button,{label:"browse",kind:"inline-text",onClick:r=>{r.stopPropagation(),a()}})]});function F({avatarUrl:a,organization:r,mode:t,pcoEnv:o,readOnly:d,onAvatarUpdate:x,fallbackAvatarUrl:A}){const[n,f]=c.useState(null),[l,y]=c.useState(!1),[i,p]=c.useState(!1),[u,w]=c.useState(""),q=c.useId(),v=c.useRef(null),g=X(t),m=a||A,D=h=>{const[s]=h;s&&f(Object.assign(s,{preview:URL.createObjectURL(s)}))};c.useEffect(()=>()=>{n?.preview&&(URL.revokeObjectURL(n.preview),w(""))},[n]);const S=async h=>{const s=await fetch($.pcoApiUrl("accounts",{env:o}),{method:"PATCH",headers:{"Content-Type":"application/json","X-CSRF-Token":G(),Accept:"application/json"},body:JSON.stringify({data:{attributes:h}})}),j=await s.json();if(!s.ok)throw j;return j},M=async h=>{if(h.preventDefault(),!(!n||l||i)){y(!0);try{const s=new FileReader,j=await new Promise((k,C)=>{s.onloadend=()=>k(s.result),s.onerror=C,s.readAsDataURL(n)}),N=await S({[t]:j,[`${t}_cache`]:""});f(null),x(t,N.data.attributes[t].url),v.current?.close()}catch{w("We were unable to save your avatar.")}finally{y(!1)}}},U=async()=>{l||i||(p(!0),I.SweetestAlert({type:"danger",title:"Remove this logo?",content:`Removing this logo will mean your light mode logo appear in ${g} mode. This cannot be undone.`,confirmButton:"Remove logo",onConfirm:async()=>{try{await S({[`remove_${t}`]:"1"}),x(t,""),v.current?.close()}catch{w("We were unable to delete your avatar.")}finally{p(!1)}},onCancel:()=>{p(!1)}}))},O=()=>{f(null),w("")};return e.jsxs(e.Fragment,{children:[e.jsxs("div",{className:"pco-org-avatar",children:[e.jsx("button",{type:"button",onClick:()=>v.current?.showModal(),disabled:d,className:`pco-org-avatar__button pco-org-avatar__button--${g}`,children:m?e.jsxs(e.Fragment,{children:[e.jsx("img",{src:m,alt:`Church logo for ${r}`}),e.jsx("div",{className:"tds-btn tds-btn--interaction tds-btn--icononly",children:e.jsx("svg",{width:"14",height:"14",viewBox:"0 0 16 16",fill:"currentColor","aria-hidden":"true",children:e.jsx("path",{d:_.pencil})})})]}):e.jsxs(e.Fragment,{children:[e.jsx("svg",{width:"48",height:"48",viewBox:"0 0 16 16",fill:"currentColor","aria-hidden":"true",children:e.jsx("path",{d:L.image})}),e.jsx("div",{className:"tds-btn tds-btn--interaction tds-btn--icononly",children:e.jsx("svg",{width:"14",height:"14",viewBox:"0 0 16 16",fill:"currentColor","aria-hidden":"true",children:e.jsx("path",{d:_.pencil})})})]})}),e.jsxs("div",{className:"pco-org-avatar__label",children:[J(g)," mode"]})]}),T.createPortal(e.jsx("dialog",{id:q,ref:v,className:"pco-org-avatar__dialog",onClose:O,children:e.jsxs("form",{onSubmit:M,children:[e.jsxs("div",{className:"pco-org-avatar__dialog-header",children:[e.jsxs("h1",{className:"pco-org-avatar__dialog-title",children:["Church logo — ",g," mode"]}),e.jsx("button",{type:"button",onClick:()=>v.current?.close(),className:"pco-org-avatar__dialog-close","aria-label":"Close modal",children:e.jsx("svg",{width:"16",height:"16",viewBox:"0 0 16 16",fill:"currentColor","aria-hidden":"true",children:e.jsx("path",{d:_.smallX})})})]}),e.jsxs("div",{className:"pco-org-avatar__dialog-body",children:[u&&e.jsxs("div",{role:"alert",className:"pco-org-avatar__dialog-alert",children:[e.jsx("svg",{width:"16",height:"16",viewBox:"0 0 16 16",className:"symbol","aria-hidden":"true",children:e.jsx("path",{d:_.exclamationTriangle})}),e.jsx("p",{children:u})]}),e.jsx(E,{accept:{"image/*":[".jpg",".jpeg",".png"]},multiple:!1,onDropAccepted:D,children:({getRootProps:h,getInputProps:s,isFocused:j,isDragAccept:N,isDragReject:k,isDragActive:C,open:R})=>{const P=W("pco-org-avatar__dialog-dropzone",`pco-org-avatar__dialog-dropzone--${g}`,{"pco-org-avatar__dialog-dropzone--image":n||m,"pco-org-avatar__dialog-dropzone--focused":j,"pco-org-avatar__dialog-dropzone--accepted":N,"pco-org-avatar__dialog-dropzone--rejected":C&&k});return e.jsxs(e.Fragment,{children:[e.jsxs("div",{...h({className:P}),children:[e.jsx("input",{...s()}),n?e.jsx("img",{src:n.preview,alt:`Church logo for ${r}`}):m?e.jsx("img",{src:m,alt:`Church logo for ${r}`}):e.jsxs(e.Fragment,{children:[e.jsx("svg",{width:"64",height:"64",viewBox:"0 0 16 16","aria-hidden":"true",children:e.jsx("path",{d:_.toCloudArrow})}),C&&k?e.jsxs(e.Fragment,{children:[e.jsx("h2",{children:"Wrong file type"}),e.jsx("p",{children:"Please upload a PNG or JPG file."})]}):e.jsxs(e.Fragment,{children:[e.jsx(z,{open:R}),e.jsx("p",{className:"pco-org-avatar__instructions-sub",children:"Use a high-quality image (up to 20mb) with a transparent background."})]})]})]}),(n||m)&&e.jsx(z,{open:R})]})}})]}),e.jsxs("div",{className:"pco-org-avatar__dialog-actions",children:[a&&g==="light"&&e.jsx(b.Button,{label:"Remove logo",kind:"secondary-delete",onClick:U,loading:i,disabled:i||l,className:"pco-org-avatar__dialog-delete"}),a&&g==="dark"&&e.jsx(b.Button,{label:"Use light mode logo",kind:"secondary",onClick:U,loading:i,disabled:i||l,className:"pco-org-avatar__dialog-delete"}),e.jsx(b.Button,{type:"button",label:"Cancel",onClick:()=>v.current?.close(),disabled:l||i}),e.jsx(b.Button,{type:"submit",label:"Update logo",kind:"primary",loading:l,disabled:!n||l||i})]})]})}),document.body)]})}function H({avatarUrl:a,darkModeAvatarUrl:r,orgName:t,onAvatarUpdate:o,pcoEnv:d,readOnly:x,showDarkModeAvatar:A=!1}){const[n,f]=c.useState(a),[l,y]=c.useState(r),i=(p,u)=>{p==="avatar"?(f(u),o?.("avatar",u)):p==="dark_mode_avatar"&&(y(u),o?.("dark_mode_avatar",u))};return e.jsxs("div",{className:"pco-org-avatars",children:[e.jsx(F,{avatarUrl:n,organization:t,mode:"avatar",pcoEnv:d,readOnly:x,onAvatarUpdate:i}),A&&e.jsx(F,{avatarUrl:l,organization:t,mode:"dark_mode_avatar",pcoEnv:d,readOnly:x,onAvatarUpdate:i,fallbackAvatarUrl:n})]})}exports.OrganizationAvatars=H;
package/dist/index.js CHANGED
@@ -1,118 +1,140 @@
1
- import { jsxs as n, Fragment as f, jsx as e } from "react/jsx-runtime";
2
- import { useState as v, useId as O, useRef as P, useEffect as T } from "react";
3
- import { createPortal as L } from "react-dom";
4
- import { pcoApiUrl as I } from "@planningcenter/url";
5
- import { Button as z } from "@planningcenter/tapestry";
6
- import { pencil as x, smallX as E, exclamationTriangle as W, toCloudArrow as q } from "@planningcenter/icons/paths/general";
7
- import { image as G } from "@planningcenter/icons/paths/services";
8
- import J from "react-dropzone";
9
- function R(a) {
1
+ import { jsxs as n, Fragment as u, jsx as a } from "react/jsx-runtime";
2
+ import { useState as b, useId as L, useRef as E, useEffect as W } from "react";
3
+ import { createPortal as q } from "react-dom";
4
+ import { pcoApiUrl as G } from "@planningcenter/url";
5
+ import { Button as k } from "@planningcenter/tapestry";
6
+ import { SweetestAlert as J } from "@planningcenter/sweetest-alert";
7
+ import { pencil as B, smallX as X, exclamationTriangle as H, toCloudArrow as K } from "@planningcenter/icons/paths/general";
8
+ import { image as Q } from "@planningcenter/icons/paths/services";
9
+ import V from "react-dropzone";
10
+ function M(e) {
10
11
  var o, r, t = "";
11
- if (typeof a == "string" || typeof a == "number") t += a;
12
- else if (typeof a == "object") if (Array.isArray(a)) {
13
- var d = a.length;
14
- for (o = 0; o < d; o++) a[o] && (r = R(a[o])) && (t && (t += " "), t += r);
15
- } else for (r in a) a[r] && (t && (t += " "), t += r);
12
+ if (typeof e == "string" || typeof e == "number") t += e;
13
+ else if (typeof e == "object") if (Array.isArray(e)) {
14
+ var s = e.length;
15
+ for (o = 0; o < s; o++) e[o] && (r = M(e[o])) && (t && (t += " "), t += r);
16
+ } else for (r in e) e[r] && (t && (t += " "), t += r);
16
17
  return t;
17
18
  }
18
- function X() {
19
- for (var a, o, r = 0, t = "", d = arguments.length; r < d; r++) (a = arguments[r]) && (o = R(a)) && (t && (t += " "), t += o);
19
+ function Y() {
20
+ for (var e, o, r = 0, t = "", s = arguments.length; r < s; r++) (e = arguments[r]) && (o = M(e)) && (t && (t += " "), t += o);
20
21
  return t;
21
22
  }
22
- function H() {
23
- const a = document.querySelector('meta[name="csrf-token"]');
24
- if (!a || !a.content)
23
+ function Z() {
24
+ const e = document.querySelector('meta[name="csrf-token"]');
25
+ if (!e || !e.content)
25
26
  throw new Error(
26
27
  'CSRF token not found. Ensure your Rails application includes <meta name="csrf-token" content="..."> in the document head.'
27
28
  );
28
- return a.content;
29
+ return e.content;
29
30
  }
30
- function K(a) {
31
- const o = a.split("");
31
+ function aa(e) {
32
+ const o = e.split("");
32
33
  return [o.shift()?.toUpperCase(), o.join("")].join("");
33
34
  }
34
- const Q = (a) => a === "dark_mode_avatar" ? "dark" : "light";
35
- function D({
36
- avatarUrl: a,
35
+ const ea = (e) => e === "dark_mode_avatar" ? "dark" : "light", D = ({ open: e }) => /* @__PURE__ */ n("h2", { className: "pco-org-avatar__instructions", children: [
36
+ "Drag & drop your logo file to upload, or",
37
+ " ",
38
+ /* @__PURE__ */ a(
39
+ k,
40
+ {
41
+ label: "browse",
42
+ kind: "inline-text",
43
+ onClick: (o) => {
44
+ o.stopPropagation(), e();
45
+ }
46
+ }
47
+ )
48
+ ] });
49
+ function F({
50
+ avatarUrl: e,
37
51
  organization: o,
38
52
  mode: r,
39
53
  pcoEnv: t,
40
- readOnly: d,
41
- onAvatarUpdate: _
54
+ readOnly: s,
55
+ onAvatarUpdate: y,
56
+ fallbackAvatarUrl: R
42
57
  }) {
43
- const [l, u] = v(null), [s, y] = v(!1), [h, b] = v(!1), [A, p] = v(""), c = O(), N = P(null), w = Q(r), M = (g) => {
44
- const [i] = g;
45
- i && u(
46
- Object.assign(i, {
47
- preview: URL.createObjectURL(i)
58
+ const [i, w] = b(null), [d, C] = b(!1), [c, m] = b(!1), [g, A] = b(""), $ = L(), v = E(null), h = ea(r), f = e || R, P = (p) => {
59
+ const [l] = p;
60
+ l && w(
61
+ Object.assign(l, {
62
+ preview: URL.createObjectURL(l)
48
63
  })
49
64
  );
50
65
  };
51
- T(() => () => {
52
- l?.preview && (URL.revokeObjectURL(l.preview), p(""));
53
- }, [l]);
54
- const U = async (g) => {
55
- const i = await fetch(I("accounts", { env: t }), {
66
+ W(() => () => {
67
+ i?.preview && (URL.revokeObjectURL(i.preview), A(""));
68
+ }, [i]);
69
+ const z = async (p) => {
70
+ const l = await fetch(G("accounts", { env: t }), {
56
71
  method: "PATCH",
57
72
  headers: {
58
73
  "Content-Type": "application/json",
59
- "X-CSRF-Token": H(),
74
+ "X-CSRF-Token": Z(),
60
75
  Accept: "application/json"
61
76
  },
62
77
  body: JSON.stringify({
63
78
  data: {
64
- attributes: g
79
+ attributes: p
65
80
  }
66
81
  })
67
- }), m = await i.json();
68
- if (!i.ok) throw m;
69
- return m;
70
- }, S = async (g) => {
71
- if (g.preventDefault(), !(!l || s || h)) {
72
- y(!0);
82
+ }), _ = await l.json();
83
+ if (!l.ok) throw _;
84
+ return _;
85
+ }, T = async (p) => {
86
+ if (p.preventDefault(), !(!i || d || c)) {
87
+ C(!0);
73
88
  try {
74
- const i = new FileReader(), m = await new Promise((k, C) => {
75
- i.onloadend = () => k(i.result), i.onerror = C, i.readAsDataURL(l);
76
- }), j = await U({
77
- [r]: m,
89
+ const l = new FileReader(), _ = await new Promise((N, U) => {
90
+ l.onloadend = () => N(l.result), l.onerror = U, l.readAsDataURL(i);
91
+ }), j = await z({
92
+ [r]: _,
78
93
  [`${r}_cache`]: ""
79
94
  });
80
- u(null), _(r, j.data.attributes[r].url), N.current?.close();
95
+ w(null), y(r, j.data.attributes[r].url), v.current?.close();
81
96
  } catch {
82
- p("We were unable to save your avatar.");
97
+ A("We were unable to save your avatar.");
83
98
  } finally {
84
- y(!1);
99
+ C(!1);
85
100
  }
86
101
  }
87
- }, F = async () => {
88
- if (!(s || h)) {
89
- b(!0);
90
- try {
91
- await U({
92
- [`remove_${r}`]: "1"
93
- }), _(r, ""), N.current?.close();
94
- } catch {
95
- p("We were unable to delete your avatar.");
96
- } finally {
97
- b(!1);
102
+ }, x = async () => {
103
+ d || c || (m(!0), J({
104
+ type: "danger",
105
+ title: "Remove this logo?",
106
+ content: `Removing this logo will mean your light mode logo appear in ${h} mode. This cannot be undone.`,
107
+ confirmButton: "Remove logo",
108
+ onConfirm: async () => {
109
+ try {
110
+ await z({
111
+ [`remove_${r}`]: "1"
112
+ }), y(r, ""), v.current?.close();
113
+ } catch {
114
+ A("We were unable to delete your avatar.");
115
+ } finally {
116
+ m(!1);
117
+ }
118
+ },
119
+ onCancel: () => {
120
+ m(!1);
98
121
  }
99
- }
100
- }, B = () => {
101
- u(null), p("");
122
+ }));
123
+ }, O = () => {
124
+ w(null), A("");
102
125
  };
103
- return /* @__PURE__ */ n(f, { children: [
126
+ return /* @__PURE__ */ n(u, { children: [
104
127
  /* @__PURE__ */ n("div", { className: "pco-org-avatar", children: [
105
- /* @__PURE__ */ e(
128
+ /* @__PURE__ */ a(
106
129
  "button",
107
130
  {
108
131
  type: "button",
109
- commandfor: c,
110
- command: "show-modal",
111
- disabled: d,
112
- className: `pco-org-avatar__button pco-org-avatar__button--${w}`,
113
- children: a ? /* @__PURE__ */ n(f, { children: [
114
- /* @__PURE__ */ e("img", { src: a, alt: `Church logo for ${o}` }),
115
- /* @__PURE__ */ e("div", { className: "tds-btn tds-btn--interaction tds-btn--icononly", children: /* @__PURE__ */ e(
132
+ onClick: () => v.current?.showModal(),
133
+ disabled: s,
134
+ className: `pco-org-avatar__button pco-org-avatar__button--${h}`,
135
+ children: f ? /* @__PURE__ */ n(u, { children: [
136
+ /* @__PURE__ */ a("img", { src: f, alt: `Church logo for ${o}` }),
137
+ /* @__PURE__ */ a("div", { className: "tds-btn tds-btn--interaction tds-btn--icononly", children: /* @__PURE__ */ a(
116
138
  "svg",
117
139
  {
118
140
  width: "14",
@@ -120,11 +142,11 @@ function D({
120
142
  viewBox: "0 0 16 16",
121
143
  fill: "currentColor",
122
144
  "aria-hidden": "true",
123
- children: /* @__PURE__ */ e("path", { d: x })
145
+ children: /* @__PURE__ */ a("path", { d: B })
124
146
  }
125
147
  ) })
126
- ] }) : /* @__PURE__ */ n(f, { children: [
127
- /* @__PURE__ */ e(
148
+ ] }) : /* @__PURE__ */ n(u, { children: [
149
+ /* @__PURE__ */ a(
128
150
  "svg",
129
151
  {
130
152
  width: "48",
@@ -132,10 +154,10 @@ function D({
132
154
  viewBox: "0 0 16 16",
133
155
  fill: "currentColor",
134
156
  "aria-hidden": "true",
135
- children: /* @__PURE__ */ e("path", { d: G })
157
+ children: /* @__PURE__ */ a("path", { d: Q })
136
158
  }
137
159
  ),
138
- /* @__PURE__ */ e("div", { className: "tds-btn tds-btn--interaction tds-btn--icononly", children: /* @__PURE__ */ e(
160
+ /* @__PURE__ */ a("div", { className: "tds-btn tds-btn--interaction tds-btn--icononly", children: /* @__PURE__ */ a(
139
161
  "svg",
140
162
  {
141
163
  width: "14",
@@ -143,41 +165,40 @@ function D({
143
165
  viewBox: "0 0 16 16",
144
166
  fill: "currentColor",
145
167
  "aria-hidden": "true",
146
- children: /* @__PURE__ */ e("path", { d: x })
168
+ children: /* @__PURE__ */ a("path", { d: B })
147
169
  }
148
170
  ) })
149
171
  ] })
150
172
  }
151
173
  ),
152
174
  /* @__PURE__ */ n("div", { className: "pco-org-avatar__label", children: [
153
- K(w),
175
+ aa(h),
154
176
  " mode"
155
177
  ] })
156
178
  ] }),
157
- L(
158
- /* @__PURE__ */ e(
179
+ q(
180
+ /* @__PURE__ */ a(
159
181
  "dialog",
160
182
  {
161
- id: c,
162
- ref: N,
183
+ id: $,
184
+ ref: v,
163
185
  className: "pco-org-avatar__dialog",
164
- onClose: B,
165
- children: /* @__PURE__ */ n("form", { onSubmit: S, children: [
186
+ onClose: O,
187
+ children: /* @__PURE__ */ n("form", { onSubmit: T, children: [
166
188
  /* @__PURE__ */ n("div", { className: "pco-org-avatar__dialog-header", children: [
167
189
  /* @__PURE__ */ n("h1", { className: "pco-org-avatar__dialog-title", children: [
168
190
  "Church logo — ",
169
- w,
191
+ h,
170
192
  " mode"
171
193
  ] }),
172
- /* @__PURE__ */ e(
194
+ /* @__PURE__ */ a(
173
195
  "button",
174
196
  {
175
197
  type: "button",
176
- commandfor: c,
177
- command: "close",
198
+ onClick: () => v.current?.close(),
178
199
  className: "pco-org-avatar__dialog-close",
179
200
  "aria-label": "Close modal",
180
- children: /* @__PURE__ */ e(
201
+ children: /* @__PURE__ */ a(
181
202
  "svg",
182
203
  {
183
204
  width: "16",
@@ -185,15 +206,15 @@ function D({
185
206
  viewBox: "0 0 16 16",
186
207
  fill: "currentColor",
187
208
  "aria-hidden": "true",
188
- children: /* @__PURE__ */ e("path", { d: E })
209
+ children: /* @__PURE__ */ a("path", { d: X })
189
210
  }
190
211
  )
191
212
  }
192
213
  )
193
214
  ] }),
194
215
  /* @__PURE__ */ n("div", { className: "pco-org-avatar__dialog-body", children: [
195
- A && /* @__PURE__ */ n("div", { role: "alert", className: "pco-org-avatar__dialog-alert", children: [
196
- /* @__PURE__ */ e(
216
+ g && /* @__PURE__ */ n("div", { role: "alert", className: "pco-org-avatar__dialog-alert", children: [
217
+ /* @__PURE__ */ a(
197
218
  "svg",
198
219
  {
199
220
  width: "16",
@@ -201,103 +222,117 @@ function D({
201
222
  viewBox: "0 0 16 16",
202
223
  className: "symbol",
203
224
  "aria-hidden": "true",
204
- children: /* @__PURE__ */ e("path", { d: W })
225
+ children: /* @__PURE__ */ a("path", { d: H })
205
226
  }
206
227
  ),
207
- /* @__PURE__ */ e("p", { children: A })
228
+ /* @__PURE__ */ a("p", { children: g })
208
229
  ] }),
209
- /* @__PURE__ */ e(
210
- J,
230
+ /* @__PURE__ */ a(
231
+ V,
211
232
  {
212
233
  accept: { "image/*": [".jpg", ".jpeg", ".png"] },
213
234
  multiple: !1,
214
- onDropAccepted: M,
235
+ onDropAccepted: P,
215
236
  children: ({
216
- getRootProps: g,
217
- getInputProps: i,
218
- isFocused: m,
237
+ getRootProps: p,
238
+ getInputProps: l,
239
+ isFocused: _,
219
240
  isDragAccept: j,
220
- isDragReject: k,
221
- isDragActive: C
241
+ isDragReject: N,
242
+ isDragActive: U,
243
+ open: S
222
244
  }) => {
223
- const $ = X(
245
+ const I = Y(
224
246
  "pco-org-avatar__dialog-dropzone",
225
- `pco-org-avatar__dialog-dropzone--${w}`,
247
+ `pco-org-avatar__dialog-dropzone--${h}`,
226
248
  {
227
- "pco-org-avatar__dialog-dropzone--image": l || a,
228
- "pco-org-avatar__dialog-dropzone--focused": m,
249
+ "pco-org-avatar__dialog-dropzone--image": i || f,
250
+ "pco-org-avatar__dialog-dropzone--focused": _,
229
251
  "pco-org-avatar__dialog-dropzone--accepted": j,
230
- "pco-org-avatar__dialog-dropzone--rejected": C && k
252
+ "pco-org-avatar__dialog-dropzone--rejected": U && N
231
253
  }
232
254
  );
233
- return /* @__PURE__ */ n("div", { ...g({ className: $ }), children: [
234
- /* @__PURE__ */ e("input", { ...i() }),
235
- l ? /* @__PURE__ */ e(
236
- "img",
237
- {
238
- src: l.preview,
239
- alt: `Church logo for ${o}`
240
- }
241
- ) : a ? /* @__PURE__ */ e(
242
- "img",
243
- {
244
- src: a,
245
- alt: `Church logo for ${o}`
246
- }
247
- ) : /* @__PURE__ */ n(f, { children: [
248
- /* @__PURE__ */ e(
249
- "svg",
255
+ return /* @__PURE__ */ n(u, { children: [
256
+ /* @__PURE__ */ n("div", { ...p({ className: I }), children: [
257
+ /* @__PURE__ */ a("input", { ...l() }),
258
+ i ? /* @__PURE__ */ a(
259
+ "img",
260
+ {
261
+ src: i.preview,
262
+ alt: `Church logo for ${o}`
263
+ }
264
+ ) : f ? /* @__PURE__ */ a(
265
+ "img",
250
266
  {
251
- width: "64",
252
- height: "64",
253
- viewBox: "0 0 16 16",
254
- "aria-hidden": "true",
255
- children: /* @__PURE__ */ e("path", { d: q })
267
+ src: f,
268
+ alt: `Church logo for ${o}`
256
269
  }
257
- ),
258
- C && k ? /* @__PURE__ */ n(f, { children: [
259
- /* @__PURE__ */ e("h2", { children: "Wrong file type" }),
260
- /* @__PURE__ */ e("p", { children: "Please upload a PNG or JPG file." })
261
- ] }) : /* @__PURE__ */ n(f, { children: [
262
- /* @__PURE__ */ e("h2", { children: "Add church logo" }),
263
- /* @__PURE__ */ e("p", { children: "Use a high-quality image (up to 20mb) with a transparent background." })
270
+ ) : /* @__PURE__ */ n(u, { children: [
271
+ /* @__PURE__ */ a(
272
+ "svg",
273
+ {
274
+ width: "64",
275
+ height: "64",
276
+ viewBox: "0 0 16 16",
277
+ "aria-hidden": "true",
278
+ children: /* @__PURE__ */ a("path", { d: K })
279
+ }
280
+ ),
281
+ U && N ? /* @__PURE__ */ n(u, { children: [
282
+ /* @__PURE__ */ a("h2", { children: "Wrong file type" }),
283
+ /* @__PURE__ */ a("p", { children: "Please upload a PNG or JPG file." })
284
+ ] }) : /* @__PURE__ */ n(u, { children: [
285
+ /* @__PURE__ */ a(D, { open: S }),
286
+ /* @__PURE__ */ a("p", { className: "pco-org-avatar__instructions-sub", children: "Use a high-quality image (up to 20mb) with a transparent background." })
287
+ ] })
264
288
  ] })
265
- ] })
289
+ ] }),
290
+ (i || f) && /* @__PURE__ */ a(D, { open: S })
266
291
  ] });
267
292
  }
268
293
  }
269
294
  )
270
295
  ] }),
271
296
  /* @__PURE__ */ n("div", { className: "pco-org-avatar__dialog-actions", children: [
272
- a && /* @__PURE__ */ e(
273
- z,
297
+ e && h === "light" && /* @__PURE__ */ a(
298
+ k,
299
+ {
300
+ label: "Remove logo",
301
+ kind: "secondary-delete",
302
+ onClick: x,
303
+ loading: c,
304
+ disabled: c || d,
305
+ className: "pco-org-avatar__dialog-delete"
306
+ }
307
+ ),
308
+ e && h === "dark" && /* @__PURE__ */ a(
309
+ k,
274
310
  {
275
- label: "Delete logo",
276
- kind: "delete",
277
- onClick: F,
278
- loading: h,
279
- disabled: h || s,
311
+ label: "Use light mode logo",
312
+ kind: "secondary",
313
+ onClick: x,
314
+ loading: c,
315
+ disabled: c || d,
280
316
  className: "pco-org-avatar__dialog-delete"
281
317
  }
282
318
  ),
283
- /* @__PURE__ */ e(
284
- z,
319
+ /* @__PURE__ */ a(
320
+ k,
285
321
  {
286
322
  type: "button",
287
323
  label: "Cancel",
288
- commandfor: c,
289
- command: "close",
290
- disabled: s || h
324
+ onClick: () => v.current?.close(),
325
+ disabled: d || c
291
326
  }
292
327
  ),
293
- /* @__PURE__ */ e(
294
- z,
328
+ /* @__PURE__ */ a(
329
+ k,
295
330
  {
296
331
  type: "submit",
297
332
  label: "Update logo",
298
333
  kind: "primary",
299
- loading: s,
300
- disabled: !l || s || h
334
+ loading: d,
335
+ disabled: !i || d || c
301
336
  }
302
337
  )
303
338
  ] })
@@ -308,45 +343,46 @@ function D({
308
343
  )
309
344
  ] });
310
345
  }
311
- function ne({
312
- avatarUrl: a,
346
+ function ga({
347
+ avatarUrl: e,
313
348
  darkModeAvatarUrl: o,
314
349
  orgName: r,
315
350
  onAvatarUpdate: t,
316
- pcoEnv: d,
317
- readOnly: _,
318
- showDarkModeAvatar: l = !1
351
+ pcoEnv: s,
352
+ readOnly: y,
353
+ showDarkModeAvatar: R = !1
319
354
  }) {
320
- const [u, s] = v(a), [y, h] = v(
355
+ const [i, w] = b(e), [d, C] = b(
321
356
  o
322
- ), b = (p, c) => {
323
- p === "avatar" ? (s(c), t?.("avatar", c)) : p === "dark_mode_avatar" && (h(c), t?.("dark_mode_avatar", c));
357
+ ), c = (m, g) => {
358
+ m === "avatar" ? (w(g), t?.("avatar", g)) : m === "dark_mode_avatar" && (C(g), t?.("dark_mode_avatar", g));
324
359
  };
325
360
  return /* @__PURE__ */ n("div", { className: "pco-org-avatars", children: [
326
- /* @__PURE__ */ e(
327
- D,
361
+ /* @__PURE__ */ a(
362
+ F,
328
363
  {
329
- avatarUrl: u,
364
+ avatarUrl: i,
330
365
  organization: r,
331
366
  mode: "avatar",
332
- pcoEnv: d,
333
- readOnly: _,
334
- onAvatarUpdate: b
367
+ pcoEnv: s,
368
+ readOnly: y,
369
+ onAvatarUpdate: c
335
370
  }
336
371
  ),
337
- l && /* @__PURE__ */ e(
338
- D,
372
+ R && /* @__PURE__ */ a(
373
+ F,
339
374
  {
340
- avatarUrl: y || u,
375
+ avatarUrl: d,
341
376
  organization: r,
342
377
  mode: "dark_mode_avatar",
343
- pcoEnv: d,
344
- readOnly: _,
345
- onAvatarUpdate: b
378
+ pcoEnv: s,
379
+ readOnly: y,
380
+ onAvatarUpdate: c,
381
+ fallbackAvatarUrl: i
346
382
  }
347
383
  )
348
384
  ] });
349
385
  }
350
386
  export {
351
- ne as OrganizationAvatars
387
+ ga as OrganizationAvatars
352
388
  };
@@ -1,2 +1,2 @@
1
1
  import type { OrganizationAvatarProps } from "./types";
2
- export declare function OrganizationAvatar({ avatarUrl, organization, mode, pcoEnv, readOnly, onAvatarUpdate, }: OrganizationAvatarProps): import("react/jsx-runtime").JSX.Element;
2
+ export declare function OrganizationAvatar({ avatarUrl, organization, mode, pcoEnv, readOnly, onAvatarUpdate, fallbackAvatarUrl, }: OrganizationAvatarProps): import("react/jsx-runtime").JSX.Element;
package/dist/style.css CHANGED
@@ -178,19 +178,6 @@
178
178
  fill: var(--t-icon-color-status-info);
179
179
  }
180
180
 
181
- .pco-org-avatar__dialog-dropzone h2 {
182
- color: var(--t-text-color-headline);
183
- font-size: var(--t-font-size-md);
184
- font-weight: var(--t-font-weight-normal);
185
- margin: 0;
186
- }
187
-
188
- .pco-org-avatar__dialog-dropzone p {
189
- color: var(--t-text-color-placeholder);
190
- font-size: var(--t-font-size-sm);
191
- margin: 0;
192
- }
193
-
194
181
  .pco-org-avatar__dialog-dropzone img {
195
182
  max-height: 100%;
196
183
  max-width: 100%;
@@ -222,11 +209,25 @@
222
209
  fill: var(--t-fill-color-status-error);
223
210
  }
224
211
 
225
- /* Light/dark backgrounds are hardcoded to match the avatar preview context,
212
+ .pco-org-avatar__instructions {
213
+ font-size: var(--t-font-size-md);
214
+ font-weight: var(--t-font-weight-normal);
215
+ margin: var(--t-spacing-2) 0 0;
216
+ text-align: center;
217
+ }
218
+
219
+ .pco-org-avatar__instructions-sub {
220
+ color: var(--t-text-color-secondary);
221
+ font-size: var(--t-font-size-sm);
222
+ margin: 0;
223
+ }
224
+
225
+ /* Light/dark colors/backgrounds are hardcoded to match the avatar preview context,
226
226
  regardless of the UI theme or whether an avatar is uploaded */
227
+
227
228
  .pco-org-avatar__button--light,
228
229
  .pco-org-avatar__dialog-dropzone--light {
229
- background: hsl(0, 0%, 100%);
230
+ background: hsl(0, 0%, 97%);
230
231
  }
231
232
 
232
233
  .pco-org-avatar__button--light:has(img),
@@ -243,3 +244,11 @@
243
244
  .pco-org-avatar__dialog-dropzone--dark:has(img) {
244
245
  border-color: transparent;
245
246
  }
247
+
248
+ .pco-org-avatar__dialog-dropzone--light .pco-org-avatar__instructions {
249
+ color: hsl(0, 0%, 12%);
250
+ }
251
+
252
+ .pco-org-avatar__dialog-dropzone--dark .pco-org-avatar__instructions {
253
+ color: hsl(0, 0%, 94%);
254
+ }
package/dist/types.d.ts CHANGED
@@ -16,6 +16,7 @@ export type OrganizationAvatarProps = {
16
16
  pcoEnv?: Environment;
17
17
  readOnly?: boolean;
18
18
  onAvatarUpdate: (attrName: AvatarMode, newUrl: string) => void;
19
+ fallbackAvatarUrl?: string;
19
20
  };
20
21
  export type FileWithPreview = File & {
21
22
  preview: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@planningcenter/organization-avatars",
3
- "version": "1.6.2",
3
+ "version": "1.7.0",
4
4
  "description": "Organization avatar upload components for Planning Center apps",
5
5
  "type": "module",
6
6
  "packageManager": "yarn@1.22.22",
@@ -31,14 +31,19 @@
31
31
  },
32
32
  "peerDependencies": {
33
33
  "@planningcenter/icons": "^15.29.1",
34
+ "@planningcenter/sweetest-alert": "^1.0.1",
34
35
  "@planningcenter/tapestry": "^2.10.1",
35
36
  "@planningcenter/url": "^3.2.0",
36
37
  "react": "^18.3.0",
37
38
  "react-dom": "^18.3.0",
38
39
  "react-dropzone": "^14.0.0"
39
40
  },
41
+ "prettier": {
42
+ "semi": false
43
+ },
40
44
  "devDependencies": {
41
45
  "@planningcenter/icons": "^15.29.1",
46
+ "@planningcenter/sweetest-alert": "^1.0.1",
42
47
  "@planningcenter/tapestry": "^2.10.1",
43
48
  "@planningcenter/url": "^3.2.0",
44
49
  "@types/react": "^18.3.0",