Files
crowd-funder-from-jason/src/App.vue
Jose Olarte III 8e8eef2ab5 Safe area implementation for iOS
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.
2025-05-08 20:11:45 +08:00

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>