-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathapp.vue
More file actions
370 lines (340 loc) · 17.4 KB
/
app.vue
File metadata and controls
370 lines (340 loc) · 17.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
<template>
<div class="bg-white"> <!-- forces light mode-->
<!-- Safari Disclaimer Popup -->
<div v-if="showSafariDisclaimer" class="fixed inset-0 z-50 flex items-center justify-center"
@keydown.esc="closeSafariDisclaimer" tabindex="0" aria-modal="true" role="dialog">
<div class="bg-white rounded-lg shadow-lg max-w-sm w-full p-6 relative border border-gray-200">
<button @click="closeSafariDisclaimer" class="absolute top-2 right-2 text-gray-500 hover:text-gray-700"
aria-label="Close">
<svg class="w-5 h-5" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
<div class="flex items-center mb-3">
<svg class="w-6 h-6 text-blue-500 mr-2" fill="none" stroke="currentColor" stroke-width="2"
viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M4 6h16M4 12h16M4 18h16" />
</svg>
<span class="font-semibold text-lg">Safari Notice</span>
</div>
<p class="text-gray-700 mb-2">
For the best experience, consider using Chrome or Firefox. Some features may not work as expected in Safari.
</p>
<button @click="closeSafariDisclaimer"
class="mt-4 w-full bg-blue-600 text-white py-2 rounded hover:bg-blue-700 transition">
Got it!
</button>
</div>
</div>
<!-- PWA Manifest and Route Announcer -->
<NuxtPwaManifest />
<NuxtRouteAnnouncer />
<div class="z-50 sticky bg-[#3A8DDE] text-white text-sm px-4 py-1">
<div class="flex justify-between items-center max-w-7xl mx-auto">
<!-- Left side: email with icon -->
<div class="flex items-center gap-2 flex-grow">
<div class="flex items-center space-x-1">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5"
stroke="currentColor" class="w-4 h-4">
<path stroke-linecap="round" stroke-linejoin="round"
d="M21.75 6.75v10.5a2.25 2.25 0 0 1-2.25 2.25h-15a2.25 2.25 0 0 1-2.25-2.25V6.75m19.5 0A2.25 2.25 0 0 0 19.5 4.5h-15a2.25 2.25 0 0 0-2.25 2.25m19.5 0v.243a2.25 2.25 0 0 1-1.07 1.916l-7.5 4.615a2.25 2.25 0 0 1-2.36 0L3.32 8.91a2.25 2.25 0 0 1-1.07-1.916V6.75" />
</svg>
<a href="mailto:info@rainbowroundup.org" class="underline hover:text-blue-200">
info@rrup.org
</a>
</div>
</div>
<!-- Right side: phone number -->
<div class="shrink-0 pl-4">
469-443-8993
</div>
</div>
</div>
<div class="sticky top-0 z-50 bg-white shadow-sm">
<div class="flex justify-between items-center px-4 py-2">
<div class="flex items-center space-x-2">
<a href="/">
<img src="/images/rrup_logo.png" alt="Rainbow Roundup Logo" class="h-12 w-auto" />
</a>
</div>
<!-- Navigation Links - added md sizing -->
<nav class="hidden md:flex space-x-6 text-sm font-medium">
<NuxtLink to="/" @click="navigate('Home')" class="text-gray-700 hover:text-black">Home</NuxtLink>
<NuxtLink v-if="isAdmin" to="/admin" @click="navigate('Admin')" class="text-gray-700 hover:text-black">Admin</NuxtLink>
<NuxtLink to="/aboutUs" @click="navigate('About Us')" class="text-gray-700 hover:text-black">About Us
</NuxtLink>
<NuxtLink to="/calendar" @click="navigate('Calendar')" class="text-gray-700 hover:text-black">Calendar
</NuxtLink>
<NuxtLink to="/merchandise" @click="navigate('Merchandise')" class="text-gray-700 hover:text-black">Merchandise
</NuxtLink>
<a class="text-gray-700 hover:text-black" href="https://buy.stripe.com/test_14k6op0Et2oF9xKaEE" @click="navigate('Donate')">Donate
</a>
<NuxtLink v-if="session?.data?.user?.id" to="/profile" @click="navigate('Profile')" class="text-gray-700 hover:text-black">Profile
</NuxtLink>
<NuxtLink v-if="!(session?.data?.user?.id)" to="/login" @click="navigate('Sign Up')" class="text-gray-700 hover:text-black">
Sign Up/Log In</NuxtLink>
<button v-else @click="logout" class="text-gray-700 hover:text-black">
Logout
</button>
<button @click="promptInstall" class="text-gray-700 hover:text-black">
Install App
</button>
<!-- Device Notifications Button - Only shows when user is logged in -->
<button @click="requestNotificationPermission()" v-if="session?.data?.user?.id && $pwa.getSWRegistration()?.pushManager"
class="flex items-center text-gray-700 hover:text-black hover:bg-gray-50 rounded px-2"
aria-label="Toggle notifications">
<span class="mr-2">Device notifications</span>
<span v-if="isSubscribedToPush">
<!-- Bell icon (notifications enabled) -->
<svg class="w-5 h-5" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round"
d="M14.857 17.082a23.848 23.848 0 0 0 5.454-1.31A8.967 8.967 0 0 1 18 9.75V9A6 6 0 0 0 6 9v.75a8.967 8.967 0 0 1-2.312 6.022c1.733.64 3.56 1.085 5.455 1.31m5.714 0a24.255 24.255 0 0 1-5.714 0m5.714 0a3 3 0 1 1-5.714 0" />
</svg>
</span>
<span v-else>
<!-- Bell with slash (notifications disabled) -->
<svg class="w-5 h-5" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round"
d="M9.143 17.082a24.248 24.248 0 0 0 3.844.148m-3.844-.148a23.856 23.856 0 0 1-5.455-1.31 8.964 8.964 0 0 0 2.3-5.542m3.155 6.852a3 3 0 0 0 5.667 1.97m1.965-2.277L21 21m-4.225-4.225a23.81 23.81 0 0 0 3.536-1.003A8.967 8.967 0 0 1 18 9.75V9A6 6 0 0 0 6.53 6.53m10.245 10.245L6.53 6.53M3 3l3.53 3.53" />
</svg>
</span>
</button>
</nav>
<!-- Hamburger Button - md: sizing -->
<button @click="toggleMobileMenu"
class="md:hidden flex items-center justify-center w-8 h-8 text-gray-700 hover:text-black focus:outline-none"
aria-label="Toggle menu">
<!-- Hamburger Icon -->
<svg v-if="!mobileMenuOpen" class="w-6 h-6" fill="none" stroke="currentColor" stroke-width="2"
viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M4 6h16M4 12h16M4 18h16" />
</svg>
<!-- Close Icon -->
<svg v-else class="w-6 h-6" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<!-- Collapsible Mobile Navigation Menu (when page is resized)-->
<div v-if="mobileMenuOpen"
class="xl:hidden bg-white border-t border-gray-200 shadow-lg absolute left-0 right-0 top-full"
style="z-index:100">
<nav class="flex flex-col space-y-1 px-4 py-3 text-sm font-medium">
<NuxtLink to="/" class="block py-2 text-gray-700 hover:text-black hover:bg-gray-50 rounded px-2"
@click.native="handleMobileNavClick">Home</NuxtLink>
<NuxtLink v-if="isAdmin" to="/admin" class="block py-2 text-gray-700 hover:text-black hover:bg-gray-50 rounded px-2"
@click.native="handleMobileNavClick">Admin</NuxtLink>
<NuxtLink to="/aboutUs" class="block py-2 text-gray-700 hover:text-black hover:bg-gray-50 rounded px-2"
@click.native="handleMobileNavClick">About Us</NuxtLink>
<NuxtLink to="/calendar" class="block py-2 text-gray-700 hover:text-black hover:bg-gray-50 rounded px-2"
@click.native="handleMobileNavClick">Calendar</NuxtLink>
<NuxtLink to="/merchandise" class="block py-2 text-gray-700 hover:text-black hover:bg-gray-50 rounded px-2"
@click.native="handleMobileNavClick">Merchandise</NuxtLink>
<a href="https://buy.stripe.com/test_14k6op0Et2oF9xKaEE"
class="block py-2 text-gray-700 hover:text-black hover:bg-gray-50 rounded px-2"
@click="handleMobileNavClick">Donate</a>
<NuxtLink v-if="session?.data?.user?.id" to="/profile" class="block py-2 text-gray-700 hover:text-black hover:bg-gray-50 rounded px-2"
@click.native="handleMobileNavClick">Profile</NuxtLink>
<NuxtLink v-if="(!session?.data?.user?.id)" to="/login" class="block py-2 text-gray-700 hover:text-black hover:bg-gray-50 rounded px-2"
@click.native="handleMobileNavClick">
Sign Up/Log In</NuxtLink>
<button v-else @click="logout(); handleMobileNavClick()"
class="block py-2 text-left text-gray-700 hover:text-black hover:bg-gray-50 rounded px-2">Logout</button>
<button @click="promptInstall(); handleMobileNavClick()"
class="block py-2 text-left text-gray-700 hover:text-black hover:bg-gray-50 rounded px-2">Install
App</button>
<!-- Device Notifications Button - Only shows when user is logged in -->
<button
v-if="session?.data?.user?.id && $pwa.getSWRegistration()?.pushManager"
@click="requestNotificationPermission(); handleMobileNavClick()"
class="flex items-center py-2 text-gray-700 hover:text-black hover:bg-gray-50 rounded px-2 border-0"
aria-label="Toggle notifications">
<span class="mr-2">Device notifications</span>
<span v-if="isSubscribedToPush">
<!-- Bell icon (notifications enabled) -->
<svg class="w-5 h-5" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round"
d="M14.857 17.082a23.848 23.848 0 0 0 5.454-1.31A8.967 8.967 0 0 1 18 9.75V9A6 6 0 0 0 6 9v.75a8.967 8.967 0 0 1-2.312 6.022c1.733.64 3.56 1.085 5.455 1.31m5.714 0a24.255 24.255 0 0 1-5.714 0m5.714 0a3 3 0 1 1-5.714 0" />
</svg>
</span>
<span v-else>
<!-- Bell with slash (notifications disabled) -->
<svg class="w-5 h-5" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round"
d="M9.143 17.082a24.248 24.248 0 0 0 3.844.148m-3.844-.148a23.856 23.856 0 0 1-5.455-1.31 8.964 8.964 0 0 0 2.3-5.542m3.155 6.852a3 3 0 0 0 5.667 1.97m1.965-2.277L21 21m-4.225-4.225a23.81 23.81 0 0 0 3.536-1.003A8.967 8.967 0 0 1 18 9.75V9A6 6 0 0 0 6.53 6.53m10.245 10.245L6.53 6.53M3 3l3.53 3.53" />
</svg>
</span>
</button>
</nav>
</div>
</div>
<NuxtPage class="min-h-screen" />
</div>
</template>
<script setup>
import { ref, onMounted } from "vue";
// Use the built-in auth composable instead of custom useUser
import { authClient } from "~/composables/auth"
const session = authClient.useSession()
const isAdmin = computed(() => session?.value?.data?.user?.role ?? false)
const config = useRuntimeConfig()
const dropdownOpen = ref(false);
const mobileMenuOpen = ref(false);
const notificationPermission = ref(false);
const deferredPrompt = ref(null);
const runtimeConfig = useRuntimeConfig();
const isSubscribedToPush = ref(false);
const { $pwa } = useNuxtApp();
const notifSubscription = ref(null)
// Toggles resized mobile menu view
const toggleMobileMenu = () => {
mobileMenuOpen.value = !mobileMenuOpen.value;
};
// Closes the resized mobile menu once a nav link is clicked
const handleMobileNavClick = () => {
mobileMenuOpen.value = false;
};
// Tracks if disclaimer has already been shown
const showSafariDisclaimer = ref(false);
// Safari detection function
const isSafari = () => {
if (typeof window === "undefined") return false;
const ua = window.navigator.userAgent;
// Detect Safari (not Chrome, not Firefox, not Edge)
return (
/Safari/.test(ua) &&
!/Chrome|Chromium|Edg|Firefox/.test(ua)
);
};
// Close Safari disclaimer
const closeSafariDisclaimer = () => {
showSafariDisclaimer.value = false;
if (typeof window !== "undefined") {
sessionStorage.setItem("safariDisclaimerDismissed", "true");
}
};
// Handle ESC key press
const handleKeyPress = (event) => {
if (event.key === "Escape" && showSafariDisclaimer.value) {
closeSafariDisclaimer();
}
};
onMounted(() => {
updateSubscriptionStatus();
checkPushSubscription() // check push subscription on load
window.addEventListener('focus', checkPushSubscription) // everytime the tab comes back in focus, reload it
// Safari disclaimer logic
if (
typeof window !== "undefined" &&
isSafari() &&
sessionStorage.getItem("safariDisclaimerDismissed") !== "true"
) {
showSafariDisclaimer.value = true;
window.addEventListener("keydown", handleKeyPress);
}
});
// This is apparently needed to clean up the listener to prevent duplicates and other issues
onBeforeUnmount(() => {
window.removeEventListener('focus', checkPushSubscription)
})
const toggleDropdown = () => {
dropdownOpen.value = !dropdownOpen.value;
};
const promptInstall = async () => {
const result = await $pwa.install() // Our PWA library can do PWA installation prompt
console.log("Outcome of installation prompt: ",result.outcome)
};
const updateSubscriptionStatus = () => {
if('Notification' in window){
notificationPermission.value = Notification?.permission === "granted";
}
};
// This only checks is the client thinks it has subscribed to push
// If the subscription is deleted from our server, this value will not reflect that
// docs: https://developer.mozilla.org/en-US/docs/Web/API/PushManager/getSubscription
// Note: I did this with a function that updates on page load instead of computed because a lot of the calls made are not reactive and will not auto-update
async function checkPushSubscription() {
if (!process.client) return
if (!('serviceWorker' in navigator)) { // ensure browser support service workers
notifSubscription.value = null
isSubscribedToPush.value = false
return
}
try {
const sw = await navigator.serviceWorker.ready // wait for our service worker to be ready to prevent race conditions
if (!sw?.pushManager) { // ensure browser support push manager
notifSubscription.value = null
isSubscribedToPush.value = false
return
}
const subscription = await sw.pushManager.getSubscription()
notifSubscription.value = subscription
isSubscribedToPush.value = !!subscription && Notification?.permission === 'granted'
} catch (e) {
// in any failure case, treat as not subscribed
notifSubscription.value = null
isSubscribedToPush.value = false
}
}
// dummy comment
const requestNotificationPermission = () => {
if(isSubscribedToPush.value){
$pwa.getSWRegistration().pushManager.getSubscription().then( async (subscription) =>{
await subscription.unsubscribe()
// This only unsubscribes locally, but deleting it on the server is not important since it will autodelete once the server tries to send it and realizes its deleted client side
// It will throw an error in console, but that is normal and expected (since the subscription is gone).
await checkPushSubscription()
console.log("Unsubscribed from notifications")
})
return
}
else if ("serviceWorker" in navigator && "Notification" in window && session?.value?.data?.user?.id) {
Notification?.requestPermission()
.then(async (permission) => {
console.log("Permission:", permission);
console.log("Does service worker exist: ", 'serviceWorker' in navigator);
if (permission === "granted" && 'serviceWorker' in navigator) {
notificationPermission.value = true;
console.log("Both service worker and permission are good!");
const applicationServerKey = config.public.PUSH_VAPID_PUBLIC_KEY;
console.log(`Public Key: ${applicationServerKey}`)
navigator.serviceWorker.ready.then(async (serviceWorkerRegistration) => {
const options = {
userVisibleOnly: true,
applicationServerKey,
};
//console.log(serviceWorkerRegistration.getNotifications);
serviceWorkerRegistration.pushManager.subscribe(options).then(
async (pushSubscription) => {
//console.log("Endpoint: ", pushSubscription.endpoint);
const result = await $fetch("/api/notification/subscribe", {
method: "POST",
body: pushSubscription,
});
checkPushSubscription();
},
(error) => {
console.error(error);
},
);
});
} else {
notificationPermission.value = false;
console.warn("Could not subscribe to notifications, either permission was not granted or your browser doesn't support service workers");
}
notificationPermission.value = Notification?.permission === "granted";
})
} else {
console.warn("Notification API or Service Worker not supported.");
}
};
const logout = async () => {
await authClient.signOut();
window.location.reload(true);
};
const navigate = (section) => {
// close mobile menu when navigating
mobileMenuOpen.value = false;
console.log(`Navigating to: ${section}`);
};
</script>