forked from jsnbuchanan/crowd-funder-for-time-pwa
Dynamic padding to clear certain iOS UI elements such as the notch, dynamic island and gesture bar, to ensure they don't overlap with our own UI elements.
554 lines
19 KiB
Vue
554 lines
19 KiB
Vue
<template>
|
|
<router-view />
|
|
|
|
<!-- Messages in the upper-right - https://github.com/emmanuelsw/notiwind -->
|
|
<NotificationGroup group="alert">
|
|
<div
|
|
class="fixed top-[calc(env(safe-area-inset-top)+1rem)] right-4 left-4 sm:left-auto sm:w-full sm: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"
|
|
></font-awesome>
|
|
</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"></font-awesome>
|
|
</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"
|
|
></font-awesome>
|
|
</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"></font-awesome>
|
|
</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"
|
|
></font-awesome>
|
|
</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"></font-awesome>
|
|
</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"
|
|
></font-awesome>
|
|
</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"></font-awesome>
|
|
</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-[env(safe-area-inset-top)] 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"></div>
|
|
<!-- dot -->
|
|
<div
|
|
class="dot absolute left-1 top-1 bg-slate-400 w-6 h-6 rounded-full transition"
|
|
></div>
|
|
</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>
|
|
#Content {
|
|
padding-left: 1.5rem;
|
|
padding-right: 1.5rem;
|
|
padding-top: calc(env(safe-area-inset-top) + 1.5rem);
|
|
padding-bottom: calc(env(safe-area-inset-bottom) + 1.5rem);
|
|
}
|
|
|
|
#QuickNav ~ #Content {
|
|
padding-bottom: calc(env(safe-area-inset-bottom) + 6rem);
|
|
}
|
|
</style>
|