hey-pharmacist-ecommerce 1.1.29 → 1.1.31
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/dist/index.d.mts +10957 -1331
- package/dist/index.d.ts +10957 -1331
- package/dist/index.js +12364 -5144
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +9353 -2205
- package/dist/index.mjs.map +1 -1
- package/package.json +4 -3
- package/src/components/AccountReviewsTab.tsx +97 -0
- package/src/components/CouponCodeInput.tsx +190 -0
- package/src/components/Header.tsx +5 -1
- package/src/components/Notification.tsx +1 -1
- package/src/components/NotificationBell.tsx +33 -0
- package/src/components/NotificationCard.tsx +211 -0
- package/src/components/NotificationDrawer.tsx +195 -0
- package/src/components/OrderCard.tsx +164 -99
- package/src/components/ProductReviewsSection.tsx +30 -0
- package/src/components/RatingDistribution.tsx +86 -0
- package/src/components/ReviewCard.tsx +59 -0
- package/src/components/ReviewForm.tsx +207 -0
- package/src/components/ReviewPromptBanner.tsx +98 -0
- package/src/components/ReviewsList.tsx +151 -0
- package/src/components/StarRating.tsx +98 -0
- package/src/hooks/useDiscounts.ts +7 -0
- package/src/hooks/useOrders.ts +15 -0
- package/src/hooks/useReviews.ts +230 -0
- package/src/hooks/useStoreCapabilities.ts +87 -0
- package/src/index.ts +29 -0
- package/src/lib/Apis/apis/auth-api.ts +19 -7
- package/src/lib/Apis/apis/categories-api.ts +97 -0
- package/src/lib/Apis/apis/discounts-api.ts +23 -72
- package/src/lib/Apis/apis/notifications-api.ts +196 -231
- package/src/lib/Apis/apis/products-api.ts +181 -0
- package/src/lib/Apis/apis/review-api.ts +283 -4
- package/src/lib/Apis/apis/shipping-api.ts +105 -0
- package/src/lib/Apis/apis/stores-api.ts +536 -0
- package/src/lib/Apis/apis/sub-categories-api.ts +97 -0
- package/src/lib/Apis/apis/users-api.ts +8 -8
- package/src/lib/Apis/models/address-created-request.ts +0 -12
- package/src/lib/Apis/models/address.ts +0 -12
- package/src/lib/Apis/models/api-key-info-dto.ts +49 -0
- package/src/lib/Apis/models/bulk-channel-toggle-dto.ts +52 -0
- package/src/lib/Apis/models/cart-body-populated.ts +3 -3
- package/src/lib/Apis/models/channel-settings-dto.ts +39 -0
- package/src/lib/Apis/models/{discount-paginated-response.ts → completed-order-dto.ts} +21 -16
- package/src/lib/Apis/models/create-address-dto.ts +0 -12
- package/src/lib/Apis/models/create-discount-dto.ts +31 -100
- package/src/lib/Apis/models/create-review-dto.ts +4 -4
- package/src/lib/Apis/models/create-shippo-account-dto.ts +45 -0
- package/src/lib/Apis/models/create-store-address-dto.ts +0 -12
- package/src/lib/Apis/models/create-store-dto-settings.ts +51 -0
- package/src/lib/Apis/models/create-store-dto.ts +13 -0
- package/src/lib/Apis/models/create-variant-dto.ts +0 -6
- package/src/lib/Apis/models/discount.ts +37 -106
- package/src/lib/Apis/models/discounts-insights-dto.ts +12 -0
- package/src/lib/Apis/models/index.ts +24 -7
- package/src/lib/Apis/models/{manual-discount.ts → manual-discount-dto.ts} +10 -10
- package/src/lib/Apis/models/manual-order-dto.ts +3 -3
- package/src/lib/Apis/models/populated-discount.ts +41 -109
- package/src/lib/Apis/models/preference-update-item.ts +59 -0
- package/src/lib/Apis/models/product-light-dto.ts +40 -0
- package/src/lib/Apis/models/product-variant.ts +0 -6
- package/src/lib/Apis/models/reorder-categories-dto.ts +27 -0
- package/src/lib/Apis/models/reorder-products-dto.ts +49 -0
- package/src/lib/Apis/models/{check-notifications-response-dto.ts → reorder-products-success-response-dto.ts} +7 -7
- package/src/lib/Apis/models/reorder-subcategories-dto.ts +33 -0
- package/src/lib/Apis/models/reorder-success-response-dto.ts +33 -0
- package/src/lib/Apis/models/review-status-dto.ts +34 -0
- package/src/lib/Apis/models/review.ts +9 -3
- package/src/lib/Apis/models/reviewable-order-dto.ts +58 -0
- package/src/lib/Apis/models/reviewable-product-dto.ts +81 -0
- package/src/lib/Apis/models/shipment-with-order.ts +18 -0
- package/src/lib/Apis/models/shipment.ts +18 -0
- package/src/lib/Apis/models/shippo-account-response-dto.ts +51 -0
- package/src/lib/Apis/models/store-api-keys-response-dto.ts +34 -0
- package/src/lib/Apis/models/store-capabilities-dto.ts +63 -0
- package/src/lib/Apis/models/store-entity.ts +13 -0
- package/src/lib/Apis/models/store.ts +13 -0
- package/src/lib/Apis/models/update-address-dto.ts +0 -12
- package/src/lib/Apis/models/update-api-keys-dto.ts +39 -0
- package/src/lib/Apis/models/update-discount-dto.ts +31 -100
- package/src/lib/Apis/models/update-manual-shipment-status-dto.ts +47 -0
- package/src/lib/Apis/models/update-notification-settings-dto.ts +28 -0
- package/src/lib/Apis/models/update-review-dto.ts +4 -4
- package/src/lib/Apis/models/update-store-dto.ts +13 -0
- package/src/lib/Apis/models/update-variant-dto.ts +0 -6
- package/src/lib/Apis/models/{pick-type-class.ts → variant-light-dto.ts} +20 -14
- package/src/lib/utils/discount.ts +155 -0
- package/src/lib/validations/discount.ts +11 -0
- package/src/providers/CartProvider.tsx +2 -2
- package/src/providers/DiscountProvider.tsx +97 -0
- package/src/providers/EcommerceProvider.tsx +13 -5
- package/src/providers/NotificationCenterProvider.tsx +436 -0
- package/src/screens/CartScreen.tsx +1 -1
- package/src/screens/CheckoutScreen.tsx +402 -290
- package/src/screens/NotificationSettingsScreen.tsx +413 -0
- package/src/screens/OrderDetailScreen.tsx +283 -0
- package/src/screens/OrderReviewsScreen.tsx +308 -0
- package/src/screens/OrdersScreen.tsx +31 -7
- package/src/screens/ProductDetailScreen.tsx +24 -11
- package/src/screens/ProfileScreen.tsx +5 -0
- package/src/screens/ResetPasswordScreen.tsx +10 -4
- package/src/lib/Apis/models/create-notification-dto.ts +0 -75
- package/src/lib/Apis/models/notification.ts +0 -93
- package/src/lib/Apis/models/single-notification-dto.ts +0 -99
|
@@ -0,0 +1,413 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React, { useState, useEffect } from 'react';
|
|
4
|
+
import { ChevronDown, ChevronRight, Loader2, Bell, Shield, CreditCard, Package, Settings, Zap } from 'lucide-react';
|
|
5
|
+
import { useNotificationCenter } from '@/providers/NotificationCenterProvider';
|
|
6
|
+
import { NotificationType, PreferenceUpdateItemTypeEnum } from '@/providers/NotificationCenterProvider';
|
|
7
|
+
import { motion, AnimatePresence } from 'framer-motion';
|
|
8
|
+
import { Button } from '@/components/ui/Button';
|
|
9
|
+
|
|
10
|
+
interface NotificationCategory {
|
|
11
|
+
name: string;
|
|
12
|
+
icon: React.ElementType;
|
|
13
|
+
types: Array<{
|
|
14
|
+
type: NotificationType;
|
|
15
|
+
label: string;
|
|
16
|
+
description: string;
|
|
17
|
+
isComingSoon?: boolean;
|
|
18
|
+
}>;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const NOTIFICATION_CATEGORIES: NotificationCategory[] = [
|
|
22
|
+
{
|
|
23
|
+
name: 'Order Updates',
|
|
24
|
+
icon: Package,
|
|
25
|
+
types: [
|
|
26
|
+
{
|
|
27
|
+
type: PreferenceUpdateItemTypeEnum.ORDERCONFIRMATION,
|
|
28
|
+
label: 'Order Confirmed',
|
|
29
|
+
description: 'Get notified when your order is confirmed',
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
type: PreferenceUpdateItemTypeEnum.ORDERSHIPPED,
|
|
33
|
+
label: 'Order Shipped',
|
|
34
|
+
description: 'Track your order when it ships',
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
type: PreferenceUpdateItemTypeEnum.ORDERDELIVERED,
|
|
38
|
+
label: 'Order Delivered',
|
|
39
|
+
description: 'Know when your order arrives',
|
|
40
|
+
},
|
|
41
|
+
],
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
name: 'Payments',
|
|
45
|
+
icon: CreditCard,
|
|
46
|
+
types: [
|
|
47
|
+
{
|
|
48
|
+
type: PreferenceUpdateItemTypeEnum.PAYMENTFAILED,
|
|
49
|
+
label: 'Payment Failed',
|
|
50
|
+
description: 'Alert when a payment fails',
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
type: PreferenceUpdateItemTypeEnum.REFUNDPROCESSED,
|
|
54
|
+
label: 'Refund Processed',
|
|
55
|
+
description: 'Confirmation when refunds are issued',
|
|
56
|
+
},
|
|
57
|
+
],
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
name: 'Security',
|
|
61
|
+
icon: Shield,
|
|
62
|
+
types: [
|
|
63
|
+
{
|
|
64
|
+
type: PreferenceUpdateItemTypeEnum.PASSWORDRESET,
|
|
65
|
+
label: 'Password Reset',
|
|
66
|
+
description: 'Security alerts for password changes',
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
type: PreferenceUpdateItemTypeEnum.NEWDEVICELOGIN,
|
|
70
|
+
label: 'New Device Login',
|
|
71
|
+
description: 'Alert when logging in from a new device',
|
|
72
|
+
isComingSoon: true,
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
type: PreferenceUpdateItemTypeEnum.TWOFACODE,
|
|
76
|
+
label: 'Two-Factor Authentication',
|
|
77
|
+
description: 'Receive 2FA codes',
|
|
78
|
+
isComingSoon: true,
|
|
79
|
+
},
|
|
80
|
+
],
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
name: 'Marketing',
|
|
84
|
+
icon: Bell,
|
|
85
|
+
types: [
|
|
86
|
+
{
|
|
87
|
+
type: PreferenceUpdateItemTypeEnum.ABANDONEDCARTREMINDER,
|
|
88
|
+
label: 'Abandoned Cart',
|
|
89
|
+
description: 'Reminders for items left in cart',
|
|
90
|
+
isComingSoon: true,
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
type: PreferenceUpdateItemTypeEnum.PRICEDROPALERT,
|
|
94
|
+
label: 'Price Drop Alert',
|
|
95
|
+
description: 'Notify when prices drop on saved items',
|
|
96
|
+
isComingSoon: true,
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
type: PreferenceUpdateItemTypeEnum.BACKINSTOCK,
|
|
100
|
+
label: 'Back in Stock',
|
|
101
|
+
description: 'Alert when out-of-stock items return',
|
|
102
|
+
isComingSoon: true,
|
|
103
|
+
},
|
|
104
|
+
],
|
|
105
|
+
},
|
|
106
|
+
];
|
|
107
|
+
|
|
108
|
+
export function NotificationSettingsScreen() {
|
|
109
|
+
const { settings, updateSettings, isLoading } = useNotificationCenter();
|
|
110
|
+
const [localSettings, setLocalSettings] = useState(settings);
|
|
111
|
+
const [expandedCategories, setExpandedCategories] = useState<Set<string>>(
|
|
112
|
+
new Set(NOTIFICATION_CATEGORIES.map(cat => cat.name))
|
|
113
|
+
);
|
|
114
|
+
const [isSaving, setIsSaving] = useState(false);
|
|
115
|
+
const [hasChanges, setHasChanges] = useState(false);
|
|
116
|
+
|
|
117
|
+
useEffect(() => {
|
|
118
|
+
console.log('NotificationSettingsScreen: settings updated:', settings);
|
|
119
|
+
if (settings) {
|
|
120
|
+
setLocalSettings(settings);
|
|
121
|
+
}
|
|
122
|
+
}, [settings]);
|
|
123
|
+
|
|
124
|
+
const toggleCategory = (categoryName: string) => {
|
|
125
|
+
setExpandedCategories(prev => {
|
|
126
|
+
const next = new Set(prev);
|
|
127
|
+
if (next.has(categoryName)) {
|
|
128
|
+
next.delete(categoryName);
|
|
129
|
+
} else {
|
|
130
|
+
next.add(categoryName);
|
|
131
|
+
}
|
|
132
|
+
return next;
|
|
133
|
+
});
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
const getChannelSetting = (type: NotificationType, channel: 'email' | 'push' | 'inApp'): boolean => {
|
|
137
|
+
const activeSettings = hasChanges ? localSettings : settings;
|
|
138
|
+
if (!activeSettings?.preferences || !Array.isArray(activeSettings.preferences)) {
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const normalize = (s: any) => s?.toString().replace(/[^a-zA-Z0-9]/g, '').toUpperCase() || '';
|
|
143
|
+
const targetType = normalize(type);
|
|
144
|
+
|
|
145
|
+
const pref = activeSettings.preferences.find(p => normalize(p.type) === targetType);
|
|
146
|
+
|
|
147
|
+
if (!pref) return false;
|
|
148
|
+
|
|
149
|
+
// Handle potential boolean-like values from backend safely
|
|
150
|
+
const val = (pref.settings as any)?.[channel];
|
|
151
|
+
return val === true || val === 'true' || val === 1 || val === '1';
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
const updateChannelSetting = (type: NotificationType, channel: 'email' | 'push' | 'inApp', value: boolean) => {
|
|
155
|
+
setLocalSettings(prev => {
|
|
156
|
+
// If we're starting local changes, base them on the current provider settings
|
|
157
|
+
const currentSettings = prev || settings || { preferences: [] };
|
|
158
|
+
const currentPrefs = Array.isArray(currentSettings.preferences) ? currentSettings.preferences : [];
|
|
159
|
+
const preferences = [...currentPrefs];
|
|
160
|
+
|
|
161
|
+
const normalize = (s: any) => s?.toString().replace(/[^a-zA-Z0-9]/g, '').toUpperCase() || '';
|
|
162
|
+
const targetType = normalize(type);
|
|
163
|
+
|
|
164
|
+
const existingIndex = preferences.findIndex(p => normalize(p.type) === targetType);
|
|
165
|
+
|
|
166
|
+
if (existingIndex >= 0) {
|
|
167
|
+
preferences[existingIndex] = {
|
|
168
|
+
...preferences[existingIndex],
|
|
169
|
+
settings: {
|
|
170
|
+
...preferences[existingIndex].settings,
|
|
171
|
+
[channel]: value,
|
|
172
|
+
},
|
|
173
|
+
};
|
|
174
|
+
} else {
|
|
175
|
+
preferences.push({
|
|
176
|
+
type,
|
|
177
|
+
settings: {
|
|
178
|
+
email: channel === 'email' ? value : true,
|
|
179
|
+
push: channel === 'push' ? value : true,
|
|
180
|
+
inApp: channel === 'inApp' ? value : true,
|
|
181
|
+
},
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return { ...currentSettings, preferences };
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
setHasChanges(true);
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
const handleSave = async () => {
|
|
192
|
+
if (!localSettings) return;
|
|
193
|
+
|
|
194
|
+
setIsSaving(true);
|
|
195
|
+
try {
|
|
196
|
+
await updateSettings(localSettings);
|
|
197
|
+
setHasChanges(false);
|
|
198
|
+
} catch (error) {
|
|
199
|
+
console.error('Failed to save settings:', error);
|
|
200
|
+
alert('Failed to save settings. Please try again.');
|
|
201
|
+
} finally {
|
|
202
|
+
setIsSaving(false);
|
|
203
|
+
}
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
const disableAllChannel = (channel: 'email' | 'push') => {
|
|
207
|
+
setLocalSettings(prev => {
|
|
208
|
+
const currentSettings = prev || { preferences: [] };
|
|
209
|
+
const prefIsArray = Array.isArray(currentSettings.preferences);
|
|
210
|
+
const preferences = NOTIFICATION_CATEGORIES.flatMap(cat =>
|
|
211
|
+
cat.types.filter(t => !t.isComingSoon).map(t => t.type)
|
|
212
|
+
).map(type => {
|
|
213
|
+
const existing = prefIsArray ? currentSettings.preferences.find(p => p.type === type) : undefined;
|
|
214
|
+
return {
|
|
215
|
+
type,
|
|
216
|
+
settings: {
|
|
217
|
+
email: channel === 'email' ? false : (existing?.settings?.email ?? true),
|
|
218
|
+
push: channel === 'push' ? false : (existing?.settings?.push ?? true),
|
|
219
|
+
inApp: existing?.settings?.inApp ?? true,
|
|
220
|
+
},
|
|
221
|
+
};
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
return { ...currentSettings, preferences };
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
setHasChanges(true);
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
if (isLoading && !settings) {
|
|
231
|
+
return (
|
|
232
|
+
<div className="flex flex-col items-center justify-center min-h-[400px] bg-linear-to-b from-[#F8FAFC] to-[#EBF4FB]">
|
|
233
|
+
<Loader2 className="w-10 h-10 text-primary animate-spin mb-4" />
|
|
234
|
+
<p className="text-muted font-medium">Loading preferences...</p>
|
|
235
|
+
</div>
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
return (
|
|
240
|
+
<div className="min-h-screen bg-linear-to-b from-[#F8FAFC] to-[#EBF4FB]">
|
|
241
|
+
{/* Header Area */}
|
|
242
|
+
<div className="container mx-auto px-4 py-8">
|
|
243
|
+
<div className="max-w-4xl mx-auto">
|
|
244
|
+
<h1 className="text-2xl font-semibold text-secondary">Notification Preferences</h1>
|
|
245
|
+
<p className="text-sm text-muted">
|
|
246
|
+
Manage how you receive notifications for different events
|
|
247
|
+
</p>
|
|
248
|
+
</div>
|
|
249
|
+
</div>
|
|
250
|
+
|
|
251
|
+
<div className="max-w-4xl mx-auto px-4 pb-32">
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
{/* Categories */}
|
|
255
|
+
<div className="space-y-4 mb-8">
|
|
256
|
+
{NOTIFICATION_CATEGORIES.map((category) => (
|
|
257
|
+
<div key={category.name} className="bg-white rounded-2xl border border-slate-200 shadow-xs overflow-hidden">
|
|
258
|
+
{/* Category Header */}
|
|
259
|
+
<button
|
|
260
|
+
onClick={() => toggleCategory(category.name)}
|
|
261
|
+
className="w-full flex items-center justify-between p-6 hover:bg-slate-50 transition-colors"
|
|
262
|
+
>
|
|
263
|
+
<div className="flex items-center gap-3">
|
|
264
|
+
<div className="p-2 bg-slate-100 rounded-lg group-hover:bg-white transition-colors">
|
|
265
|
+
<category.icon className="w-5 h-5 text-secondary" />
|
|
266
|
+
</div>
|
|
267
|
+
<h2 className="text-lg font-semibold text-secondary">{category.name}</h2>
|
|
268
|
+
</div>
|
|
269
|
+
{expandedCategories.has(category.name) ? (
|
|
270
|
+
<ChevronDown className="w-5 h-5 text-muted" />
|
|
271
|
+
) : (
|
|
272
|
+
<ChevronRight className="w-5 h-5 text-muted" />
|
|
273
|
+
)}
|
|
274
|
+
</button>
|
|
275
|
+
|
|
276
|
+
<AnimatePresence>
|
|
277
|
+
{expandedCategories.has(category.name) && (
|
|
278
|
+
<motion.div
|
|
279
|
+
initial={{ height: 0, opacity: 0 }}
|
|
280
|
+
animate={{ height: 'auto', opacity: 1 }}
|
|
281
|
+
exit={{ height: 0, opacity: 0 }}
|
|
282
|
+
transition={{ duration: 0.2 }}
|
|
283
|
+
className="border-t border-slate-100"
|
|
284
|
+
>
|
|
285
|
+
<div className="p-6 space-y-4">
|
|
286
|
+
{category.types.map((notifType) => (
|
|
287
|
+
<div
|
|
288
|
+
key={notifType.type}
|
|
289
|
+
className={`rounded-xl border border-slate-200 bg-slate-50 p-4 transition-all hover:bg-white hover:shadow-sm ${notifType.isComingSoon ? 'opacity-50 grayscale select-none' : ''}`}
|
|
290
|
+
>
|
|
291
|
+
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-6">
|
|
292
|
+
<div className="flex-1">
|
|
293
|
+
<div className="flex items-center gap-2">
|
|
294
|
+
<h3 className="font-semibold text-secondary">{notifType.label}</h3>
|
|
295
|
+
{notifType.isComingSoon && (
|
|
296
|
+
<span className="text-[10px] font-bold uppercase tracking-wider bg-slate-200 text-muted px-2 py-0.5 rounded-full">
|
|
297
|
+
Soon
|
|
298
|
+
</span>
|
|
299
|
+
)}
|
|
300
|
+
</div>
|
|
301
|
+
<p className="text-xs text-muted mt-1 leading-relaxed">{notifType.description}</p>
|
|
302
|
+
</div>
|
|
303
|
+
|
|
304
|
+
{!notifType.isComingSoon && (
|
|
305
|
+
<div className="flex items-center gap-6 sm:border-l sm:border-slate-200 sm:pl-6">
|
|
306
|
+
{/* Email */}
|
|
307
|
+
<label className="flex items-center gap-2 cursor-pointer group/label">
|
|
308
|
+
<input
|
|
309
|
+
type="checkbox"
|
|
310
|
+
checked={getChannelSetting(notifType.type, 'email')}
|
|
311
|
+
onChange={(e) =>
|
|
312
|
+
updateChannelSetting(notifType.type, 'email', e.target.checked)
|
|
313
|
+
}
|
|
314
|
+
className="w-4 h-4 text-primary border-slate-300 rounded-sm focus:ring-primary focus:ring-offset-0"
|
|
315
|
+
/>
|
|
316
|
+
<span className="text-[10px] font-bold text-muted uppercase tracking-wider group-hover/label:text-primary transition-colors">Email</span>
|
|
317
|
+
</label>
|
|
318
|
+
|
|
319
|
+
{/* Push */}
|
|
320
|
+
<label className="flex items-center gap-2 cursor-pointer group/label">
|
|
321
|
+
<input
|
|
322
|
+
type="checkbox"
|
|
323
|
+
checked={getChannelSetting(notifType.type, 'push')}
|
|
324
|
+
onChange={(e) =>
|
|
325
|
+
updateChannelSetting(notifType.type, 'push', e.target.checked)
|
|
326
|
+
}
|
|
327
|
+
className="w-4 h-4 text-primary border-slate-300 rounded-sm focus:ring-primary focus:ring-offset-0"
|
|
328
|
+
/>
|
|
329
|
+
<span className="text-[10px] font-bold text-muted uppercase tracking-wider group-hover/label:text-primary transition-colors">Push</span>
|
|
330
|
+
</label>
|
|
331
|
+
|
|
332
|
+
{/* In-App */}
|
|
333
|
+
<label className="flex items-center gap-2 cursor-pointer group/label">
|
|
334
|
+
<input
|
|
335
|
+
type="checkbox"
|
|
336
|
+
checked={getChannelSetting(notifType.type, 'inApp')}
|
|
337
|
+
onChange={(e) =>
|
|
338
|
+
updateChannelSetting(notifType.type, 'inApp', e.target.checked)
|
|
339
|
+
}
|
|
340
|
+
className="w-4 h-4 text-primary border-slate-300 rounded-sm focus:ring-primary focus:ring-offset-0"
|
|
341
|
+
/>
|
|
342
|
+
<span className="text-[10px] font-bold text-muted uppercase tracking-wider group-hover/label:text-primary transition-colors">In-App</span>
|
|
343
|
+
</label>
|
|
344
|
+
</div>
|
|
345
|
+
)}
|
|
346
|
+
</div>
|
|
347
|
+
</div>
|
|
348
|
+
))}
|
|
349
|
+
</div>
|
|
350
|
+
</motion.div>
|
|
351
|
+
)}
|
|
352
|
+
</AnimatePresence>
|
|
353
|
+
</div>
|
|
354
|
+
))}
|
|
355
|
+
</div>
|
|
356
|
+
|
|
357
|
+
{/* Quick Actions */}
|
|
358
|
+
<div className="bg-white rounded-2xl p-6 border border-slate-200 mb-8 shadow-xs">
|
|
359
|
+
<div className="flex items-center gap-2 mb-4">
|
|
360
|
+
<Zap className="h-5 w-5 text-accent" />
|
|
361
|
+
<h3 className="text-lg font-semibold text-secondary">Quick Actions</h3>
|
|
362
|
+
</div>
|
|
363
|
+
<div className="flex flex-wrap gap-3">
|
|
364
|
+
<Button
|
|
365
|
+
variant="outline-solid"
|
|
366
|
+
size="sm"
|
|
367
|
+
onClick={() => disableAllChannel('email')}
|
|
368
|
+
className="rounded-xl border-slate-200 hover:bg-slate-50"
|
|
369
|
+
>
|
|
370
|
+
Disable All Emails
|
|
371
|
+
</Button>
|
|
372
|
+
<Button
|
|
373
|
+
variant="outline-solid"
|
|
374
|
+
size="sm"
|
|
375
|
+
onClick={() => disableAllChannel('push')}
|
|
376
|
+
className="rounded-xl border-slate-200 hover:bg-slate-50"
|
|
377
|
+
>
|
|
378
|
+
Disable All Push
|
|
379
|
+
</Button>
|
|
380
|
+
</div>
|
|
381
|
+
</div>
|
|
382
|
+
|
|
383
|
+
{/* Save Button */}
|
|
384
|
+
<AnimatePresence>
|
|
385
|
+
{hasChanges && (
|
|
386
|
+
<motion.div
|
|
387
|
+
initial={{ opacity: 0, y: 100 }}
|
|
388
|
+
animate={{ opacity: 1, y: 0 }}
|
|
389
|
+
exit={{ opacity: 0, y: 100 }}
|
|
390
|
+
className="fixed bottom-10 left-0 right-0 flex justify-center z-50 pointer-events-none"
|
|
391
|
+
>
|
|
392
|
+
<div className="pointer-events-auto px-6 py-4 bg-secondary shadow-lg rounded-3xl flex items-center gap-8 max-w-xl mx-auto transform hover:scale-[1.02] transition-all duration-300 ease-out border border-white/10">
|
|
393
|
+
<div className="hidden sm:block">
|
|
394
|
+
<p className="text-sm font-bold text-white leading-tight tracking-wide italic">Changes Pending</p>
|
|
395
|
+
<p className="text-[11px] text-white/60 font-medium">Click to synchronize with account</p>
|
|
396
|
+
</div>
|
|
397
|
+
<Button
|
|
398
|
+
onClick={handleSave}
|
|
399
|
+
isLoading={isSaving}
|
|
400
|
+
disabled={isSaving}
|
|
401
|
+
variant="secondary"
|
|
402
|
+
className="px-8 py-3 bg-accent border-none text-white rounded-2xl font-bold text-xs uppercase tracking-widest hover:bg-accent-dark transition-all active:scale-95 shadow-lg shadow-accent/25 min-w-[180px]"
|
|
403
|
+
>
|
|
404
|
+
{isSaving ? 'Saving...' : 'Sync Changes'}
|
|
405
|
+
</Button>
|
|
406
|
+
</div>
|
|
407
|
+
</motion.div>
|
|
408
|
+
)}
|
|
409
|
+
</AnimatePresence>
|
|
410
|
+
</div>
|
|
411
|
+
</div>
|
|
412
|
+
);
|
|
413
|
+
}
|