Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 33 additions & 15 deletions src/lib/IONOS/components/common/Dialog.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import { createEventDispatcher } from 'svelte';
import { slideDuration } from '$lib/IONOS/components/constants';
import { mobile } from '$lib/stores';
import XMark from '$lib/IONOS/components/icons/XMark.svelte';

const dispatch = createEventDispatcher();

Expand All @@ -11,6 +12,8 @@
*/
export let mobileCover = true;
export let dialogId = 'dialog';
export let externalClose = false;

let el: HTMLDialogElement|null = null;

$: if (show) {
Expand All @@ -25,7 +28,7 @@
* Non-mobile: immediately closed
*/
function onTransitionEnd(e) {
if (e.target == el && e.propertyName === 'translate' && !show) {
if ((e.target == el || el?.contains(e.target)) && e.propertyName === 'translate' && !show) {
el?.close();
}
}
Expand All @@ -39,31 +42,46 @@
dispatch('close');
}
}

function close() {
show = false;
dispatch('close');
}
</script>

<dialog
on:toggle={onToggle}
on:close={() => {
show = false;
dispatch('close');
}}
on:close={close}
on:transitionend={onTransitionEnd}
bind:this={el}
closedby="any"
closedby='any'
style:--slide-duration="{slideDuration}ms"
class="fixed top-0 right-0 left-0 bottom-0 m-auto bg-white backdrop:bg-black/75 z-[99999999] overflow-hidden overscroll-contain shadow-xl rounded-2xl {mobileCover ? 'max-md:h-full max-md:max-h-dvh max-md:w-dvw max-md:max-w-dvw max-md:m-0' : ''} max-md:transition-transform duration-(--slide-duration) ease-out {$$props.class ?? 'p-[30px]'}"
class="backdrop:bg-black/75"
>
<div
data-id={`dialog-${dialogId}`}
class="min-h-full flex flex-col"
>
<slot name="header" />

<div class="fixed top-0 right-0 left-0 bottom-0 m-auto bg-white z-[99999999] overscroll-contain shadow-xl rounded-2xl {mobileCover ? 'max-md:h-full max-md:max-h-dvh max-md:w-dvw max-md:max-w-dvw max-md:m-0' : 'max-h-fit'} max-md:transition-transform duration-(--slide-duration) ease-out {$$props.class ?? 'p-[30px]'}">

{#if externalClose}
<div class="relative -top-[70px] flex justify-center w-full">
<button on:click={() => close()} class="bg-transparent text-white">
<XMark />
</button>
</div>

{/if}

<div
data-id={`dialog-content-${dialogId}`}
class="flex flex-col flex-1"
data-id={`dialog-${dialogId}`}
class="flex flex-col"
>
<slot name="content"/>
<slot name="header" />

<div
data-id={`dialog-content-${dialogId}`}
class="flex flex-col flex-1"
>
<slot name="content"/>
</div>
</div>
</div>
</dialog>
2 changes: 1 addition & 1 deletion src/lib/IONOS/components/common/DialogHeader.svelte
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<script lang="ts">
import { createEventDispatcher } from 'svelte';
import XMark from '$lib/IONOS/components/icons/XMark.svelte';
import ChevronLeft from '$lib/IONOS/components/icons//ChevronLeft.svelte';
import ChevronLeft from '$lib/IONOS/components/icons/ChevronLeft.svelte';

const dispatch = createEventDispatcher();

Expand Down
13 changes: 13 additions & 0 deletions src/lib/IONOS/components/icons/Share.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<script lang="ts">
export let className = 'size-5';
</script>

<!--
Source: Custom
Copyright: IONOS
Origin: iOS Share icon for PWA instructions
-->

<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="currentColor" class={className}>
<path d="M12 2.5L8.5 6L10 7.5L11 6.5V13H13V6.5L14 7.5L15.5 6L12 2.5ZM6 9V19C6 20.1 6.9 21 8 21H16C17.1 21 18 20.1 18 19V9H16V19H8V9H6Z"/>
</svg>
13 changes: 13 additions & 0 deletions src/lib/IONOS/components/icons/Touch.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<script lang="ts">
export let className = 'size-5';
</script>

<!--
Source: Figma
Copyright: unknown
Origin: Figma
-->

<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="currentColor" class={className}>
<path d="M3.11246 5.11111C3.11246 2.78311 5.00624 0.888889 7.33468 0.888889C9.66313 0.888889 11.5569 2.78311 11.5569 5.11111C11.5569 5.47956 11.2587 5.77778 10.8902 5.77778C10.5218 5.77778 10.2236 5.47956 10.2236 5.11111C10.2236 3.51822 8.92757 2.22222 7.33468 2.22222C5.74179 2.22222 4.44579 3.51822 4.44579 5.11111C4.44579 5.47956 4.14757 5.77778 3.77913 5.77778C3.41068 5.77778 3.11246 5.47956 3.11246 5.11111ZM12.9267 10.8307L12.1582 14.0964C12.0165 14.6987 11.4791 15.1244 10.8605 15.1244H6.63824C6.2969 15.1244 5.9689 14.9938 5.72135 14.7591L3.26935 12.4356C2.54179 11.7462 2.46357 10.6351 3.08713 9.85067C3.4529 9.39067 4.00002 9.12667 4.58802 9.12667C4.83557 9.12667 5.07646 9.17333 5.3049 9.26578V5.56889C5.3049 4.46622 6.20224 3.56889 7.3049 3.56889C8.40757 3.56889 9.3049 4.46622 9.3049 5.56889V7.32222C9.38846 7.34933 9.46979 7.38267 9.54846 7.42222L11.8751 8.58578C12.7071 9.00178 13.1396 9.92489 12.9267 10.8307ZM11.2791 9.77822L8.95246 8.61467C8.8889 8.58267 8.82046 8.56756 8.75335 8.56756C8.6249 8.56756 8.49913 8.62267 8.41202 8.72578L8.08935 9.10845C8.08935 9.10845 8.04757 9.164 8.01424 9.164C7.99113 9.164 7.97202 9.13822 7.97202 9.052V5.56889C7.97202 5.20089 7.67335 4.90222 7.30535 4.90222C6.93735 4.90222 6.63868 5.20089 6.63868 5.56889V10.9996C6.63868 11.128 6.53335 11.2218 6.41646 11.2218C6.38579 11.2218 6.35424 11.2151 6.32357 11.2009L4.8329 10.5133C4.75424 10.4769 4.67113 10.4596 4.58846 10.4596C4.41468 10.4596 4.2449 10.5373 4.13113 10.68C3.94268 10.9169 3.96668 11.2587 4.18668 11.4671L6.63868 13.7907H10.8609L11.6293 10.5249C11.7 10.2249 11.5551 9.91556 11.2796 9.77778L11.2791 9.77822Z"/>
</svg>
Original file line number Diff line number Diff line change
@@ -1,22 +1,30 @@
<script lang="ts">
import { fly } from 'svelte/transition';
import type { Readable } from 'svelte/store';
import type { I18Next } from '$lib/IONOS/i18next.d.ts';
import Heart from '$lib/IONOS/components/icons/Heart.svelte';
import XMark from '$lib/IONOS/components/icons/XMark.svelte';
import EmojiSad from '$lib/IONOS/components/icons/EmojiSad.svelte';
import Touch from '$lib/IONOS/components/icons/Touch.svelte';
import Link from '$lib/IONOS/components/common/Link.svelte';
import { NotificationType, type Notification } from '$lib/IONOS/stores/notifications';
import { createEventDispatcher } from 'svelte';
import { createEventDispatcher, getContext } from 'svelte';

const dispatch = createEventDispatcher();
const i18n = getContext<Readable<I18Next>>('i18n');

Check failure on line 14 in src/lib/IONOS/components/notifications/NotificationBanner.svelte

View workflow job for this annotation

GitHub Actions / Lint, Format & Test Frontend

'i18n' is assigned a value but never used

export let notification: Notification;
</script>


<div transition:fly={{ y: -200, duration: 50 }} class="ease-in-out w-full p-4 sm:p-5 flex items-center justify-start text-sm gap-2 {notification.type}">
<div id="notification-icon" class="self-start pt-0.5 sm:pt-0">
{#if notification.type === NotificationType.FEEDBACK}
<Heart />
{:else if notification.type === NotificationType.ERROR}
<EmojiSad className="text-red-500"/>
{:else if notification.type === NotificationType.INFO}
<Touch />
{/if}
</div>
<div class="flex flex-col md:flex-row gap-2.5 items-start">
Expand Down
100 changes: 95 additions & 5 deletions src/lib/IONOS/components/notifications/NotificationManager.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,28 @@
import type { I18Next } from '$lib/IONOS/i18next.d.ts';
import type { Chat } from '$lib/apis/chats/types.ts';
import NotificationBanner from "$lib/IONOS/components/notifications/NotificationBanner.svelte";
import PWAInstallDialog from "$lib/IONOS/components/notifications/PWAInstallDialog.svelte";
import { chats, user } from '$lib/stores';
import { updateSettings } from '$lib/IONOS/services/settings';
import { buildSurveyUrl } from '$lib/IONOS/services/survey';
import { notifications, addNotification, removeNotification, type Notification, NotificationType } from "$lib/IONOS/stores/notifications";
import { getContext, onDestroy } from 'svelte';
import { getContext, onDestroy, onMount } from 'svelte';
import { getUserSettings } from '$lib/apis/users';
import {
shouldShowPWAPrompt,
dismissPWAPrompt,
triggerPWAInstall,
isIOSDevice,
isSafari
} from '$lib/IONOS/services/pwa';
import { deferredPrompt, isPWAInstallable, setupGlobalPWAListener, clearDeferredPrompt } from '$lib/IONOS/stores/pwa-prompt';

const i18n = getContext<Readable<I18Next>>('i18n');

const DAYS = 24 * 60 * 60 * 1000;

// Check if the user should be prompted for feedback
let showPWADialog = false;
let cleanupPWAListeners: (() => void) | null = null;

const unsubscribeChats = chats.subscribe(async (chats: Chat[]|null) => {
const userSettings = await getUserSettings(localStorage.token);
const userCreatedAt = new Date($user!.created_at * 1000);
Expand Down Expand Up @@ -50,16 +60,96 @@
};

const dismissHandler = (event: CustomEvent) => {
removeNotification(event.detail.notification);
const notification = event.detail.notification;

if (notification.type === NotificationType.PWA_INSTALL) {
dismissPWAPrompt();
}

removeNotification(notification);
};

onDestroy(() => {
unsubscribeChats();
cleanupPWAListeners?.();
});

onMount(() => {
cleanupPWAListeners = setupGlobalPWAListener();

if (shouldShowPWAPrompt($deferredPrompt)) {
addPWANotification();
}
});

$: if ($isPWAInstallable && shouldShowPWAPrompt($deferredPrompt)) {
addPWANotification();
}

$: if (!$isPWAInstallable && !$deferredPrompt) {
showPWADialog = false;
notifications.update((currentNotifications: Notification[]) =>
currentNotifications.filter(n => n.type !== NotificationType.PWA_INSTALL)
);
}

const addPWANotification = () => {
const pwaNotification: Notification = {
type: NotificationType.PWA_INSTALL,
title: $i18n.t('Install IONOS GPT', { ns: 'ionos' }),
message: $i18n.t('For quick and easy access, you can now install IONOS GPT like an app!', { ns: 'ionos' }),
actions: [{
label: $i18n.t('Install', { ns: 'ionos' }),
handler: () => {
handlePWAInstall();
}
}],
dismissible: true,
};
addNotification(pwaNotification);
};

const handlePWAInstall = async () => {
const showIOSInstructions = isIOSDevice() && isSafari();

if (showIOSInstructions) {
showPWADialog = true;
} else if ($deferredPrompt) {
const accepted = await triggerPWAInstall($deferredPrompt);
if (accepted) {
clearDeferredPrompt();
removeNotification({ type: NotificationType.PWA_INSTALL } as Notification);
}
}
};

const handlePWADialogDismiss = () => {
showPWADialog = false;
};

const handlePWADialogInstall = async () => {
if ($deferredPrompt) {
const accepted = await triggerPWAInstall($deferredPrompt);
if (accepted) {
clearDeferredPrompt();
}
}
showPWADialog = false;
removeNotification({ type: NotificationType.PWA_INSTALL } as Notification);
};
</script>

<div class="sticky top-0 flex flex-col w-full z-50">
{#each $notifications as notification }
<NotificationBanner {notification} on:dismiss={dismissHandler} />
<NotificationBanner
{notification}
on:dismiss={dismissHandler}
/>
{/each}
</div>

<PWAInstallDialog
bind:show={showPWADialog}
on:install={handlePWADialogInstall}
on:close={handlePWADialogDismiss}
/>
103 changes: 103 additions & 0 deletions src/lib/IONOS/components/notifications/PWAInstallDialog.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
<script lang="ts">
import type { Readable } from 'svelte/store';
import type { I18Next } from '$lib/IONOS/i18next.d.ts';
import Dialog from '$lib/IONOS/components/common/Dialog.svelte';
import Touch from '$lib/IONOS/components/icons/Touch.svelte';
import Share from '$lib/IONOS/components/icons/Share.svelte';
import Button from '$lib/IONOS/components/common/Button.svelte';
import { ButtonType } from '$lib/IONOS/components/common/buttons.ts';
import { getContext } from 'svelte';
import { createEventDispatcher } from 'svelte';
import { isIOSDevice, isSafari } from '$lib/IONOS/services/pwa';
import DialogHeader from '$lib/IONOS/components/common/DialogHeader.svelte';

const i18n = getContext<Readable<I18Next>>('i18n');
const dispatch = createEventDispatcher();

export let show = false;

const showIOSInstructions = isIOSDevice() && isSafari();

const handleInstall = () => {
dispatch('install');
show = false;
};

const handleLater = () => {
dispatch('close');
show = false;
};
</script>

<Dialog bind:show on:close={handleLater}
dialogId="pwa-install"
class="p-[30px] items-end md:items-center pb-[25px] px-[25px] max-md:mb-0 {show ? 'max-md:translate-y-[-5dvh]' : 'max-md:translate-y-[50dvh]'}"
animationDuration={150}
mobileCover={false}
externalClose={true}
>
<DialogHeader closable={false} slot="header" />
<div slot="content" class="w-full ">
{#if showIOSInstructions}
<div class="text-center mb-6">
<div class="w-16 h-16 bg-blue-100 rounded-full flex items-center justify-center mx-auto mb-4">
<Touch className="w-8 h-8 text-blue-600" />
</div>
<h3 class="text-xl font-semibold text-gray-900 mb-2">
{$i18n.t('Install IONOS GPT', { ns: 'ionos' })}
</h3>
<p class="text-gray-600 mb-6">
{$i18n.t('For quick and easy access, you can now install IONOS GPT like an app!', { ns: 'ionos' })}
</p>
</div>

<div class="flex flex-col justify-center space-y-4 mb-6">
<div class="flex items-start space-x-3">
<div class="flex-shrink-0 w-6 h-6 bg-blue-600 rounded-full flex items-center justify-center">
<span class="text-white text-sm font-medium">1</span>
</div>
<div class="flex-1">
<p class="text-gray-900">
{$i18n.t('Tap the Share button', { ns: 'ionos' })}
<Share className="inline w-4 h-4 mx-1" />
</p>
</div>
</div>

<div class="flex items-start space-x-3">
<div class="flex-shrink-0 w-6 h-6 bg-blue-600 rounded-full flex items-center justify-center">
<span class="text-white text-sm font-medium">2</span>
</div>
<div class="flex-1 flex flex-row gap-1">
<p class="text-gray-900">
{$i18n.t('Select', { ns: 'ionos' })}
</p>
<p class="font-semibold">{$i18n.t('Add to Home Screen', { ns: 'ionos' })}</p>
</div>
</div>
</div>
{:else}
<div class="text-center mb-6">
<div class="w-16 h-16 bg-blue-100 rounded-full flex items-center justify-center mx-auto mb-4">
<Touch className="w-8 h-8 text-blue-600" />
</div>
<h3 class="text-xl font-semibold text-gray-900 mb-2">
{$i18n.t('Install IONOS GPT', { ns: 'ionos' })}
</h3>
<p class="text-gray-600 mb-6">
{$i18n.t('For quick and easy access, you can now install IONOS GPT like an app!', { ns: 'ionos' })}
</p>
</div>

<div class="flex justify-center space-x-3">
<Button
on:click={handleInstall}
type={ButtonType.primary}
className="transition-colors"
>
{$i18n.t('Install IONOS GPT', { ns: 'ionos' })}
</Button>
</div>
{/if}
</div>
</Dialog>
Loading
Loading