@planningcenter/organization-avatars 1.6.3 → 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"),i=require("react"),M=require("react-dom"),B=require("@planningcenter/url"),A=require("@planningcenter/tapestry"),x=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 l=a.length;for(o=0;o<l;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="",l=arguments.length;t<l;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:l,onAvatarUpdate:j}){const[s,g]=i.useState(null),[c,_]=i.useState(!1),[d,f]=i.useState(!1),[b,u]=i.useState(""),p=i.useId(),v=i.useRef(null),y=I(t),F=h=>{const[n]=h;n&&g(Object.assign(n,{preview:URL.createObjectURL(n)}))};i.useEffect(()=>()=>{s?.preview&&(URL.revokeObjectURL(s.preview),u(""))},[s]);const N=async h=>{const n=await fetch(B.pcoApiUrl("accounts",{env:r}),{method:"PATCH",headers:{"Content-Type":"application/json","X-CSRF-Token":$(),Accept:"application/json"},body:JSON.stringify({data:{attributes:h}})}),m=await n.json();if(!n.ok)throw m;return m},R=async h=>{if(h.preventDefault(),!(!s||c||d)){_(!0);try{const n=new FileReader,m=await new Promise((w,k)=>{n.onloadend=()=>w(n.result),n.onerror=k,n.readAsDataURL(s)}),C=await N({[t]:m,[`${t}_cache`]:""});g(null),j(t,C.data.attributes[t].url),v.current?.close()}catch{u("We were unable to save your avatar.")}finally{_(!1)}}},U=async()=>{if(!(c||d)){f(!0);try{await N({[`remove_${t}`]:"1"}),j(t,""),v.current?.close()}catch{u("We were unable to delete your avatar.")}finally{f(!1)}}},q=()=>{g(null),u("")};return e.jsxs(e.Fragment,{children:[e.jsxs("div",{className:"pco-org-avatar",children:[e.jsx("button",{type:"button",onClick:()=>v.current?.showModal(),disabled:l,className:`pco-org-avatar__button pco-org-avatar__button--${y}`,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:x.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:x.pencil})})})]})}),e.jsxs("div",{className:"pco-org-avatar__label",children:[L(y)," mode"]})]}),M.createPortal(e.jsx("dialog",{id:p,ref:v,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 — ",y," 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:x.smallX})})})]}),e.jsxs("div",{className:"pco-org-avatar__dialog-body",children:[b&&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:x.exclamationTriangle})}),e.jsx("p",{children:b})]}),e.jsx(P,{accept:{"image/*":[".jpg",".jpeg",".png"]},multiple:!1,onDropAccepted:F,children:({getRootProps:h,getInputProps:n,isFocused:m,isDragAccept:C,isDragReject:w,isDragActive:k})=>{const D=T("pco-org-avatar__dialog-dropzone",`pco-org-avatar__dialog-dropzone--${y}`,{"pco-org-avatar__dialog-dropzone--image":s||a,"pco-org-avatar__dialog-dropzone--focused":m,"pco-org-avatar__dialog-dropzone--accepted":C,"pco-org-avatar__dialog-dropzone--rejected":k&&w});return e.jsxs("div",{...h({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:x.toCloudArrow})}),k&&w?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(A.Button,{label:"Delete logo",kind:"delete",onClick:U,loading:d,disabled:d||c,className:"pco-org-avatar__dialog-delete"}),e.jsx(A.Button,{type:"button",label:"Cancel",onClick:()=>v.current?.close(),disabled:c||d}),e.jsx(A.Button,{type:"submit",label:"Update logo",kind:"primary",loading:c,disabled:!s||c||d})]})]})}),document.body)]})}function E({avatarUrl:a,darkModeAvatarUrl:o,orgName:t,onAvatarUpdate:r,pcoEnv:l,readOnly:j,showDarkModeAvatar:s=!1}){const[g,c]=i.useState(a),[_,d]=i.useState(o),f=(u,p)=>{u==="avatar"?(c(p),r?.("avatar",p)):u==="dark_mode_avatar"&&(d(p),r?.("dark_mode_avatar",p))},b=_||g;return e.jsxs("div",{className:"pco-org-avatars",children:[e.jsx(S,{avatarUrl:g,organization:t,mode:"avatar",pcoEnv:l,readOnly:j,onAvatarUpdate:f}),s&&e.jsx(S,{avatarUrl:b,organization:t,mode:"dark_mode_avatar",pcoEnv:l,readOnly:j,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,117 +1,140 @@
1
- import { jsxs as n, Fragment as f, jsx as e } from "react/jsx-runtime";
2
- import { useState as _, 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 M(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 c = a.length;
14
- for (o = 0; o < c; o++) a[o] && (r = M(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 = "", c = arguments.length; r < c; r++) (a = arguments[r]) && (o = M(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: c,
41
- onAvatarUpdate: b
54
+ readOnly: s,
55
+ onAvatarUpdate: y,
56
+ fallbackAvatarUrl: R
42
57
  }) {
43
- const [l, p] = _(null), [d, w] = _(!1), [s, y] = _(!1), [N, h] = _(""), g = O(), m = P(null), k = Q(r), R = (u) => {
44
- const [i] = u;
45
- i && p(
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), h(""));
53
- }, [l]);
54
- const U = async (u) => {
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: u
79
+ attributes: p
65
80
  }
66
81
  })
67
- }), v = await i.json();
68
- if (!i.ok) throw v;
69
- return v;
70
- }, S = async (u) => {
71
- if (u.preventDefault(), !(!l || d || s)) {
72
- w(!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(), v = await new Promise((C, A) => {
75
- i.onloadend = () => C(i.result), i.onerror = A, i.readAsDataURL(l);
76
- }), j = await U({
77
- [r]: v,
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
- p(null), b(r, j.data.attributes[r].url), m.current?.close();
95
+ w(null), y(r, j.data.attributes[r].url), v.current?.close();
81
96
  } catch {
82
- h("We were unable to save your avatar.");
97
+ A("We were unable to save your avatar.");
83
98
  } finally {
84
- w(!1);
99
+ C(!1);
85
100
  }
86
101
  }
87
- }, F = async () => {
88
- if (!(d || s)) {
89
- y(!0);
90
- try {
91
- await U({
92
- [`remove_${r}`]: "1"
93
- }), b(r, ""), m.current?.close();
94
- } catch {
95
- h("We were unable to delete your avatar.");
96
- } finally {
97
- y(!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
- p(null), h("");
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
- onClick: () => m.current?.showModal(),
110
- disabled: c,
111
- className: `pco-org-avatar__button pco-org-avatar__button--${k}`,
112
- children: a ? /* @__PURE__ */ n(f, { children: [
113
- /* @__PURE__ */ e("img", { src: a, alt: `Church logo for ${o}` }),
114
- /* @__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(
115
138
  "svg",
116
139
  {
117
140
  width: "14",
@@ -119,11 +142,11 @@ function D({
119
142
  viewBox: "0 0 16 16",
120
143
  fill: "currentColor",
121
144
  "aria-hidden": "true",
122
- children: /* @__PURE__ */ e("path", { d: x })
145
+ children: /* @__PURE__ */ a("path", { d: B })
123
146
  }
124
147
  ) })
125
- ] }) : /* @__PURE__ */ n(f, { children: [
126
- /* @__PURE__ */ e(
148
+ ] }) : /* @__PURE__ */ n(u, { children: [
149
+ /* @__PURE__ */ a(
127
150
  "svg",
128
151
  {
129
152
  width: "48",
@@ -131,10 +154,10 @@ function D({
131
154
  viewBox: "0 0 16 16",
132
155
  fill: "currentColor",
133
156
  "aria-hidden": "true",
134
- children: /* @__PURE__ */ e("path", { d: G })
157
+ children: /* @__PURE__ */ a("path", { d: Q })
135
158
  }
136
159
  ),
137
- /* @__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(
138
161
  "svg",
139
162
  {
140
163
  width: "14",
@@ -142,40 +165,40 @@ function D({
142
165
  viewBox: "0 0 16 16",
143
166
  fill: "currentColor",
144
167
  "aria-hidden": "true",
145
- children: /* @__PURE__ */ e("path", { d: x })
168
+ children: /* @__PURE__ */ a("path", { d: B })
146
169
  }
147
170
  ) })
148
171
  ] })
149
172
  }
150
173
  ),
151
174
  /* @__PURE__ */ n("div", { className: "pco-org-avatar__label", children: [
152
- K(k),
175
+ aa(h),
153
176
  " mode"
154
177
  ] })
155
178
  ] }),
156
- L(
157
- /* @__PURE__ */ e(
179
+ q(
180
+ /* @__PURE__ */ a(
158
181
  "dialog",
159
182
  {
160
- id: g,
161
- ref: m,
183
+ id: $,
184
+ ref: v,
162
185
  className: "pco-org-avatar__dialog",
163
- onClose: B,
164
- children: /* @__PURE__ */ n("form", { onSubmit: S, children: [
186
+ onClose: O,
187
+ children: /* @__PURE__ */ n("form", { onSubmit: T, children: [
165
188
  /* @__PURE__ */ n("div", { className: "pco-org-avatar__dialog-header", children: [
166
189
  /* @__PURE__ */ n("h1", { className: "pco-org-avatar__dialog-title", children: [
167
190
  "Church logo — ",
168
- k,
191
+ h,
169
192
  " mode"
170
193
  ] }),
171
- /* @__PURE__ */ e(
194
+ /* @__PURE__ */ a(
172
195
  "button",
173
196
  {
174
197
  type: "button",
175
- onClick: () => m.current?.close(),
198
+ onClick: () => v.current?.close(),
176
199
  className: "pco-org-avatar__dialog-close",
177
200
  "aria-label": "Close modal",
178
- children: /* @__PURE__ */ e(
201
+ children: /* @__PURE__ */ a(
179
202
  "svg",
180
203
  {
181
204
  width: "16",
@@ -183,15 +206,15 @@ function D({
183
206
  viewBox: "0 0 16 16",
184
207
  fill: "currentColor",
185
208
  "aria-hidden": "true",
186
- children: /* @__PURE__ */ e("path", { d: E })
209
+ children: /* @__PURE__ */ a("path", { d: X })
187
210
  }
188
211
  )
189
212
  }
190
213
  )
191
214
  ] }),
192
215
  /* @__PURE__ */ n("div", { className: "pco-org-avatar__dialog-body", children: [
193
- N && /* @__PURE__ */ n("div", { role: "alert", className: "pco-org-avatar__dialog-alert", children: [
194
- /* @__PURE__ */ e(
216
+ g && /* @__PURE__ */ n("div", { role: "alert", className: "pco-org-avatar__dialog-alert", children: [
217
+ /* @__PURE__ */ a(
195
218
  "svg",
196
219
  {
197
220
  width: "16",
@@ -199,102 +222,117 @@ function D({
199
222
  viewBox: "0 0 16 16",
200
223
  className: "symbol",
201
224
  "aria-hidden": "true",
202
- children: /* @__PURE__ */ e("path", { d: W })
225
+ children: /* @__PURE__ */ a("path", { d: H })
203
226
  }
204
227
  ),
205
- /* @__PURE__ */ e("p", { children: N })
228
+ /* @__PURE__ */ a("p", { children: g })
206
229
  ] }),
207
- /* @__PURE__ */ e(
208
- J,
230
+ /* @__PURE__ */ a(
231
+ V,
209
232
  {
210
233
  accept: { "image/*": [".jpg", ".jpeg", ".png"] },
211
234
  multiple: !1,
212
- onDropAccepted: R,
235
+ onDropAccepted: P,
213
236
  children: ({
214
- getRootProps: u,
215
- getInputProps: i,
216
- isFocused: v,
237
+ getRootProps: p,
238
+ getInputProps: l,
239
+ isFocused: _,
217
240
  isDragAccept: j,
218
- isDragReject: C,
219
- isDragActive: A
241
+ isDragReject: N,
242
+ isDragActive: U,
243
+ open: S
220
244
  }) => {
221
- const $ = X(
245
+ const I = Y(
222
246
  "pco-org-avatar__dialog-dropzone",
223
- `pco-org-avatar__dialog-dropzone--${k}`,
247
+ `pco-org-avatar__dialog-dropzone--${h}`,
224
248
  {
225
- "pco-org-avatar__dialog-dropzone--image": l || a,
226
- "pco-org-avatar__dialog-dropzone--focused": v,
249
+ "pco-org-avatar__dialog-dropzone--image": i || f,
250
+ "pco-org-avatar__dialog-dropzone--focused": _,
227
251
  "pco-org-avatar__dialog-dropzone--accepted": j,
228
- "pco-org-avatar__dialog-dropzone--rejected": A && C
252
+ "pco-org-avatar__dialog-dropzone--rejected": U && N
229
253
  }
230
254
  );
231
- return /* @__PURE__ */ n("div", { ...u({ className: $ }), children: [
232
- /* @__PURE__ */ e("input", { ...i() }),
233
- l ? /* @__PURE__ */ e(
234
- "img",
235
- {
236
- src: l.preview,
237
- alt: `Church logo for ${o}`
238
- }
239
- ) : a ? /* @__PURE__ */ e(
240
- "img",
241
- {
242
- src: a,
243
- alt: `Church logo for ${o}`
244
- }
245
- ) : /* @__PURE__ */ n(f, { children: [
246
- /* @__PURE__ */ e(
247
- "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",
248
260
  {
249
- width: "64",
250
- height: "64",
251
- viewBox: "0 0 16 16",
252
- "aria-hidden": "true",
253
- children: /* @__PURE__ */ e("path", { d: q })
261
+ src: i.preview,
262
+ alt: `Church logo for ${o}`
254
263
  }
255
- ),
256
- A && C ? /* @__PURE__ */ n(f, { children: [
257
- /* @__PURE__ */ e("h2", { children: "Wrong file type" }),
258
- /* @__PURE__ */ e("p", { children: "Please upload a PNG or JPG file." })
259
- ] }) : /* @__PURE__ */ n(f, { children: [
260
- /* @__PURE__ */ e("h2", { children: "Add church logo" }),
261
- /* @__PURE__ */ e("p", { children: "Use a high-quality image (up to 20mb) with a transparent background." })
264
+ ) : f ? /* @__PURE__ */ a(
265
+ "img",
266
+ {
267
+ src: f,
268
+ alt: `Church logo for ${o}`
269
+ }
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
+ ] })
262
288
  ] })
263
- ] })
289
+ ] }),
290
+ (i || f) && /* @__PURE__ */ a(D, { open: S })
264
291
  ] });
265
292
  }
266
293
  }
267
294
  )
268
295
  ] }),
269
296
  /* @__PURE__ */ n("div", { className: "pco-org-avatar__dialog-actions", children: [
270
- a && /* @__PURE__ */ e(
271
- z,
297
+ e && h === "light" && /* @__PURE__ */ a(
298
+ k,
272
299
  {
273
- label: "Delete logo",
274
- kind: "delete",
275
- onClick: F,
276
- loading: s,
277
- disabled: s || d,
300
+ label: "Remove logo",
301
+ kind: "secondary-delete",
302
+ onClick: x,
303
+ loading: c,
304
+ disabled: c || d,
278
305
  className: "pco-org-avatar__dialog-delete"
279
306
  }
280
307
  ),
281
- /* @__PURE__ */ e(
282
- z,
308
+ e && h === "dark" && /* @__PURE__ */ a(
309
+ k,
310
+ {
311
+ label: "Use light mode logo",
312
+ kind: "secondary",
313
+ onClick: x,
314
+ loading: c,
315
+ disabled: c || d,
316
+ className: "pco-org-avatar__dialog-delete"
317
+ }
318
+ ),
319
+ /* @__PURE__ */ a(
320
+ k,
283
321
  {
284
322
  type: "button",
285
323
  label: "Cancel",
286
- onClick: () => m.current?.close(),
287
- disabled: d || s
324
+ onClick: () => v.current?.close(),
325
+ disabled: d || c
288
326
  }
289
327
  ),
290
- /* @__PURE__ */ e(
291
- z,
328
+ /* @__PURE__ */ a(
329
+ k,
292
330
  {
293
331
  type: "submit",
294
332
  label: "Update logo",
295
333
  kind: "primary",
296
334
  loading: d,
297
- disabled: !l || d || s
335
+ disabled: !i || d || c
298
336
  }
299
337
  )
300
338
  ] })
@@ -305,45 +343,46 @@ function D({
305
343
  )
306
344
  ] });
307
345
  }
308
- function ne({
309
- avatarUrl: a,
346
+ function ga({
347
+ avatarUrl: e,
310
348
  darkModeAvatarUrl: o,
311
349
  orgName: r,
312
350
  onAvatarUpdate: t,
313
- pcoEnv: c,
314
- readOnly: b,
315
- showDarkModeAvatar: l = !1
351
+ pcoEnv: s,
352
+ readOnly: y,
353
+ showDarkModeAvatar: R = !1
316
354
  }) {
317
- const [p, d] = _(a), [w, s] = _(
355
+ const [i, w] = b(e), [d, C] = b(
318
356
  o
319
- ), y = (h, g) => {
320
- h === "avatar" ? (d(g), t?.("avatar", g)) : h === "dark_mode_avatar" && (s(g), t?.("dark_mode_avatar", g));
357
+ ), c = (m, g) => {
358
+ m === "avatar" ? (w(g), t?.("avatar", g)) : m === "dark_mode_avatar" && (C(g), t?.("dark_mode_avatar", g));
321
359
  };
322
360
  return /* @__PURE__ */ n("div", { className: "pco-org-avatars", children: [
323
- /* @__PURE__ */ e(
324
- D,
361
+ /* @__PURE__ */ a(
362
+ F,
325
363
  {
326
- avatarUrl: p,
364
+ avatarUrl: i,
327
365
  organization: r,
328
366
  mode: "avatar",
329
- pcoEnv: c,
330
- readOnly: b,
331
- onAvatarUpdate: y
367
+ pcoEnv: s,
368
+ readOnly: y,
369
+ onAvatarUpdate: c
332
370
  }
333
371
  ),
334
- l && /* @__PURE__ */ e(
335
- D,
372
+ R && /* @__PURE__ */ a(
373
+ F,
336
374
  {
337
- avatarUrl: w || p,
375
+ avatarUrl: d,
338
376
  organization: r,
339
377
  mode: "dark_mode_avatar",
340
- pcoEnv: c,
341
- readOnly: b,
342
- onAvatarUpdate: y
378
+ pcoEnv: s,
379
+ readOnly: y,
380
+ onAvatarUpdate: c,
381
+ fallbackAvatarUrl: i
343
382
  }
344
383
  )
345
384
  ] });
346
385
  }
347
386
  export {
348
- ne as OrganizationAvatars
387
+ ga as OrganizationAvatars
349
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.3",
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",