@churchapps/apphelper 0.4.17 → 0.4.19

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.
Files changed (146) hide show
  1. package/dist/components/DisplayBox.js +1 -1
  2. package/dist/components/DisplayBox.js.map +1 -1
  3. package/dist/components/FormCardPayment.js +2 -2
  4. package/dist/components/FormCardPayment.js.map +1 -1
  5. package/dist/components/FormSubmissionEdit.d.ts.map +1 -1
  6. package/dist/components/FormSubmissionEdit.js +4 -5
  7. package/dist/components/FormSubmissionEdit.js.map +1 -1
  8. package/dist/components/InputBox.js +1 -1
  9. package/dist/components/InputBox.js.map +1 -1
  10. package/dist/components/Loading.js +1 -1
  11. package/dist/components/Loading.js.map +1 -1
  12. package/dist/components/PageHeader.d.ts +15 -0
  13. package/dist/components/PageHeader.d.ts.map +1 -0
  14. package/dist/components/PageHeader.js +41 -0
  15. package/dist/components/PageHeader.js.map +1 -0
  16. package/dist/components/PersonAvatar.d.ts +12 -0
  17. package/dist/components/PersonAvatar.d.ts.map +1 -0
  18. package/dist/components/PersonAvatar.js +55 -0
  19. package/dist/components/PersonAvatar.js.map +1 -0
  20. package/dist/components/header/SiteHeader.d.ts +2 -1
  21. package/dist/components/header/SiteHeader.d.ts.map +1 -1
  22. package/dist/components/header/SiteHeader.js +100 -4
  23. package/dist/components/header/SiteHeader.js.map +1 -1
  24. package/dist/components/header/SupportDrawer.js.map +1 -1
  25. package/dist/components/index.d.ts +2 -0
  26. package/dist/components/index.d.ts.map +1 -1
  27. package/dist/components/index.js +2 -0
  28. package/dist/components/index.js.map +1 -1
  29. package/dist/components/notes/AddNote.d.ts.map +1 -1
  30. package/dist/components/notes/AddNote.js +45 -7
  31. package/dist/components/notes/AddNote.js.map +1 -1
  32. package/dist/components/notes/Note.d.ts.map +1 -1
  33. package/dist/components/notes/Note.js +6 -6
  34. package/dist/components/notes/Note.js.map +1 -1
  35. package/dist/components/notes/Notes.d.ts.map +1 -1
  36. package/dist/components/notes/Notes.js +120 -20
  37. package/dist/components/notes/Notes.js.map +1 -1
  38. package/dist/components/wrapper/ChurchList.d.ts.map +1 -1
  39. package/dist/components/wrapper/ChurchList.js +44 -6
  40. package/dist/components/wrapper/ChurchList.js.map +1 -1
  41. package/dist/components/wrapper/NewPrivateMessage.d.ts.map +1 -1
  42. package/dist/components/wrapper/NewPrivateMessage.js +28 -21
  43. package/dist/components/wrapper/NewPrivateMessage.js.map +1 -1
  44. package/dist/components/wrapper/Notifications.d.ts.map +1 -1
  45. package/dist/components/wrapper/Notifications.js +47 -20
  46. package/dist/components/wrapper/Notifications.js.map +1 -1
  47. package/dist/components/wrapper/PrivateMessageDetails.d.ts +1 -0
  48. package/dist/components/wrapper/PrivateMessageDetails.d.ts.map +1 -1
  49. package/dist/components/wrapper/PrivateMessageDetails.js +53 -4
  50. package/dist/components/wrapper/PrivateMessageDetails.js.map +1 -1
  51. package/dist/components/wrapper/PrivateMessages.d.ts.map +1 -1
  52. package/dist/components/wrapper/PrivateMessages.js +360 -41
  53. package/dist/components/wrapper/PrivateMessages.js.map +1 -1
  54. package/dist/components/wrapper/UserMenu.d.ts.map +1 -1
  55. package/dist/components/wrapper/UserMenu.js +164 -27
  56. package/dist/components/wrapper/UserMenu.js.map +1 -1
  57. package/dist/components/wrapper/index.d.ts +2 -1
  58. package/dist/components/wrapper/index.d.ts.map +1 -1
  59. package/dist/components/wrapper/index.js +2 -1
  60. package/dist/components/wrapper/index.js.map +1 -1
  61. package/dist/helpers/ArrayHelper.d.ts.map +1 -1
  62. package/dist/helpers/ArrayHelper.js +0 -1
  63. package/dist/helpers/ArrayHelper.js.map +1 -1
  64. package/dist/helpers/ErrorHelper.js +1 -1
  65. package/dist/helpers/ErrorHelper.js.map +1 -1
  66. package/dist/helpers/EventHelper.d.ts.map +1 -1
  67. package/dist/helpers/EventHelper.js +0 -3
  68. package/dist/helpers/EventHelper.js.map +1 -1
  69. package/dist/helpers/Locale.d.ts +1 -1
  70. package/dist/helpers/Locale.d.ts.map +1 -1
  71. package/dist/helpers/Locale.js +7 -2
  72. package/dist/helpers/Locale.js.map +1 -1
  73. package/dist/helpers/NotificationService.d.ts +56 -0
  74. package/dist/helpers/NotificationService.d.ts.map +1 -0
  75. package/dist/helpers/NotificationService.js +176 -0
  76. package/dist/helpers/NotificationService.js.map +1 -0
  77. package/dist/helpers/SocketHelper.d.ts.map +1 -1
  78. package/dist/helpers/SocketHelper.js +22 -17
  79. package/dist/helpers/SocketHelper.js.map +1 -1
  80. package/dist/helpers/UserHelper.js +2 -2
  81. package/dist/helpers/UserHelper.js.map +1 -1
  82. package/dist/helpers/index.d.ts +2 -0
  83. package/dist/helpers/index.d.ts.map +1 -1
  84. package/dist/helpers/index.js +1 -0
  85. package/dist/helpers/index.js.map +1 -1
  86. package/dist/hooks/index.d.ts +2 -0
  87. package/dist/hooks/index.d.ts.map +1 -1
  88. package/dist/hooks/index.js +1 -0
  89. package/dist/hooks/index.js.map +1 -1
  90. package/dist/hooks/useNotifications.d.ts +30 -0
  91. package/dist/hooks/useNotifications.d.ts.map +1 -0
  92. package/dist/hooks/useNotifications.js +79 -0
  93. package/dist/hooks/useNotifications.js.map +1 -0
  94. package/dist/public/css/styles.css +6 -2
  95. package/package.json +1 -1
  96. package/public/css/styles.css +6 -2
  97. package/src/components/DisplayBox.tsx +8 -8
  98. package/src/components/FormCardPayment.tsx +2 -2
  99. package/src/components/FormSubmissionEdit.tsx +5 -6
  100. package/src/components/InputBox.tsx +8 -8
  101. package/src/components/Loading.tsx +1 -1
  102. package/src/components/PageHeader.tsx +111 -0
  103. package/src/components/PersonAvatar.tsx +78 -0
  104. package/src/components/header/SiteHeader.tsx +133 -10
  105. package/src/components/header/SupportDrawer.tsx +1 -1
  106. package/src/components/index.tsx +2 -0
  107. package/src/components/notes/AddNote.tsx +105 -19
  108. package/src/components/notes/Note.tsx +43 -22
  109. package/src/components/notes/Notes.tsx +160 -21
  110. package/src/components/wrapper/ChurchList.tsx +45 -5
  111. package/src/components/wrapper/NewPrivateMessage.tsx +181 -44
  112. package/src/components/wrapper/Notifications.tsx +165 -29
  113. package/src/components/wrapper/PrivateMessageDetails.tsx +100 -13
  114. package/src/components/wrapper/PrivateMessages.tsx +539 -65
  115. package/src/components/wrapper/UserMenu.tsx +223 -38
  116. package/src/components/wrapper/index.tsx +3 -2
  117. package/src/helpers/ArrayHelper.ts +0 -1
  118. package/src/helpers/ErrorHelper.ts +1 -1
  119. package/src/helpers/EventHelper.ts +0 -3
  120. package/src/helpers/Locale.ts +7 -2
  121. package/src/helpers/NotificationService.ts +211 -0
  122. package/src/helpers/SocketHelper.ts +23 -17
  123. package/src/helpers/UserHelper.ts +2 -2
  124. package/src/helpers/index.ts +2 -0
  125. package/src/hooks/index.ts +2 -0
  126. package/src/hooks/useNotifications.ts +94 -0
  127. package/dist/components/wrapper/Drawers.d.ts +0 -5
  128. package/dist/components/wrapper/Drawers.d.ts.map +0 -1
  129. package/dist/components/wrapper/Drawers.js +0 -49
  130. package/dist/components/wrapper/Drawers.js.map +0 -1
  131. package/dist/components/wrapper/SiteWrapper.d.ts +0 -15
  132. package/dist/components/wrapper/SiteWrapper.d.ts.map +0 -1
  133. package/dist/components/wrapper/SiteWrapper.js +0 -60
  134. package/dist/components/wrapper/SiteWrapper.js.map +0 -1
  135. package/dist/components/wrapper/TabPanel.d.ts +0 -9
  136. package/dist/components/wrapper/TabPanel.d.ts.map +0 -1
  137. package/dist/components/wrapper/TabPanel.js +0 -17
  138. package/dist/components/wrapper/TabPanel.js.map +0 -1
  139. package/dist/helpers/ApiHelper.d.ts +0 -18
  140. package/dist/helpers/ApiHelper.d.ts.map +0 -1
  141. package/dist/helpers/ApiHelper.js +0 -119
  142. package/dist/helpers/ApiHelper.js.map +0 -1
  143. package/src/components/wrapper/Drawers.tsx +0 -62
  144. package/src/components/wrapper/SiteWrapper.tsx +0 -110
  145. package/src/components/wrapper/TabPanel.tsx +0 -32
  146. package/src/helpers/ApiHelper.ts +0 -127
@@ -26,10 +26,10 @@ export const FormCardPayment = forwardRef((props: Props, ref) => {
26
26
 
27
27
  const getChurchData = () => {
28
28
  let fundId = props.question.choices.find(c => c.text === "FundId")?.value;
29
- ApiHelper.get("/churches/" + props.churchId, "MembershipApi").then(data => {
29
+ ApiHelper.get("/churches/" + props.churchId, "MembershipApi").then((data: any) => {
30
30
  setChurch(data);
31
31
  });
32
- ApiHelper.get("/funds/churchId/" + props.churchId, "GivingApi").then(data => {
32
+ ApiHelper.get("/funds/churchId/" + props.churchId, "GivingApi").then((data: any) => {
33
33
  const result = ArrayHelper.getOne(data, "id", fundId);
34
34
  setFund(result);
35
35
  })
@@ -58,10 +58,9 @@ export const FormSubmissionEdit: React.FC<Props> = ({showHeader = true, noBackgr
58
58
  }
59
59
 
60
60
  const loadData = () => {
61
- console.log("loadData", "fs", props.formSubmissionId, "af", props.addFormId, props.unRestrictedFormId)
62
- if (!UniqueIdHelper.isMissing(props.formSubmissionId)) ApiHelper.get("/formsubmissions/" + props.formSubmissionId + "/?include=questions,answers,form", "MembershipApi").then(data => setFormSubmission(data));
63
- else if (!UniqueIdHelper.isMissing(props.addFormId)) ApiHelper.get("/questions/?formId=" + props.addFormId, "MembershipApi").then(data => setFormSubmissionData(data));
64
- else if (!UniqueIdHelper.isMissing(props.unRestrictedFormId)) ApiHelper.get("/questions/unrestricted?formId=" + props.unRestrictedFormId, "MembershipApi").then(data => setFormSubmissionData(data));
61
+ if (!UniqueIdHelper.isMissing(props.formSubmissionId)) ApiHelper.get("/formsubmissions/" + props.formSubmissionId + "/?include=questions,answers,form", "MembershipApi").then((data: any) => setFormSubmission(data));
62
+ else if (!UniqueIdHelper.isMissing(props.addFormId)) ApiHelper.get("/questions/?formId=" + props.addFormId, "MembershipApi").then((data: any) => setFormSubmissionData(data));
63
+ else if (!UniqueIdHelper.isMissing(props.unRestrictedFormId)) ApiHelper.get("/questions/unrestricted?formId=" + props.unRestrictedFormId, "MembershipApi").then((data: any) => setFormSubmissionData(data));
65
64
  }
66
65
 
67
66
  const getDefaultValue = (q: QuestionInterface) => {
@@ -116,7 +115,7 @@ export const FormSubmissionEdit: React.FC<Props> = ({showHeader = true, noBackgr
116
115
  fs.submissionDate = new Date();
117
116
  fs.churchId = props.churchId || null;
118
117
 
119
- ApiHelper.post("/formsubmissions/", [fs], "MembershipApi").then((res) => {
118
+ ApiHelper.post("/formsubmissions/", [fs], "MembershipApi").then((res: any) => {
120
119
  if (res?.[0]?.error) {
121
120
  setErrors([res?.[0].error]);
122
121
  } else {
@@ -143,7 +142,7 @@ export const FormSubmissionEdit: React.FC<Props> = ({showHeader = true, noBackgr
143
142
 
144
143
  React.useEffect(() => {
145
144
  if (props.churchId) {
146
- ApiHelper.get("/gateways/churchId/" + props.churchId, "GivingApi").then(data => {
145
+ ApiHelper.get("/gateways/churchId/" + props.churchId, "GivingApi").then((data: any) => {
147
146
  if (data.length && data[0]?.publicKey) {
148
147
  setStripe(loadStripe(data[0].publicKey));
149
148
  }
@@ -72,23 +72,23 @@ export function InputBox({ mainContainerCssProps = {}, ...props }: Props) {
72
72
  }
73
73
 
74
74
  return (
75
- <Paper id={props.id} sx={{ padding: 2, marginBottom: 4 }} data-testid={props["data-testid"]} {...mainContainerCssProps}>
75
+ <Paper id={props.id || "input-box"} sx={{ padding: 2, marginBottom: 4 }} data-testid={props["data-testid"]} {...mainContainerCssProps}>
76
76
  {props.help && <HelpIcon article={props.help} />}
77
- <Box sx={{ display: "flex", justifyContent: "space-between", alignItems: "center", position: "relative" }} data-testid="input-box-header">
78
- <Box display="flex" alignItems="center">
79
- {props.headerIcon && <Icon sx={{ color: headerText }}>{props.headerIcon}</Icon>}
77
+ <Box id="input-box-header" sx={{ display: "flex", justifyContent: "space-between", alignItems: "center", position: "relative" }} data-testid="input-box-header">
78
+ <Box id="input-box-title-section" display="flex" alignItems="center">
79
+ {props.headerIcon && <Icon id="input-box-icon" sx={{ color: headerText }}>{props.headerIcon}</Icon>}
80
80
  {props.headerText && (
81
- <Typography component="h2" sx={{ display: "inline-block", marginLeft: props.headerIcon ? 1 : 0 }} variant="h6" color={headerText}>
81
+ <Typography id="input-box-title" component="h2" sx={{ display: "inline-block", marginLeft: props.headerIcon ? 1 : 0 }} variant="h6" color={headerText}>
82
82
  {props.headerText}
83
83
  </Typography>
84
84
  )}
85
85
  </Box>
86
- <Box>
86
+ <Box id="input-box-actions">
87
87
  {props.headerActionContent}
88
88
  </Box>
89
89
  </Box>
90
- <CustomContextBox>{props.children}</CustomContextBox>
91
- <Stack direction="row" sx={{ marginTop: 1 }} spacing={1} justifyContent="end">
90
+ <CustomContextBox id="input-box-content">{props.children}</CustomContextBox>
91
+ <Stack id="input-box-buttons" direction="row" sx={{ marginTop: 1 }} spacing={1} justifyContent="end">
92
92
  {buttons}
93
93
  </Stack>
94
94
  </Paper>
@@ -70,7 +70,7 @@ export const Loading: React.FC<Props> = (props) => {
70
70
  }
71
71
 
72
72
  return (
73
- <div style={{ textAlign: "center", fontFamily: "Roboto" }}>
73
+ <div id="loading-component" style={{ textAlign: "center", fontFamily: "Roboto" }}>
74
74
  {getContents()}
75
75
  </div>
76
76
  )
@@ -0,0 +1,111 @@
1
+ import React, { ReactNode } from "react";
2
+ import { Box, Typography, Stack } from "@mui/material";
3
+
4
+ interface PageHeaderProps {
5
+ icon: ReactNode;
6
+ title: string;
7
+ subtitle?: string;
8
+ children?: ReactNode; // For action buttons or tabs
9
+ statistics?: Array<{ icon: ReactNode; value: string; label: string }>;
10
+ }
11
+
12
+ export const PageHeader: React.FC<PageHeaderProps> = ({ icon, title, subtitle, children, statistics }) => {
13
+ return (
14
+ <Box id="page-header" sx={{
15
+ backgroundColor: "var(--c1l2)",
16
+ color: "#FFF",
17
+ position: 'relative',
18
+ left: '50%',
19
+ right: '50%',
20
+ marginLeft: '-50vw',
21
+ marginRight: '-50vw',
22
+ width: '100vw',
23
+ '--c1': '#1565C0',
24
+ '--c1d1': '#1358AD',
25
+ '--c1d2': '#114A99',
26
+ '--c1l2': '#568BDA'
27
+ }}>
28
+ <Box sx={{
29
+ paddingX: { xs: 2, sm: 3, md: 4 },
30
+ paddingY: 3
31
+ }}>
32
+ <Stack direction={{ xs: "column", md: "row" }} spacing={{ xs: 2, md: 4 }} alignItems={{ xs: "flex-start", md: "center" }} sx={{ width: "100%" }}>
33
+ {/* Left side: Title and Icon */}
34
+ <Stack id="page-header-title-section" direction="row" spacing={2} alignItems="center" sx={{ flex: 1 }}>
35
+ <Box
36
+ id="page-header-icon"
37
+ sx={{
38
+ backgroundColor: "rgba(255,255,255,0.2)",
39
+ borderRadius: "12px",
40
+ p: 1.5,
41
+ display: "flex",
42
+ alignItems: "center",
43
+ justifyContent: "center",
44
+ }}
45
+ >
46
+ {React.cloneElement(icon as React.ReactElement<any>, { sx: { fontSize: 32, color: "#FFF" } })}
47
+ </Box>
48
+ <Box id="page-header-text">
49
+ <Typography
50
+ id="page-header-title"
51
+ variant="h4"
52
+ sx={{
53
+ fontWeight: 600,
54
+ mb: 0.5,
55
+ fontSize: { xs: "1.75rem", md: "2.125rem" },
56
+ }}
57
+ >
58
+ {title}
59
+ </Typography>
60
+ {subtitle && (
61
+ <Typography
62
+ id="page-header-subtitle"
63
+ variant="body1"
64
+ sx={{
65
+ color: "rgba(255,255,255,0.9)",
66
+ fontSize: { xs: "0.875rem", md: "1rem" },
67
+ }}
68
+ >
69
+ {subtitle}
70
+ </Typography>
71
+ )}
72
+ </Box>
73
+ </Stack>
74
+
75
+ {/* Right side: Action Buttons/Tabs */}
76
+ {children && (
77
+ <Stack
78
+ id="page-header-actions"
79
+ direction="row"
80
+ spacing={1}
81
+ sx={{
82
+ flexShrink: 0,
83
+ justifyContent: { xs: "flex-start", md: "flex-end" },
84
+ width: { xs: "100%", md: "auto" },
85
+ }}
86
+ >
87
+ {children}
88
+ </Stack>
89
+ )}
90
+ </Stack>
91
+
92
+ {/* Statistics row */}
93
+ {statistics && statistics.length > 0 && (
94
+ <Stack id="page-header-statistics" direction={{ xs: "column", sm: "row" }} spacing={3} sx={{ mt: 3 }}>
95
+ {statistics.map((stat, index) => (
96
+ <Stack key={index} direction="row" spacing={1} alignItems="center">
97
+ {React.cloneElement(stat.icon as React.ReactElement<any>, { sx: { color: "#FFF", fontSize: 20 } })}
98
+ <Typography variant="h6" sx={{ color: "#FFF", fontWeight: 600, mr: 1 }}>
99
+ {stat.value}
100
+ </Typography>
101
+ <Typography variant="body2" sx={{ color: "rgba(255,255,255,0.9)", fontSize: "0.875rem" }}>
102
+ {stat.label}
103
+ </Typography>
104
+ </Stack>
105
+ ))}
106
+ </Stack>
107
+ )}
108
+ </Box>
109
+ </Box>
110
+ );
111
+ };
@@ -0,0 +1,78 @@
1
+ "use client";
2
+
3
+ import React from "react";
4
+ import { Avatar, SxProps } from "@mui/material";
5
+ import { PersonInterface } from "@churchapps/helpers";
6
+ import { PersonHelper } from "../helpers";
7
+
8
+ interface Props {
9
+ person: PersonInterface;
10
+ size?: "small" | "medium" | "large" | "xlarge" | "xxlarge" | "responsive";
11
+ sx?: SxProps;
12
+ onClick?: () => void;
13
+ }
14
+
15
+ export const PersonAvatar: React.FC<Props> = ({ person, size = "medium", sx, onClick }) => {
16
+ const [imageError, setImageError] = React.useState(false);
17
+
18
+ const getSizeProps = () => {
19
+ switch (size) {
20
+ case "small":
21
+ return { width: 48, height: 48 };
22
+ case "medium":
23
+ return { width: 56, height: 56 };
24
+ case "large":
25
+ return { width: 80, height: 80 };
26
+ case "xlarge":
27
+ return { width: 100, height: 100 };
28
+ case "xxlarge":
29
+ return { width: 120, height: 120 };
30
+ case "responsive":
31
+ return { width: { xs: 70, sm: 80, md: 100 }, height: { xs: 70, sm: 80, md: 100 } };
32
+ default:
33
+ return { width: 56, height: 56 };
34
+ }
35
+ };
36
+
37
+ const getInitials = () => {
38
+ if (!person?.name?.display) return "?";
39
+
40
+ const names = person.name.display.trim().split(" ");
41
+ if (names.length >= 2) {
42
+ return (names[0][0] + names[names.length - 1][0]).toUpperCase();
43
+ } else if (names.length === 1) {
44
+ return names[0][0]?.toUpperCase() || "?";
45
+ }
46
+ return "?";
47
+ };
48
+
49
+ const photoUrl = PersonHelper.getPhotoUrl(person);
50
+ const sizeProps = getSizeProps();
51
+
52
+ // Combine default styles with custom sx
53
+ const combinedSx = {
54
+ ...sizeProps,
55
+ cursor: onClick ? "pointer" : "default",
56
+ "&:hover": onClick ? {
57
+ opacity: 0.8,
58
+ transition: "opacity 0.2s ease-in-out",
59
+ } : {},
60
+ ...sx,
61
+ };
62
+
63
+ const handleImageError = () => {
64
+ setImageError(true);
65
+ };
66
+
67
+ return (
68
+ <Avatar
69
+ src={!imageError ? photoUrl : undefined}
70
+ alt={person?.name?.display || "User avatar"}
71
+ sx={combinedSx}
72
+ onClick={onClick}
73
+ onError={handleImageError}
74
+ >
75
+ {(imageError || !photoUrl) && getInitials()}
76
+ </Avatar>
77
+ );
78
+ };
@@ -7,7 +7,8 @@ import { PrimaryMenu } from "./PrimaryMenu";
7
7
  import { SecondaryMenu } from "./SecondaryMenu";
8
8
  import { SecondaryMenuAlt } from "./SecondaryMenuAlt";
9
9
  import { SupportDrawer } from "./SupportDrawer";
10
- import { UserContextInterface } from "@churchapps/helpers";
10
+ import { UserContextInterface, CommonEnvironmentHelper } from "@churchapps/helpers";
11
+ import { NotificationService } from "../../helpers/NotificationService";
11
12
 
12
13
  type Props = {
13
14
  primaryMenuLabel: string;
@@ -19,11 +20,68 @@ type Props = {
19
20
  onNavigate: (url: string) => void;
20
21
  }
21
22
 
22
- export const SiteHeader = (props:Props) => {
23
+ export const SiteHeader = React.memo((props:Props) => {
24
+ // Initialize NotificationService without subscribing to count changes to prevent re-renders
25
+ React.useEffect(() => {
26
+ const initializeNotifications = async () => {
27
+ if (props.context?.person?.id && props.context?.userChurch?.church?.id) {
28
+ const service = NotificationService.getInstance();
29
+ await service.initialize(props.context);
30
+ }
31
+ };
32
+
33
+ initializeNotifications();
34
+ }, [props.context?.person?.id, props.context?.userChurch?.church?.id]);
35
+
36
+ const refresh = React.useCallback(async () => {
37
+ // Direct access to NotificationService for refresh functionality
38
+ await NotificationService.getInstance().refresh();
39
+ }, []);
40
+
41
+ // Memoize userName to prevent recreation
42
+ const userName = React.useMemo(() => {
43
+ if (props.context?.user) {
44
+ return `${props.context.user.firstName} ${props.context.user.lastName}`;
45
+ }
46
+ return '';
47
+ }, [props.context?.user?.firstName, props.context?.user?.lastName]);
48
+
49
+ // Memoize profilePicture URL
50
+ const profilePicture = React.useMemo(() => {
51
+ return PersonHelper.getPhotoUrl(props.context?.person);
52
+ }, [props.context?.person]);
53
+
54
+ // Create a stable context object to prevent UserMenu recreation
55
+ const stableContext = React.useMemo(() => {
56
+ if (!props.context) return undefined;
57
+
58
+ return {
59
+ user: props.context.user,
60
+ person: props.context.person,
61
+ userChurch: props.context.userChurch,
62
+ userChurches: props.context.userChurches,
63
+ setUser: props.context.setUser,
64
+ setPerson: props.context.setPerson,
65
+ setUserChurch: props.context.setUserChurch,
66
+ setUserChurches: props.context.setUserChurches
67
+ };
68
+ }, [
69
+ props.context?.user?.id,
70
+ props.context?.user?.firstName,
71
+ props.context?.user?.lastName,
72
+ props.context?.person?.id,
73
+ props.context?.userChurch?.church?.id,
74
+ props.context?.userChurches,
75
+ props.context?.setUser,
76
+ props.context?.setPerson,
77
+ props.context?.setUserChurch,
78
+ props.context?.setUserChurches
79
+ ]);
23
80
 
24
81
  const CustomAppBar = styled(AppBar)(
25
82
  ({ theme }) => ({
26
83
  zIndex: theme.zIndex.drawer + 1,
84
+ backgroundColor: "var(--c1, #1565C0)",
27
85
  transition: theme.transitions.create(["width", "margin"], {
28
86
  easing: theme.transitions.easing.sharp,
29
87
  duration: theme.transitions.duration.leavingScreen
@@ -63,21 +121,86 @@ export const SiteHeader = (props:Props) => {
63
121
 
64
122
  /*<Typography variant="h6" noWrap>{UserHelper.currentUserChurch?.church?.name || ""}</Typography>*/
65
123
  return (<>
66
- <div style={{backgroundColor:"var(--c1)", color: "#FFF"}}>
67
- <CustomAppBar position="absolute">
68
- <Toolbar sx={{ pr: "24px", backgroundColor: "var(--c1)" }}>
124
+ <div id="site-header" style={{
125
+ '--c1': '#1565C0',
126
+ '--c1d1': '#1358AD',
127
+ '--c1d2': '#114A99',
128
+ '--c1l2': '#568BDA',
129
+ backgroundColor:"var(--c1)",
130
+ color: "#FFF"
131
+ } as React.CSSProperties}>
132
+ <CustomAppBar id="site-app-bar" position="absolute">
133
+ <Toolbar id="site-toolbar" sx={{ pr: "24px", backgroundColor: "var(--c1)", minHeight: "64px !important" }}>
69
134
  <PrimaryMenu label={props.primaryMenuLabel} menuItems={props.primaryMenuItems} onNavigate={props.onNavigate} />
70
135
  <SecondaryMenu label={props.secondaryMenuLabel} menuItems={props.secondaryMenuItems} onNavigate={props.onNavigate} />
71
- <div style={{ flex: 1 }}>
136
+ <div id="secondary-menu-container" style={{ flex: 1 }}>
72
137
  <SecondaryMenuAlt label={props.secondaryMenuLabel} menuItems={props.secondaryMenuItems} onNavigate={props.onNavigate} />
73
138
  </div>
74
- {UserHelper.user && <UserMenu profilePicture={PersonHelper.getPhotoUrl(props.context?.person)} userName={`${UserHelper.user?.firstName} ${UserHelper.user?.lastName}`} userChurches={UserHelper.userChurches} currentUserChurch={UserHelper.currentUserChurch} context={props.context} appName={props.appName} loadCounts={() => {}} notificationCounts={{notificationCount:0, pmCount:0}} onNavigate={props.onNavigate} />}
75
- {!UserHelper.user && <Link href="/login" color="inherit" style={{ textDecoration: "none" }}>Login</Link>}
139
+ {props.context?.user?.id && (
140
+ <UserMenu
141
+ key="user-menu-stable"
142
+ profilePicture={profilePicture}
143
+ userName={userName}
144
+ userChurches={props.context?.userChurches}
145
+ currentUserChurch={props.context?.userChurch}
146
+ context={stableContext}
147
+ appName={props.appName}
148
+ loadCounts={refresh}
149
+ notificationCounts={{notificationCount: 0, pmCount: 0}}
150
+ onNavigate={props.onNavigate}
151
+ />
152
+ )}
153
+ {!props.context?.user?.id && <Link id="login-link" href="/login" color="inherit" style={{ textDecoration: "none" }}>Login</Link>}
76
154
  <SupportDrawer appName={props.appName} relatedArticles={getRelatedArticles()} />
77
155
  </Toolbar>
78
156
  </CustomAppBar>
79
-
157
+ <div id="app-bar-spacer" style={{ height: '64px' }}></div>
80
158
  </div>
81
159
  </>
82
160
  );
83
- }
161
+ }, (prevProps, nextProps) => {
162
+ // Custom comparison to prevent unnecessary re-renders
163
+
164
+ // Check if essential props have changed
165
+ if (prevProps.primaryMenuLabel !== nextProps.primaryMenuLabel ||
166
+ prevProps.secondaryMenuLabel !== nextProps.secondaryMenuLabel ||
167
+ prevProps.appName !== nextProps.appName) {
168
+ return false;
169
+ }
170
+
171
+ // Check if menu items arrays have changed (shallow comparison)
172
+ if (prevProps.primaryMenuItems?.length !== nextProps.primaryMenuItems?.length ||
173
+ prevProps.secondaryMenuItems?.length !== nextProps.secondaryMenuItems?.length) {
174
+ return false;
175
+ }
176
+
177
+ // Check if user context has actually changed (deep comparison of essential parts)
178
+ const prevUser = prevProps.context?.user;
179
+ const nextUser = nextProps.context?.user;
180
+
181
+ if (prevUser?.id !== nextUser?.id ||
182
+ prevUser?.firstName !== nextUser?.firstName ||
183
+ prevUser?.lastName !== nextUser?.lastName) {
184
+ return false;
185
+ }
186
+
187
+ // Check if person context has changed
188
+ if (prevProps.context?.person?.id !== nextProps.context?.person?.id) {
189
+ return false;
190
+ }
191
+
192
+ // Check if church context has changed
193
+ if (prevProps.context?.userChurch?.church?.id !== nextProps.context?.userChurch?.church?.id) {
194
+ return false;
195
+ }
196
+
197
+ // Check if onNavigate function reference has changed
198
+ if (prevProps.onNavigate !== nextProps.onNavigate) {
199
+ return false;
200
+ }
201
+
202
+ // All essential props are the same, skip re-render
203
+ return true;
204
+ });
205
+
206
+ SiteHeader.displayName = 'SiteHeader';
@@ -33,7 +33,7 @@ export const SupportDrawer = (props: Props) => {
33
33
 
34
34
  const loadData = () => {
35
35
  if (UserHelper?.currentUserChurch?.church?.id) {
36
- ApiHelper.get("/settings/public/" + UserHelper.currentUserChurch.church.id, "MembershipApi").then((data) => {
36
+ ApiHelper.get("/settings/public/" + UserHelper.currentUserChurch.church.id, "MembershipApi").then((data: any) => {
37
37
  const contactRes = data?.supportContact;
38
38
  if (contactRes && contactRes !== "") setSupportContact(contactRes);
39
39
 
@@ -8,6 +8,8 @@ export { ImageEditor } from "./ImageEditor";
8
8
  export { InputBox } from "./InputBox";
9
9
  export { Loading } from "./Loading";
10
10
  export { Notes } from "./notes/Notes";
11
+ export { PageHeader } from "./PageHeader";
12
+ export { PersonAvatar } from "./PersonAvatar";
11
13
  export { QuestionEdit } from "./QuestionEdit";
12
14
  export { SmallButton } from "./SmallButton";
13
15
  export { SupportModal } from "./SupportModal";
@@ -3,7 +3,16 @@
3
3
  import React, { useState, useEffect } from "react"
4
4
  import { ApiHelper, Locale, PersonHelper } from "../../helpers"
5
5
  import { MessageInterface, UserContextInterface } from "@churchapps/helpers"
6
- import { Icon, Stack, TextField } from "@mui/material"
6
+ import {
7
+ Box,
8
+ Stack,
9
+ TextField,
10
+ IconButton,
11
+ Paper,
12
+ CircularProgress,
13
+ Avatar
14
+ } from "@mui/material"
15
+ import { Send as SendIcon, Delete as DeleteIcon } from "@mui/icons-material"
7
16
  import { ErrorMessages } from "../ErrorMessages"
8
17
  import { SmallButton } from "../SmallButton"
9
18
 
@@ -22,7 +31,7 @@ export function AddNote({ context, ...props }: Props) {
22
31
  const headerText = props.messageId ? "Edit note" : "Add a note"
23
32
 
24
33
  useEffect(() => {
25
- if (props.messageId) ApiHelper.get(`/messages/${props.messageId}`, "MessagingApi").then(n => setMessage(n));
34
+ if (props.messageId) ApiHelper.get(`/messages/${props.messageId}`, "MessagingApi").then((n: any) => setMessage(n));
26
35
  else setMessage({ conversationId: props.conversationId, content: "" });
27
36
  return () => {
28
37
  setMessage(null);
@@ -38,7 +47,7 @@ export function AddNote({ context, ...props }: Props) {
38
47
 
39
48
  const validate = () => {
40
49
  const result = [];
41
- if (!message.content.trim()) result.push(Locale.label("notes.validate.content"));
50
+ if (!message.content.trim()) result.push(Locale.label("notes.validate.content", "Please enter a message"));
42
51
  setErrors(result);
43
52
  return result.length === 0;
44
53
  }
@@ -58,8 +67,13 @@ export function AddNote({ context, ...props }: Props) {
58
67
  m.content = "";
59
68
  setMessage(m);
60
69
  })
61
- .catch((error) => {
62
- if (error?.message === "Forbidden") setErrors(["You can't edit the message sent by others."])
70
+ .catch((error: any) => {
71
+ console.error("Error saving message:", error);
72
+ if (error?.message === "Forbidden") {
73
+ setErrors(["You can't edit the message sent by others."]);
74
+ } else {
75
+ setErrors([error?.message || "Failed to save message. Please try again."]);
76
+ }
63
77
  })
64
78
  .finally(() => { setIsSubmitting(false); });
65
79
  }
@@ -75,21 +89,93 @@ export function AddNote({ context, ...props }: Props) {
75
89
  const image = PersonHelper.getPhotoUrl(context?.person)
76
90
 
77
91
  return (
78
- <>
92
+ <Box sx={{ width: '100%' }}>
79
93
  <ErrorMessages errors={errors} />
80
-
81
- <Stack direction="row" spacing={1.5} style={{ marginTop: 15 }} justifyContent="end">
82
-
83
- {image ? <img src={image} alt="user" style={{ width: 60, height: 45, borderRadius: 5, marginLeft: 8 }} /> : <Icon>person</Icon>}
84
- <Stack direction="column" spacing={2} style={{ width: "100%" }} justifyContent="end">
85
- <div><b>{context?.person?.name?.display}</b></div>
86
- <TextField fullWidth name="noteText" aria-label={headerText} placeholder="Add a note" multiline style={{ marginTop: 0, border: "none" }} variant="standard" onChange={handleChange} value={message?.content} />
87
- </Stack>
88
- <Stack direction="column" spacing={1} justifyContent="end">
89
- <SmallButton icon="send" onClick={handleSave} />
90
- {deleteFunction && <SmallButton icon="delete" onClick={deleteFunction} disabled={isSubmitting} />}
94
+
95
+ <Paper
96
+ variant="outlined"
97
+ sx={{
98
+ p: 2,
99
+ bgcolor: 'grey.50',
100
+ borderColor: 'grey.300'
101
+ }}
102
+ >
103
+ <Stack direction="row" spacing={2} alignItems="flex-start">
104
+ <Avatar
105
+ src={image}
106
+ alt={context?.person?.name?.display}
107
+ sx={{ width: 48, height: 48 }}
108
+ />
109
+
110
+ <Box sx={{ flex: 1 }}>
111
+ <TextField
112
+ fullWidth
113
+ multiline
114
+ rows={2}
115
+ name="noteText"
116
+ aria-label={headerText}
117
+ placeholder={props.messageId ? "Edit your message..." : "Type a message..."}
118
+ variant="standard"
119
+ value={message?.content || ''}
120
+ onChange={handleChange}
121
+ disabled={isSubmitting}
122
+ InputProps={{
123
+ disableUnderline: true,
124
+ sx: {
125
+ fontSize: '1rem',
126
+ '& textarea': {
127
+ resize: 'vertical',
128
+ minHeight: '40px'
129
+ }
130
+ }
131
+ }}
132
+ sx={{
133
+ bgcolor: 'white',
134
+ borderRadius: 1,
135
+ p: 1,
136
+ border: '1px solid',
137
+ borderColor: 'grey.300',
138
+ '&:hover': {
139
+ borderColor: 'grey.400'
140
+ },
141
+ '&.Mui-focused': {
142
+ borderColor: 'primary.main'
143
+ }
144
+ }}
145
+ />
146
+
147
+ <Box sx={{ display: 'flex', justifyContent: 'flex-end', mt: 1, gap: 0.5 }}>
148
+ {deleteFunction && (
149
+ <IconButton
150
+ size="small"
151
+ onClick={deleteFunction}
152
+ disabled={isSubmitting}
153
+ sx={{ color: 'error.main' }}
154
+ >
155
+ <DeleteIcon fontSize="small" />
156
+ </IconButton>
157
+ )}
158
+ <IconButton
159
+ size="small"
160
+ color="primary"
161
+ onClick={handleSave}
162
+ disabled={isSubmitting || !message?.content?.trim()}
163
+ sx={{
164
+ bgcolor: 'primary.main',
165
+ color: 'white',
166
+ '&:hover': { bgcolor: 'primary.dark' },
167
+ '&:disabled': {
168
+ bgcolor: 'action.disabledBackground',
169
+ color: 'action.disabled'
170
+ }
171
+ }}
172
+ >
173
+ {isSubmitting ? <CircularProgress size={18} color="inherit" /> : <SendIcon fontSize="small" />}
174
+ </IconButton>
175
+ </Box>
176
+ </Box>
91
177
  </Stack>
92
- </Stack>
93
- </>
178
+ </Paper>
179
+ </Box>
94
180
  );
95
181
  }