@@ -13,17 +13,24 @@ const emailCelebration = ref(false);
1313const emailEncouragement = ref (false );
1414const acknowledgeCloud = ref (false );
1515
16+ // Prevent watchers from firing during server-to-local sync
17+ const syncing = ref (false );
18+
1619// Watch settings to sync form
1720watch (
1821 () => settings .value ,
1922 (s ) => {
2023 if (! s ) return ;
24+ syncing .value = true ;
2125 celebrationEnabled .value = s .celebrationEnabled ;
2226 encouragementEnabled .value = s .encouragementEnabled ;
2327 celebrationTier .value = s .celebrationTier ;
2428 emailCelebration .value = s .deliveryChannels .celebration .email ;
2529 emailEncouragement .value = s .deliveryChannels .encouragement .email ;
2630 acknowledgeCloud .value = s .privacy .cloudAcknowledged ;
31+ nextTick (() => {
32+ syncing .value = false ;
33+ });
2734 },
2835 { immediate: true },
2936);
@@ -39,54 +46,120 @@ const needsCloudAck = computed(() => {
3946 return isCloud && ! settings .value ?.privacy .cloudAcknowledged && ! acknowledgeCloud .value ;
4047});
4148
42- async function handleSave() {
43- const result = await saveSettings ({
44- celebrationEnabled: celebrationEnabled .value ,
45- encouragementEnabled: encouragementEnabled .value ,
46- celebrationTier: celebrationTier .value ,
49+ // Auto-save helper
50+ async function autoSave(input : Parameters <typeof saveSettings >[0 ]) {
51+ const result = await saveSettings (input );
52+ if (! result ?.saved && error .value ) {
53+ showError (error .value );
54+ }
55+ }
56+
57+ // Per-field watchers for auto-save
58+ watch (encouragementEnabled , (val ) => {
59+ if (syncing .value ) return ;
60+ const input: Parameters <typeof saveSettings >[0 ] = { encouragementEnabled: val };
61+ // Auto-enable email delivery when opting in
62+ if (val && settings .value ?.email .configured && ! settings .value ?.email .unsubscribed ) {
63+ emailEncouragement .value = true ;
64+ input .deliveryChannels = {
65+ celebration: { inApp: true , email: emailCelebration .value , push: false },
66+ encouragement: { inApp: true , email: true , push: false },
67+ };
68+ }
69+ autoSave (input );
70+ });
71+
72+ watch (celebrationEnabled , (val ) => {
73+ if (syncing .value ) return ;
74+ const input: Parameters <typeof saveSettings >[0 ] = { celebrationEnabled: val };
75+ // Auto-enable email delivery when opting in
76+ if (val && settings .value ?.email .configured && ! settings .value ?.email .unsubscribed ) {
77+ emailCelebration .value = true ;
78+ input .deliveryChannels = {
79+ celebration: { inApp: true , email: true , push: false },
80+ encouragement: { inApp: true , email: emailEncouragement .value , push: false },
81+ };
82+ }
83+ autoSave (input );
84+ });
85+
86+ watch (celebrationTier , (val ) => {
87+ if (syncing .value ) return ;
88+ autoSave ({ celebrationTier: val });
89+ });
90+
91+ watch (emailCelebration , (val ) => {
92+ if (syncing .value ) return ;
93+ autoSave ({
4794 deliveryChannels: {
48- celebration: {
49- inApp: true ,
50- email: emailCelebration .value ,
51- push: false ,
52- },
53- encouragement: {
54- inApp: true ,
55- email: emailEncouragement .value ,
56- push: false ,
57- },
95+ celebration: { inApp: true , email: val , push: false },
96+ encouragement: { inApp: true , email: emailEncouragement .value , push: false },
5897 },
59- acknowledgeCloudPrivacy: acknowledgeCloud .value || undefined ,
6098 });
99+ });
61100
62- if (result ?.saved ) {
63- showSuccess (" Weekly rhythm settings saved" );
64- } else if (error .value ) {
65- showError (error .value );
101+ watch (emailEncouragement , (val ) => {
102+ if (syncing .value ) return ;
103+ autoSave ({
104+ deliveryChannels: {
105+ celebration: { inApp: true , email: emailCelebration .value , push: false },
106+ encouragement: { inApp: true , email: val , push: false },
107+ },
108+ });
109+ });
110+
111+ // Cloud ack: only auto-save when checked (intentional friction)
112+ watch (acknowledgeCloud , (val ) => {
113+ if (syncing .value || ! val ) return ;
114+ autoSave ({ acknowledgeCloudPrivacy: true });
115+ });
116+
117+ // Test email
118+ const sendingTestCelebration = ref (false );
119+ const sendingTestEncouragement = ref (false );
120+
121+ async function sendTestEmail(kind : " celebration" | " encouragement" ) {
122+ const sending = kind === " celebration" ? sendingTestCelebration : sendingTestEncouragement ;
123+ sending .value = true ;
124+ try {
125+ await $fetch (" /api/weekly-rhythms/test-email" , {
126+ method: " POST" ,
127+ body: { kind },
128+ });
129+ const label = kind === " celebration" ? " celebration" : " encouragement" ;
130+ showSuccess (` Test ${label } email sent! Check your inbox. ` );
131+ } catch (err : unknown ) {
132+ const message =
133+ err && typeof err === " object" && " data" in err
134+ ? (err as { data? : { message? : string } }).data ?.message
135+ : null ;
136+ showError (message || " Something went wrong sending the test email. Please try again later." );
137+ } finally {
138+ sending .value = false ;
66139 }
67140}
68141 </script >
69142
70143<template >
71144 <div >
72- <div v-if =" loading" class =" text-sm text-stone-500 dark:text-stone-400 py-4" >
73- Loading weekly rhythm settings...
145+ <div v-if =" loading" class =" text-sm text-stone-500 dark:text-stone-400 py-4 px-4 " >
146+ Loading rhythm settings...
74147 </div >
75148
76- <div v-else class =" space-y-4 " >
77- <!-- Celebration toggle -->
149+ <div v-else class =" space-y-0 " >
150+ <!-- Mid-Week Encouragement toggle -->
78151 <div class =" p-4 flex items-center justify-between" >
79152 <div >
80153 <div class =" font-medium text-sm text-stone-800 dark:text-stone-100" >
81- Weekly Celebration
154+ Mid-Week Encouragement
82155 </div >
83156 <p class =" text-xs text-stone-500 dark:text-stone-400 mt-0.5" >
84- Receive a stats summary every Monday morning
157+ A gentle Thursday check-in with stretch goals
85158 </p >
86159 </div >
87160 <label class =" relative inline-flex items-center cursor-pointer" >
88161 <input
89- v-model =" celebrationEnabled "
162+ v-model =" encouragementEnabled "
90163 type =" checkbox"
91164 class =" sr-only peer"
92165 />
@@ -96,19 +169,19 @@ async function handleSave() {
96169 </label >
97170 </div >
98171
99- <!-- Encouragement toggle -->
172+ <!-- Weekly Celebration toggle -->
100173 <div class =" p-4 flex items-center justify-between" >
101174 <div >
102175 <div class =" font-medium text-sm text-stone-800 dark:text-stone-100" >
103- Thursday Encouragement
176+ Weekly Celebration
104177 </div >
105178 <p class =" text-xs text-stone-500 dark:text-stone-400 mt-0.5" >
106- A mid-week check-in with gentle stretch goals
179+ Receive a stats summary every Monday morning
107180 </p >
108181 </div >
109182 <label class =" relative inline-flex items-center cursor-pointer" >
110183 <input
111- v-model =" encouragementEnabled "
184+ v-model =" celebrationEnabled "
112185 type =" checkbox"
113186 class =" sr-only peer"
114187 />
@@ -161,48 +234,76 @@ async function handleSave() {
161234 (unsubscribed)
162235 </span >
163236 </p >
164- <div class =" space-y-2" >
165- <label
237+ <div class =" space-y-3" >
238+ <!-- Email celebration toggle -->
239+ <div
166240 v-if =" celebrationEnabled"
167- class =" flex items-center gap-2 text-sm text-stone-700 dark:text-stone-300 "
241+ class =" flex items-center justify-between "
168242 >
169- <input
170- v-model =" emailCelebration"
171- type =" checkbox"
172- class =" rounded border-stone-300 text-emerald-500 focus:ring-emerald-500"
173- :disabled =" settings.email.unsubscribed"
174- />
175- Email celebrations
176- </label >
177- <label
243+ <span class =" text-sm text-stone-700 dark:text-stone-300" >Email celebrations</span >
244+ <label class =" relative inline-flex items-center cursor-pointer" >
245+ <input
246+ v-model =" emailCelebration"
247+ type =" checkbox"
248+ class =" sr-only peer"
249+ :disabled =" settings.email.unsubscribed"
250+ />
251+ <div
252+ class =" w-9 h-5 bg-stone-200 peer-focus:outline-none rounded-full peer dark:bg-stone-700 peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-stone-300 after:border after:rounded-full after:h-4 after:w-4 after:transition-all dark:border-stone-600 peer-checked:bg-emerald-500 peer-disabled:opacity-50"
253+ />
254+ </label >
255+ </div >
256+
257+ <!-- Email encouragement toggle -->
258+ <div
178259 v-if =" encouragementEnabled"
179- class =" flex items-center gap-2 text-sm text-stone-700 dark:text-stone-300 "
260+ class =" flex items-center justify-between "
180261 >
181- <input
182- v-model =" emailEncouragement"
183- type =" checkbox"
184- class =" rounded border-stone-300 text-emerald-500 focus:ring-emerald-500"
185- :disabled =" settings.email.unsubscribed"
186- />
187- Email encouragements
188- </label >
262+ <span class =" text-sm text-stone-700 dark:text-stone-300" >Email encouragements</span >
263+ <label class =" relative inline-flex items-center cursor-pointer" >
264+ <input
265+ v-model =" emailEncouragement"
266+ type =" checkbox"
267+ class =" sr-only peer"
268+ :disabled =" settings.email.unsubscribed"
269+ />
270+ <div
271+ class =" w-9 h-5 bg-stone-200 peer-focus:outline-none rounded-full peer dark:bg-stone-700 peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-stone-300 after:border after:rounded-full after:h-4 after:w-4 after:transition-all dark:border-stone-600 peer-checked:bg-emerald-500 peer-disabled:opacity-50"
272+ />
273+ </label >
274+ </div >
189275 </div >
190- </div >
191276
192- <!-- Save button -->
193- <div class =" p-4 pt-0" >
194- <button
195- class =" px-4 py-2 bg-emerald-500 text-white text-sm font-medium rounded-lg hover:bg-emerald-600 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
196- :disabled =" saving || (needsCloudAck && !acknowledgeCloud)"
197- @click =" handleSave"
277+ <!-- Test email buttons -->
278+ <div
279+ v-if =" (emailCelebration && celebrationEnabled) || (emailEncouragement && encouragementEnabled)"
280+ class =" mt-3 flex gap-2"
198281 >
199- {{ saving ? "Saving..." : "Save Weekly Rhythm Settings" }}
200- </button >
282+ <button
283+ v-if =" emailCelebration && celebrationEnabled"
284+ class =" text-xs px-3 py-1.5 rounded-lg border border-stone-200 dark:border-stone-600 text-stone-600 dark:text-stone-400 hover:bg-stone-50 dark:hover:bg-stone-700 disabled:opacity-50 transition-colors"
285+ :disabled =" sendingTestCelebration"
286+ @click =" sendTestEmail('celebration')"
287+ >
288+ {{ sendingTestCelebration ? "Sending..." : "Send Test Celebration" }}
289+ </button >
290+ <button
291+ v-if =" emailEncouragement && encouragementEnabled"
292+ class =" text-xs px-3 py-1.5 rounded-lg border border-stone-200 dark:border-stone-600 text-stone-600 dark:text-stone-400 hover:bg-stone-50 dark:hover:bg-stone-700 disabled:opacity-50 transition-colors"
293+ :disabled =" sendingTestEncouragement"
294+ @click =" sendTestEmail('encouragement')"
295+ >
296+ {{ sendingTestEncouragement ? "Sending..." : "Send Test Encouragement" }}
297+ </button >
298+ </div >
201299 </div >
202300
203- <!-- Error message -->
204- <div v-if =" error" class =" p-4 pt-0" >
205- <p class =" text-sm text-red-600 dark:text-red-400" >{{ error }}</p >
301+ <!-- Status indicators -->
302+ <div v-if =" saving" class =" px-4 pb-3" >
303+ <p class =" text-xs text-stone-400 dark:text-stone-500" >Saving...</p >
304+ </div >
305+ <div v-if =" error" class =" px-4 pb-3" >
306+ <p class =" text-xs text-red-600 dark:text-red-400" >{{ error }}</p >
206307 </div >
207308 </div >
208309 </div >
0 commit comments