Skip to content

Commit a84acdc

Browse files
committed
Added mobile themes
1 parent 3cc6233 commit a84acdc

File tree

4 files changed

+223
-97
lines changed

4 files changed

+223
-97
lines changed

src/attendance/components/CheckinThemeEdit.tsx

Lines changed: 48 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,6 @@ import { ExpandMore, Delete as DeleteIcon, Add as AddIcon } from "@mui/icons-mat
44
import { ApiHelper, InputBox, ImageEditor } from "@churchapps/apphelper";
55
import type { GenericSettingInterface } from "@churchapps/helpers";
66

7-
interface CheckinThemeColors {
8-
primary: string;
9-
primaryContrast: string;
10-
secondary: string;
11-
secondaryContrast: string;
12-
headerBackground: string;
13-
subheaderBackground: string;
14-
buttonBackground: string;
15-
buttonText: string;
16-
}
17-
187
interface IdleSlide {
198
imageUrl: string;
209
durationSeconds: number;
@@ -27,31 +16,18 @@ interface IdleScreenConfig {
2716
slides: IdleSlide[];
2817
}
2918

30-
interface CheckinThemeConfig {
31-
colors: CheckinThemeColors;
19+
interface CheckinSettingsConfig {
3220
backgroundImage: string;
3321
idleScreen: IdleScreenConfig;
3422
}
3523

36-
const DEFAULT_COLORS: CheckinThemeColors = {
37-
primary: "#1565C0",
38-
primaryContrast: "#FFFFFF",
39-
secondary: "#568BDA",
40-
secondaryContrast: "#FFFFFF",
41-
headerBackground: "#1565C0",
42-
subheaderBackground: "#568BDA",
43-
buttonBackground: "#1565C0",
44-
buttonText: "#FFFFFF"
45-
};
46-
47-
const DEFAULT_THEME: CheckinThemeConfig = {
48-
colors: DEFAULT_COLORS,
24+
const DEFAULT_SETTINGS: CheckinSettingsConfig = {
4925
backgroundImage: "",
5026
idleScreen: { enabled: false, timeoutSeconds: 120, slides: [] }
5127
};
5228

5329
export const CheckinThemeEdit: React.FC = () => {
54-
const [themeConfig, setThemeConfig] = React.useState<CheckinThemeConfig>({ ...DEFAULT_THEME });
30+
const [config, setConfig] = React.useState<CheckinSettingsConfig>({ ...DEFAULT_SETTINGS });
5531
const [setting, setSetting] = React.useState<GenericSettingInterface | null>(null);
5632
const [isSubmitting, setIsSubmitting] = React.useState(false);
5733
const [editingImage, setEditingImage] = React.useState<string | null>(null);
@@ -60,18 +36,30 @@ export const CheckinThemeEdit: React.FC = () => {
6036
const loadData = React.useCallback(async () => {
6137
try {
6238
const allSettings: GenericSettingInterface[] = await ApiHelper.get("/settings", "MembershipApi");
63-
const themeSetting = allSettings.find(s => s.keyName === "checkinTheme");
39+
// Try new key first, fall back to legacy checkinTheme
40+
let themeSetting = allSettings.find(s => s.keyName === "checkinSettings");
41+
if (!themeSetting) {
42+
const legacy = allSettings.find(s => s.keyName === "checkinTheme");
43+
if (legacy?.value) {
44+
const parsed = JSON.parse(legacy.value);
45+
setSetting(null);
46+
setConfig({
47+
backgroundImage: parsed.backgroundImage || "",
48+
idleScreen: { ...DEFAULT_SETTINGS.idleScreen, ...(parsed.idleScreen || {}) }
49+
});
50+
return;
51+
}
52+
}
6453
if (themeSetting?.value) {
6554
setSetting(themeSetting);
6655
const parsed = JSON.parse(themeSetting.value);
67-
setThemeConfig({
68-
colors: { ...DEFAULT_COLORS, ...(parsed.colors || {}) },
56+
setConfig({
6957
backgroundImage: parsed.backgroundImage || "",
70-
idleScreen: { ...DEFAULT_THEME.idleScreen, ...(parsed.idleScreen || {}) }
58+
idleScreen: { ...DEFAULT_SETTINGS.idleScreen, ...(parsed.idleScreen || {}) }
7159
});
7260
}
7361
} catch (error) {
74-
console.error("Error loading checkin theme:", error);
62+
console.error("Error loading checkin settings:", error);
7563
}
7664
}, []);
7765

@@ -80,42 +68,36 @@ export const CheckinThemeEdit: React.FC = () => {
8068
const handleSave = async () => {
8169
setIsSubmitting(true);
8270
try {
83-
const s: GenericSettingInterface = setting || { keyName: "checkinTheme", public: 1 };
84-
s.value = JSON.stringify(themeConfig);
71+
const s: GenericSettingInterface = setting || { keyName: "checkinSettings", public: 1 };
72+
s.value = JSON.stringify(config);
8573
await ApiHelper.post("/settings", [s], "MembershipApi");
86-
// Reload to get the saved setting with ID
8774
await loadData();
8875
} catch (error) {
89-
console.error("Error saving checkin theme:", error);
76+
console.error("Error saving checkin settings:", error);
9077
} finally {
9178
setIsSubmitting(false);
9279
}
9380
};
9481

95-
const updateColor = (key: keyof CheckinThemeColors, value: string) => {
96-
setThemeConfig(prev => ({ ...prev, colors: { ...prev.colors, [key]: value } }));
97-
};
98-
9982
const handleBackgroundImageUpdate = async (dataUrl: string) => {
10083
if (!dataUrl) { setEditingImage(null); return; }
101-
// Save image as separate setting so API converts base64 to S3 URL
102-
const imgSetting: GenericSettingInterface = { keyName: "checkinTheme_bg", value: dataUrl, public: 1 };
84+
const imgSetting: GenericSettingInterface = { keyName: "checkinSettings_bg", value: dataUrl, public: 1 };
10385
const saved = await ApiHelper.post("/settings", [imgSetting], "MembershipApi");
104-
const result = saved?.checkinTheme_bg || saved?.find?.((s: any) => s.keyName === "checkinTheme_bg");
86+
const result = saved?.checkinSettings_bg || saved?.find?.((s: any) => s.keyName === "checkinSettings_bg");
10587
if (result?.value) {
106-
setThemeConfig(prev => ({ ...prev, backgroundImage: result.value }));
88+
setConfig(prev => ({ ...prev, backgroundImage: result.value }));
10789
}
10890
setEditingImage(null);
10991
};
11092

11193
const handleSlideImageUpdate = async (dataUrl: string) => {
11294
if (!dataUrl) { setEditingImage(null); return; }
113-
const slideKey = "checkinTheme_slide_" + editingSlideIndex;
95+
const slideKey = "checkinSettings_slide_" + editingSlideIndex;
11496
const imgSetting: GenericSettingInterface = { keyName: slideKey, value: dataUrl, public: 1 };
11597
const saved = await ApiHelper.post("/settings", [imgSetting], "MembershipApi");
11698
const result = saved?.[slideKey] || saved?.find?.((s: any) => s.keyName === slideKey);
11799
if (result?.value) {
118-
setThemeConfig(prev => {
100+
setConfig(prev => {
119101
const slides = [...prev.idleScreen.slides];
120102
if (editingSlideIndex < slides.length) {
121103
slides[editingSlideIndex] = { ...slides[editingSlideIndex], imageUrl: result.value };
@@ -129,88 +111,60 @@ export const CheckinThemeEdit: React.FC = () => {
129111
};
130112

131113
const addSlide = () => {
132-
setEditingSlideIndex(themeConfig.idleScreen.slides.length);
114+
setEditingSlideIndex(config.idleScreen.slides.length);
133115
setEditingImage("slide");
134116
};
135117

136118
const removeSlide = (index: number) => {
137-
setThemeConfig(prev => {
119+
setConfig(prev => {
138120
const slides = prev.idleScreen.slides.filter((_, i) => i !== index);
139121
return { ...prev, idleScreen: { ...prev.idleScreen, slides } };
140122
});
141123
};
142124

143125
const updateSlideDuration = (index: number, duration: number) => {
144-
setThemeConfig(prev => {
126+
setConfig(prev => {
145127
const slides = [...prev.idleScreen.slides];
146128
slides[index] = { ...slides[index], durationSeconds: duration };
147129
return { ...prev, idleScreen: { ...prev.idleScreen, slides } };
148130
});
149131
};
150132

151-
const colorField = (label: string, key: keyof CheckinThemeColors) => (
152-
<Stack direction="row" spacing={2} alignItems="center" sx={{ mb: 1.5 }}>
153-
<TextField
154-
type="color"
155-
label={label}
156-
value={themeConfig.colors[key]}
157-
onChange={e => updateColor(key, e.target.value)}
158-
sx={{ width: 120 }}
159-
size="small"
160-
/>
161-
<Box sx={{ width: 60, height: 32, borderRadius: 1, backgroundColor: themeConfig.colors[key], border: "1px solid #ccc" }} />
162-
<Typography variant="body2" color="text.secondary">{themeConfig.colors[key]}</Typography>
163-
</Stack>
164-
);
165-
166133
if (editingImage === "background") {
167-
return <ImageEditor aspectRatio={16 / 9} photoUrl={themeConfig.backgroundImage} onCancel={() => setEditingImage(null)} onUpdate={handleBackgroundImageUpdate} outputWidth={1920} outputHeight={1080} />;
134+
return <ImageEditor aspectRatio={16 / 9} photoUrl={config.backgroundImage} onCancel={() => setEditingImage(null)} onUpdate={handleBackgroundImageUpdate} outputWidth={1920} outputHeight={1080} />;
168135
}
169136

170137
if (editingImage === "slide") {
171-
const currentUrl = editingSlideIndex < themeConfig.idleScreen.slides.length ? themeConfig.idleScreen.slides[editingSlideIndex]?.imageUrl : "";
138+
const currentUrl = editingSlideIndex < config.idleScreen.slides.length ? config.idleScreen.slides[editingSlideIndex]?.imageUrl : "";
172139
return <ImageEditor aspectRatio={16 / 9} photoUrl={currentUrl || ""} onCancel={() => setEditingImage(null)} onUpdate={handleSlideImageUpdate} outputWidth={1920} outputHeight={1080} />;
173140
}
174141

175142
return (
176-
<InputBox headerText="Kiosk Theme" headerIcon="palette" saveFunction={handleSave} isSubmitting={isSubmitting}>
177-
{/* Colors Section */}
178-
<Accordion defaultExpanded>
179-
<AccordionSummary expandIcon={<ExpandMore />}>
180-
<Typography variant="subtitle1" fontWeight={600}>Colors</Typography>
181-
</AccordionSummary>
182-
<AccordionDetails>
183-
{colorField("Primary", "primary")}
184-
{colorField("Primary Contrast", "primaryContrast")}
185-
{colorField("Secondary", "secondary")}
186-
{colorField("Secondary Contrast", "secondaryContrast")}
187-
{colorField("Header Background", "headerBackground")}
188-
{colorField("Subheader Background", "subheaderBackground")}
189-
{colorField("Button Background", "buttonBackground")}
190-
{colorField("Button Text", "buttonText")}
191-
</AccordionDetails>
192-
</Accordion>
143+
<InputBox headerText="Kiosk Settings" headerIcon="settings" saveFunction={handleSave} isSubmitting={isSubmitting}>
144+
<Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>
145+
Kiosk colors are managed in Settings &gt; App Theme. Configure background images and idle screen behavior below.
146+
</Typography>
193147

194148
{/* Background Image Section */}
195-
<Accordion>
149+
<Accordion defaultExpanded>
196150
<AccordionSummary expandIcon={<ExpandMore />}>
197151
<Typography variant="subtitle1" fontWeight={600}>Background Image</Typography>
198152
</AccordionSummary>
199153
<AccordionDetails>
200154
<Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>
201155
Optional background image for the lookup/welcome screen. Recommended: 1920x1080.
202156
</Typography>
203-
{themeConfig.backgroundImage && (
157+
{config.backgroundImage && (
204158
<Box sx={{ mb: 2 }}>
205-
<img src={themeConfig.backgroundImage} alt="Background" style={{ maxWidth: 300, maxHeight: 170, borderRadius: 8, border: "1px solid #ccc" }} />
159+
<img src={config.backgroundImage} alt="Background" style={{ maxWidth: 300, maxHeight: 170, borderRadius: 8, border: "1px solid #ccc" }} />
206160
</Box>
207161
)}
208162
<Stack direction="row" spacing={2}>
209163
<Button variant="outlined" onClick={() => setEditingImage("background")}>
210-
{themeConfig.backgroundImage ? "Change Image" : "Upload Image"}
164+
{config.backgroundImage ? "Change Image" : "Upload Image"}
211165
</Button>
212-
{themeConfig.backgroundImage && (
213-
<Button variant="outlined" color="error" onClick={() => setThemeConfig(prev => ({ ...prev, backgroundImage: "" }))}>
166+
{config.backgroundImage && (
167+
<Button variant="outlined" color="error" onClick={() => setConfig(prev => ({ ...prev, backgroundImage: "" }))}>
214168
Remove
215169
</Button>
216170
)}
@@ -227,24 +181,24 @@ export const CheckinThemeEdit: React.FC = () => {
227181
<FormControlLabel
228182
control={
229183
<Switch
230-
checked={themeConfig.idleScreen.enabled}
231-
onChange={e => setThemeConfig(prev => ({ ...prev, idleScreen: { ...prev.idleScreen, enabled: e.target.checked } }))}
184+
checked={config.idleScreen.enabled}
185+
onChange={e => setConfig(prev => ({ ...prev, idleScreen: { ...prev.idleScreen, enabled: e.target.checked } }))}
232186
/>
233187
}
234188
label="Enable idle screen"
235189
/>
236190
<TextField
237191
type="number"
238192
label="Timeout (seconds)"
239-
value={themeConfig.idleScreen.timeoutSeconds}
240-
onChange={e => setThemeConfig(prev => ({ ...prev, idleScreen: { ...prev.idleScreen, timeoutSeconds: parseInt(e.target.value) || 120 } }))}
193+
value={config.idleScreen.timeoutSeconds}
194+
onChange={e => setConfig(prev => ({ ...prev, idleScreen: { ...prev.idleScreen, timeoutSeconds: parseInt(e.target.value) || 120 } }))}
241195
size="small"
242196
sx={{ mt: 2, mb: 3, width: 200 }}
243197
slotProps={{ htmlInput: { min: 10 } }}
244198
/>
245199

246200
<Typography variant="subtitle2" sx={{ mb: 1 }}>Slides</Typography>
247-
{themeConfig.idleScreen.slides.map((slide, index) => (
201+
{config.idleScreen.slides.map((slide, index) => (
248202
<Stack key={index} direction="row" spacing={2} alignItems="center" sx={{ mb: 2, p: 1.5, border: "1px solid #e0e0e0", borderRadius: 2 }}>
249203
{slide.imageUrl && (
250204
<img src={slide.imageUrl} alt={`Slide ${index + 1}`} style={{ width: 120, height: 68, objectFit: "cover", borderRadius: 4 }} />

src/settings/ManageChurch.tsx

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,9 @@ import { type ChurchInterface } from "@churchapps/helpers";
33
import { UserHelper, Permissions, Locale, ApiHelper, Loading, PageHeader } from "@churchapps/apphelper";
44
import { Navigate, useNavigate, useLocation } from "react-router-dom";
55
import { Box, Stack, Button } from "@mui/material";
6-
import { Lock as LockIcon, PlayArrow as PlayArrowIcon, Edit as EditIcon, PhoneIphone as PhoneIphoneIcon, History as HistoryIcon } from "@mui/icons-material";
6+
import { Lock as LockIcon, PlayArrow as PlayArrowIcon, Edit as EditIcon, PhoneIphone as PhoneIphoneIcon, History as HistoryIcon, Palette as PaletteIcon } from "@mui/icons-material";
77
import { RolesTab, ChurchSettingsEdit } from "./components";
8+
import { AppThemeEdit } from "./components/AppThemeEdit";
89
import { MobileAppSettingsPage } from "./MobileAppSettingsPage";
910
import { useQuery } from "@tanstack/react-query";
1011

@@ -40,6 +41,7 @@ export const ManageChurch = () => {
4041
switch (selectedTab) {
4142
case "roles": return <RolesTab church={church.data} />;
4243
case "mobileApps": return <MobileAppSettingsPage />;
44+
case "appTheme": return <AppThemeEdit />;
4345
default: return <div></div>;
4446
}
4547
}
@@ -98,6 +100,21 @@ export const ManageChurch = () => {
98100
}}>
99101
{Locale.label("settings.manageChurch.mobileApps")}
100102
</Button>
103+
<Button
104+
variant={selectedTab === "appTheme" ? "contained" : "outlined"}
105+
startIcon={<PaletteIcon />}
106+
onClick={() => setSelectedTab("appTheme")}
107+
sx={{
108+
color: selectedTab === "appTheme" ? "primary.main" : "#FFF",
109+
backgroundColor: selectedTab === "appTheme" ? "#FFF" : "transparent",
110+
borderColor: "#FFF",
111+
"&:hover": {
112+
backgroundColor: selectedTab === "appTheme" ? "#FFF" : "rgba(255,255,255,0.2)",
113+
color: selectedTab === "appTheme" ? "primary.main" : "#FFF"
114+
}
115+
}}>
116+
App Theme
117+
</Button>
101118
<Button
102119
variant={selectedTab === "roles" ? "contained" : "outlined"}
103120
startIcon={<LockIcon />}
@@ -158,7 +175,7 @@ export const ManageChurch = () => {
158175
)}
159176

160177
{/* Tab Content - hidden when editing church settings */}
161-
{!showChurchSettings && (selectedTab === "roles" || selectedTab === "mobileApps") && <Box sx={{ p: 2 }}>{getCurrentTab()}</Box>}
178+
{!showChurchSettings && (selectedTab === "roles" || selectedTab === "mobileApps" || selectedTab === "appTheme") && <Box sx={{ p: 2 }}>{getCurrentTab()}</Box>}
162179
</>
163180
);
164181
};

0 commit comments

Comments
 (0)