@@ -4,17 +4,6 @@ import { ExpandMore, Delete as DeleteIcon, Add as AddIcon } from "@mui/icons-mat
44import { ApiHelper , InputBox , ImageEditor } from "@churchapps/apphelper" ;
55import 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-
187interface 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
5329export 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 > 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 } } />
0 commit comments