Files
crowd-funder-for-time-pwa/src/App.vue
Matthew Raymer e5518cd47c fix: update Vue template syntax and improve Vite config
- Fix Vue template syntax in App.vue by using proper event handler format
- Update Vite config to properly handle ESM imports and crypto modules
- Add manual chunks for better code splitting
- Improve environment variable handling in vite-env.d.ts
- Fix TypeScript linting errors in App.vue
2025-04-18 09:59:33 +00:00

543 lines
18 KiB
Vue

<template>
<router-view />
<!-- Messages in the upper-right - https://github.com/emmanuelsw/notiwind -->
<NotificationGroup group="alert">
<div
class="fixed top-4 right-4 w-full max-w-sm flex flex-col items-start justify-end"
>
<Notification
v-slot="{ notifications, close }"
enter="transform ease-out duration-300 transition"
enter-from="translate-y-2 opacity-0 sm:translate-y-0 sm:translate-x-4"
enter-to="translate-y-0 opacity-100 sm:translate-x-0"
leave="transition ease-in duration-500"
leave-from="opacity-100"
leave-to="opacity-0"
move="transition duration-500"
move-delay="delay-300"
>
<div
v-for="notification in notifications"
:key="notification.id"
class="w-full"
role="alert"
>
<div
v-if="notification.type === 'toast'"
class="w-full max-w-sm mx-auto mb-3 overflow-hidden bg-slate-900/90 text-white rounded-lg shadow-md"
>
<div class="w-full px-4 py-3">
<span class="font-semibold">{{ notification.title }}</span>
<p class="text-sm">
{{ notification.text }}
</p>
</div>
</div>
<div
v-if="notification.type === 'info'"
class="flex w-full max-w-sm mx-auto mb-3 overflow-hidden bg-slate-100 rounded-lg shadow-md"
>
<div
class="flex items-center justify-center w-12 bg-slate-600 text-slate-100"
>
<font-awesome icon="circle-info" class="fa-fw fa-xl" />
</div>
<div class="relative w-full pl-4 pr-8 py-2 text-slate-900">
<span class="font-semibold">{{ notification.title }}</span>
<p class="text-sm">
{{ truncateLongWords(notification.text) }}
</p>
<button
class="absolute top-2 right-2 px-0.5 py-0 rounded-full bg-slate-200 text-slate-600"
@click="close(notification.id)"
>
<font-awesome icon="xmark" class="fa-fw" />
</button>
</div>
</div>
<div
v-if="notification.type === 'success'"
class="flex w-full max-w-sm mx-auto mb-3 overflow-hidden bg-emerald-100 rounded-lg shadow-md"
>
<div
class="flex items-center justify-center w-12 bg-emerald-600 text-emerald-100"
>
<font-awesome icon="circle-info" class="fa-fw fa-xl" />
</div>
<div class="relative w-full pl-4 pr-8 py-2 text-emerald-900">
<span class="font-semibold">{{ notification.title }}</span>
<p class="text-sm">
{{ truncateLongWords(notification.text) }}
</p>
<button
class="absolute top-2 right-2 px-0.5 py-0 rounded-full bg-emerald-200 text-emerald-600"
@click="close(notification.id)"
>
<font-awesome icon="xmark" class="fa-fw" />
</button>
</div>
</div>
<div
v-if="notification.type === 'warning'"
class="flex w-full max-w-sm mx-auto mb-3 overflow-hidden bg-amber-100 rounded-lg shadow-md"
>
<div
class="flex items-center justify-center w-12 bg-amber-600 text-amber-100"
>
<font-awesome icon="triangle-exclamation" class="fa-fw fa-xl" />
</div>
<div class="relative w-full pl-4 pr-8 py-2 text-amber-900">
<span class="font-semibold">{{ notification.title }}</span>
<p class="text-sm">
{{ truncateLongWords(notification.text) }}
</p>
<button
class="absolute top-2 right-2 px-0.5 py-0 rounded-full bg-amber-200 text-amber-600"
@click="close(notification.id)"
>
<font-awesome icon="xmark" class="fa-fw" />
</button>
</div>
</div>
<div
v-if="notification.type === 'danger'"
class="flex w-full max-w-sm mx-auto mb-3 overflow-hidden bg-rose-100 rounded-lg shadow-md"
>
<div
class="flex items-center justify-center w-12 bg-rose-600 text-rose-100"
>
<font-awesome icon="triangle-exclamation" class="fa-fw fa-xl" />
</div>
<div class="relative w-full pl-4 pr-8 py-2 text-rose-900">
<span class="font-semibold">{{ notification.title }}</span>
<p class="text-sm">
{{ truncateLongWords(notification.text) }}
</p>
<button
class="absolute top-2 right-2 px-0.5 py-0 rounded-full bg-rose-200 text-rose-600"
@click="close(notification.id)"
>
<font-awesome icon="xmark" class="fa-fw" />
</button>
</div>
</div>
</div>
</Notification>
</div>
</NotificationGroup>
<!--
This "group" of "modal" is the prompt for an answer.
Set "type" as follows: "confirm" for yes/no, and "notification" ones:
"-permission", "-mute", "-off"
-->
<NotificationGroup group="modal">
<div class="fixed z-[100] top-0 inset-x-0 w-full">
<Notification
v-slot="{ notifications, close }"
enter="transform ease-out duration-300 transition"
enter-from="translate-y-2 opacity-0 sm:translate-y-4"
enter-to="translate-y-0 opacity-100 sm:translate-y-0"
leave="transition ease-in duration-500"
leave-from="opacity-100"
leave-to="opacity-0"
move="transition duration-500"
move-delay="delay-300"
>
<!-- see NotificationIface in constants/app.ts -->
<div
v-for="notification in notifications"
:key="notification.id"
class="w-full"
role="alert"
>
<!--
Type of "confirm" will post a message.
With onYes function, show a "Yes" button to call that function.
With onNo function, show a "No" button to call that function,
and pass it state of "askAgain" field shown if you set promptToStopAsking.
-->
<div
v-if="notification.type === 'confirm'"
class="absolute inset-0 h-screen flex flex-col items-center justify-center bg-slate-900/50"
>
<div
class="flex w-11/12 max-w-sm mx-auto mb-3 overflow-hidden bg-white rounded-lg shadow-lg"
>
<div class="w-full px-6 py-6 text-slate-900 text-center">
<span class="font-semibold text-lg">
{{ notification.title }}
</span>
<p class="text-sm mb-2">
{{ notification.text }}
</p>
<button
v-if="notification.onYes"
class="block w-full text-center text-md font-bold uppercase bg-blue-600 text-white px-2 py-2 rounded-md mb-2"
@click="{
notification.onYes();
close(notification.id);
}"
>
Yes{{
notification.yesText ? ', ' + notification.yesText : ''
}}
</button>
<button
v-if="notification.onNo"
class="block w-full text-center text-md font-bold uppercase bg-yellow-600 text-white px-2 py-2 rounded-md mb-2"
@click="{
notification.onNo(stopAsking);
close(notification.id);
stopAsking = false; // reset value
}"
>
No{{ notification.noText ? ', ' + notification.noText : '' }}
</button>
<label
v-if="notification.promptToStopAsking && notification.onNo"
for="toggleStopAsking"
class="flex items-center justify-between cursor-pointer my-4"
@click="stopAsking = !stopAsking"
>
<!-- label -->
<span class="ml-2">... and do not ask again.</span>
<!-- toggle -->
<div class="relative ml-2">
<!-- input -->
<input
v-model="stopAsking"
type="checkbox"
name="stopAsking"
class="sr-only"
/>
<!-- line -->
<div class="block bg-slate-500 w-14 h-8 rounded-full" />
<!-- dot -->
<div
class="dot absolute left-1 top-1 bg-slate-400 w-6 h-6 rounded-full transition"
/>
</div>
</label>
<button
class="block w-full text-center text-md font-bold uppercase bg-slate-600 text-white px-2 py-2 rounded-md"
@click="{
notification.onCancel
? notification.onCancel(stopAsking)
: null;
close(notification.id);
stopAsking = false; // reset value for next time they open this modal
}"
>
{{ notification.onYes ? 'Cancel' : 'Close' }}
</button>
</div>
</div>
</div>
<div
v-if="notification.type === 'notification-mute'"
class="absolute inset-0 h-screen flex flex-col items-center justify-center bg-slate-900/50"
>
<div
class="flex w-11/12 max-w-sm mx-auto mb-3 overflow-hidden bg-white rounded-lg shadow-lg"
>
<div class="w-full px-6 py-6 text-slate-900 text-center">
<p class="text-lg mb-4">Mute app notifications:</p>
<button
class="block w-full text-center text-md font-bold uppercase bg-blue-600 text-white px-2 py-2 rounded-md mb-2"
>
For 1 Day
</button>
<button
class="block w-full text-center text-md font-bold uppercase bg-blue-600 text-white px-2 py-2 rounded-md mb-2"
>
For 2 Days
</button>
<button
class="block w-full text-center text-md font-bold uppercase bg-blue-600 text-white px-2 py-2 rounded-md mb-2"
>
For 1 Week
</button>
<button
class="block w-full text-center text-md font-bold uppercase bg-blue-600 text-white px-2 py-2 rounded-md mb-2"
>
Until I turn it back on
</button>
<button
class="block w-full text-center text-md font-bold uppercase bg-slate-600 text-white px-2 py-2 rounded-md"
@click="close(notification.id)"
>
Cancel
</button>
</div>
</div>
</div>
<div
v-if="notification.type === 'notification-off'"
class="absolute inset-0 h-screen flex flex-col items-center justify-center bg-slate-900/50"
>
<div
class="flex w-11/12 max-w-sm mx-auto mb-3 overflow-hidden bg-white rounded-lg shadow-lg"
>
<div class="w-full px-6 py-6 text-slate-900 text-center">
<p class="text-lg mb-4">
Would you like to <b>turn off</b> this notification?
</p>
<button
class="block w-full text-center text-md font-bold uppercase bg-rose-600 text-white px-2 py-2 rounded-md mb-2"
@click="{
close(notification.id);
turnOffNotifications(notification);
}"
>
Turn Off Notification
</button>
<button
class="block w-full text-center text-md font-bold uppercase bg-slate-600 text-white px-2 py-2 rounded-md"
@click="close(notification.id)"
>
Leave it On
</button>
</div>
</div>
</div>
</div>
</Notification>
</div>
</NotificationGroup>
</template>
<script lang="ts">
import { Vue, Component } from 'vue-facing-decorator'
import { logConsoleAndDb, retrieveSettingsForActiveAccount } from './db/index'
import { NotificationIface } from './constants/app'
import { logger } from './utils/logger'
interface Settings {
notifyingNewActivityTime?: string
notifyingReminderTime?: string
}
@Component
export default class App extends Vue {
$notify!: (notification: NotificationIface, timeout?: number) => void
stopAsking = false
// created() {
// logger.log(
// "Component created: Reactivity set up.",
// window.location.pathname,
// );
// }
// beforeCreate() {
// logger.log("Component beforeCreate: Instance initialized.");
// }
// beforeMount() {
// logger.log("Component beforeMount: Template is about to be rendered.");
// }
// mounted() {
// logger.log("Component mounted: Template is now rendered.");
// }
// beforeUpdate() {
// logger.log("Component beforeUpdate: DOM is about to be updated.");
// }
// updated() {
// logger.log("Component updated: DOM has been updated.");
// }
// beforeUnmount() {
// logger.log("Component beforeUnmount: Cleaning up before removal.");
// }
// unmounted() {
// logger.log("Component unmounted: Component removed from the DOM.");
// }
truncateLongWords(sentence: string) {
return sentence
.split(' ')
.map((word) => (word.length > 30 ? word.slice(0, 30) + '...' : word))
.join(' ')
}
async turnOffNotifications(
notification: NotificationIface
): Promise<boolean> {
logger.log('Starting turnOffNotifications...')
let subscription: PushSubscriptionJSON | null = null
let allGoingOff = false
try {
logger.log('Retrieving settings for the active account...')
const settings: Settings = await retrieveSettingsForActiveAccount()
logger.log('Retrieved settings:', settings)
const notifyingNewActivity = !!settings?.notifyingNewActivityTime
const notifyingReminder = !!settings?.notifyingReminderTime
if (!notifyingNewActivity || !notifyingReminder) {
allGoingOff = true
logger.log('Both notifications are being turned off.')
}
logger.log('Checking service worker readiness...')
await navigator.serviceWorker?.ready
.then((registration) => {
logger.log('Service worker is ready. Fetching subscription...')
return registration.pushManager.getSubscription()
})
.then(async (subscript: PushSubscription | null) => {
if (subscript) {
subscription = subscript.toJSON()
logger.log('PushSubscription retrieved:', subscription)
if (allGoingOff) {
logger.log('Unsubscribing from push notifications...')
await subscript.unsubscribe()
logger.log('Successfully unsubscribed.')
}
} else {
logConsoleAndDb('Subscription object is not available.')
logger.log('No subscription found.')
}
})
.catch((error) => {
logConsoleAndDb(
'Push provider server communication failed: ' +
JSON.stringify(error),
true
)
logger.error('Error during subscription fetch:', error)
})
if (!subscription) {
logger.log('No subscription available. Notifying user...')
this.$notify(
{
group: 'alert',
type: 'info',
title: 'Finished',
text: 'Notifications are off.'
},
5000
)
logger.log('Exiting as there is no subscription to process.')
return true
}
const serverSubscription = {
...subscription
}
if (!allGoingOff) {
serverSubscription['notifyType'] = notification.title
logger.log(
`Server subscription updated with notifyType: ${notification.title}`
)
}
logger.log('Sending unsubscribe request to the server...')
const pushServerSuccess = await fetch('/web-push/unsubscribe', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(serverSubscription)
})
.then(async (response) => {
if (!response.ok) {
const errorBody = await response.text()
logConsoleAndDb(
`Push server failed: ${response.status} ${errorBody}`,
true
)
logger.error('Push server error response:', errorBody)
}
logger.log(`Server response status: ${response.status}`)
return response.ok
})
.catch((error) => {
logConsoleAndDb(
'Push server communication failed: ' + JSON.stringify(error),
true
)
logger.error('Error during server communication:', error)
return false
})
const message = pushServerSuccess
? 'Notification is off.'
: 'Notification is still on. Try to turn it off again.'
logger.log('Server response processed. Message:', message)
this.$notify(
{
group: 'alert',
type: 'info',
title: 'Finished',
text: message
},
5000
)
if (notification.callback) {
logger.log('Executing notification callback...')
notification.callback(pushServerSuccess)
}
logger.log(
'Completed turnOffNotifications with success:',
pushServerSuccess
)
return pushServerSuccess
} catch (error) {
logConsoleAndDb(
'Error turning off notifications: ' + JSON.stringify(error),
true
)
logger.error('Critical error in turnOffNotifications:', error)
this.$notify(
{
group: 'alert',
type: 'error',
title: 'Error',
text: 'Failed to turn off notifications. Please try again.'
},
5000
)
return false
}
}
}
</script>
<style></style>