mirror of
https://github.com/appwrite/console.git
synced 2026-04-07 19:17:46 +00:00
Merge branch 'main' of https://github.com/appwrite/console into console-roles-dl
This commit is contained in:
@@ -0,0 +1,384 @@
|
||||
<script lang="ts">
|
||||
import { isCloud } from '$lib/system';
|
||||
import { Button } from '$lib/elements/forms/index';
|
||||
import { hideNotification, shouldShowNotification } from '$lib/helpers/notifications';
|
||||
import { app } from '$lib/stores/app';
|
||||
import {
|
||||
type BottomModalAlertItem,
|
||||
bottomModalAlerts,
|
||||
dismissBottomModalAlert,
|
||||
hideAllModalAlerts
|
||||
} from '$lib/stores/bottom-alerts';
|
||||
import { onMount } from 'svelte';
|
||||
import { organization } from '$lib/stores/organization';
|
||||
import { BillingPlan } from '$lib/constants';
|
||||
import { upgradeURL } from '$lib/stores/billing';
|
||||
import { addBottomModalAlerts } from '$routes/(console)/project-[project]/bottomAlerts';
|
||||
|
||||
let currentIndex = 0;
|
||||
let openModalOnMobile = false;
|
||||
|
||||
$: filteredModalAlerts = $bottomModalAlerts
|
||||
.sort((a, b) => b.importance - a.importance)
|
||||
.filter((alert) => alert.show && shouldShowNotification(alert.id));
|
||||
|
||||
$: currentModalAlert = filteredModalAlerts[currentIndex] as BottomModalAlertItem;
|
||||
|
||||
function handleClose() {
|
||||
const modalAlert = currentModalAlert;
|
||||
dismissBottomModalAlert(modalAlert.id);
|
||||
hideNotification(modalAlert.id);
|
||||
if (modalAlert.closed) modalAlert.closed();
|
||||
|
||||
if (currentIndex === filteredModalAlerts.length - 1 && filteredModalAlerts.length > 1) {
|
||||
currentIndex = currentIndex - 1;
|
||||
} else {
|
||||
currentIndex = currentIndex % filteredModalAlerts.length;
|
||||
}
|
||||
}
|
||||
|
||||
function showNext() {
|
||||
currentIndex = (currentIndex + 1) % filteredModalAlerts.length;
|
||||
}
|
||||
|
||||
function showPrevious() {
|
||||
currentIndex = (currentIndex - 1 + filteredModalAlerts.length) % filteredModalAlerts.length;
|
||||
}
|
||||
|
||||
function showUpgrade() {
|
||||
const plan = currentModalAlert.plan;
|
||||
const organizationPlan = $organization.billingPlan;
|
||||
switch (plan) {
|
||||
case 'free':
|
||||
return false;
|
||||
case 'pro':
|
||||
return organizationPlan === BillingPlan.FREE;
|
||||
case 'scale':
|
||||
return (
|
||||
organizationPlan === BillingPlan.FREE || organizationPlan === BillingPlan.PRO
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
addBottomModalAlerts();
|
||||
});
|
||||
</script>
|
||||
|
||||
{#if filteredModalAlerts.length > 0 && currentModalAlert}
|
||||
{@const shouldShowUpgrade = showUpgrade()}
|
||||
<div class="main-alert-wrapper is-not-mobile">
|
||||
<div class="alert-container">
|
||||
<article class="card">
|
||||
{#key currentModalAlert.id}
|
||||
<button class="icon-inline-tag" on:click={() => handleClose()}>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
fill="none">
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M4.29289 4.29289C4.68342 3.90237 5.31658 3.90237 5.70711 4.29289L10 8.58579L14.2929 4.29289C14.6834 3.90237 15.3166 3.90237 15.7071 4.29289C16.0976 4.68342 16.0976 5.31658 15.7071 5.70711L11.4142 10L15.7071 14.2929C16.0976 14.6834 16.0976 15.3166 15.7071 15.7071C15.3166 16.0976 14.6834 16.0976 14.2929 15.7071L10 11.4142L5.70711 15.7071C5.31658 16.0976 4.68342 16.0976 4.29289 15.7071C3.90237 15.3166 3.90237 14.6834 4.29289 14.2929L8.58579 10L4.29289 5.70711C3.90237 5.31658 3.90237 4.68342 4.29289 4.29289Z"
|
||||
fill="#97979B" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<div class="content-wrapper u-flex-vertical u-gap-16">
|
||||
{#if $app.themeInUse === 'dark'}
|
||||
<img
|
||||
src={currentModalAlert.src.dark}
|
||||
alt={currentModalAlert.title}
|
||||
class="showcase-image u-image-object-fit-contain u-block u-only-dark" />
|
||||
{:else}
|
||||
<img
|
||||
src={currentModalAlert.src.light}
|
||||
alt={currentModalAlert.title}
|
||||
class="showcase-image u-image-object-fit-contain u-block u-only-light" />
|
||||
{/if}
|
||||
|
||||
{#if filteredModalAlerts.length > 1}
|
||||
<div class="u-flex u-main-space-between u-cross-baseline">
|
||||
<span class="inline-tag feature-count-tag">
|
||||
Feature {currentIndex + 1} of {filteredModalAlerts.length}
|
||||
</span>
|
||||
|
||||
<div class="u-flex u-gap-10">
|
||||
<button
|
||||
class="icon-cheveron-left"
|
||||
on:click={showPrevious}
|
||||
disabled={currentIndex === 0}
|
||||
class:active={currentIndex > 0} />
|
||||
|
||||
<button
|
||||
class="icon-cheveron-right"
|
||||
on:click={showNext}
|
||||
disabled={currentIndex === filteredModalAlerts.length - 1}
|
||||
class:active={currentIndex !==
|
||||
filteredModalAlerts.length - 1} />
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="u-flex-vertical u-gap-4 u-padding-inline-8">
|
||||
<h3 class="body-text-2 u-bold">{currentModalAlert.title}</h3>
|
||||
|
||||
<span class="u-width-fit-content">
|
||||
{#if currentModalAlert.isHtml}
|
||||
{@html currentModalAlert.message}
|
||||
{:else}
|
||||
{currentModalAlert.message}
|
||||
{/if}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="buttons u-flex u-flex-vertical-mobile u-gap-4 u-padding-inline-8 u-padding-block-8">
|
||||
<Button
|
||||
secondary
|
||||
class="button"
|
||||
href={shouldShowUpgrade ? $upgradeURL : currentModalAlert.cta.link}
|
||||
external={!isCloud}
|
||||
fullWidthMobile
|
||||
on:click={() => handleClose()}>
|
||||
{shouldShowUpgrade ? 'Upgrade plan' : currentModalAlert.cta.text}
|
||||
</Button>
|
||||
|
||||
{#if currentModalAlert.learnMore && currentModalAlert.learnMore.link}
|
||||
<Button
|
||||
text
|
||||
class="button"
|
||||
external
|
||||
fullWidthMobile
|
||||
href={currentModalAlert.learnMore.link}>
|
||||
{currentModalAlert.learnMore.text
|
||||
? currentModalAlert.learnMore.text
|
||||
: 'Learn More'}
|
||||
</Button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/key}
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="main-alert-wrapper is-only-mobile" class:closed={!openModalOnMobile}>
|
||||
{#if openModalOnMobile}
|
||||
<div class="alert-container">
|
||||
<article class="card">
|
||||
{#key currentModalAlert.id}
|
||||
<button class="icon-inline-tag" on:click={() => handleClose()}>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
fill="none">
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M4.29289 4.29289C4.68342 3.90237 5.31658 3.90237 5.70711 4.29289L10 8.58579L14.2929 4.29289C14.6834 3.90237 15.3166 3.90237 15.7071 4.29289C16.0976 4.68342 16.0976 5.31658 15.7071 5.70711L11.4142 10L15.7071 14.2929C16.0976 14.6834 16.0976 15.3166 15.7071 15.7071C15.3166 16.0976 14.6834 16.0976 14.2929 15.7071L10 11.4142L5.70711 15.7071C5.31658 16.0976 4.68342 16.0976 4.29289 15.7071C3.90237 15.3166 3.90237 14.6834 4.29289 14.2929L8.58579 10L4.29289 5.70711C3.90237 5.31658 3.90237 4.68342 4.29289 4.29289Z"
|
||||
fill="#97979B" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<div class="content-wrapper u-flex-vertical u-gap-16">
|
||||
{#if $app.themeInUse === 'dark'}
|
||||
<img
|
||||
src={currentModalAlert.src.dark}
|
||||
alt={currentModalAlert.title}
|
||||
class="showcase-image u-image-object-fit-contain u-block u-only-dark" />
|
||||
{:else}
|
||||
<img
|
||||
src={currentModalAlert.src.light}
|
||||
alt={currentModalAlert.title}
|
||||
class="showcase-image u-image-object-fit-contain u-block u-only-light" />
|
||||
{/if}
|
||||
|
||||
{#if filteredModalAlerts.length > 1}
|
||||
<div class="u-flex u-main-space-between u-cross-baseline">
|
||||
<span class="inline-tag feature-count-tag">
|
||||
Feature {currentIndex + 1} of {filteredModalAlerts.length}
|
||||
</span>
|
||||
|
||||
<div class="u-flex u-gap-10">
|
||||
<button
|
||||
class="icon-cheveron-left"
|
||||
on:click={showPrevious}
|
||||
disabled={currentIndex === 0}
|
||||
class:active={currentIndex > 0} />
|
||||
|
||||
<button
|
||||
class="icon-cheveron-right"
|
||||
on:click={showNext}
|
||||
disabled={currentIndex ===
|
||||
filteredModalAlerts.length - 1}
|
||||
class:active={currentIndex !==
|
||||
filteredModalAlerts.length - 1} />
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="u-flex-vertical u-gap-8 u-padding-inline-8">
|
||||
<h3 class="body-text-2 u-bold">{currentModalAlert.title}</h3>
|
||||
|
||||
<span class="u-width-fit-content">
|
||||
{#if currentModalAlert.isHtml}
|
||||
{@html currentModalAlert.message}
|
||||
{:else}
|
||||
{currentModalAlert.message}
|
||||
{/if}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="buttons u-flex u-flex-vertical-mobile u-gap-4 u-padding-inline-8 u-padding-block-8">
|
||||
<Button
|
||||
secondary
|
||||
class="button"
|
||||
href={shouldShowUpgrade
|
||||
? $upgradeURL
|
||||
: currentModalAlert.cta.link}
|
||||
external={!isCloud}
|
||||
fullWidthMobile
|
||||
on:click={() => handleClose()}>
|
||||
{shouldShowUpgrade
|
||||
? 'Upgrade plan'
|
||||
: currentModalAlert.cta.text}
|
||||
</Button>
|
||||
|
||||
{#if currentModalAlert.learnMore && currentModalAlert.learnMore.link}
|
||||
<Button
|
||||
text
|
||||
class="button"
|
||||
external
|
||||
fullWidthMobile
|
||||
href={currentModalAlert.learnMore.link}>
|
||||
{currentModalAlert.learnMore.text
|
||||
? currentModalAlert.learnMore.text
|
||||
: 'Learn More'}
|
||||
</Button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/key}
|
||||
</article>
|
||||
</div>
|
||||
{:else}
|
||||
<button
|
||||
class:showing={!openModalOnMobile}
|
||||
class="card notification-card u-width-full-line"
|
||||
on:click={() => (openModalOnMobile = true)}>
|
||||
<div class="u-flex-vertical u-gap-4">
|
||||
<div class="u-flex u-cross-center u-main-space-between">
|
||||
<h3 class="body-text-2 u-bold">Early access</h3>
|
||||
<button on:click={hideAllModalAlerts}>
|
||||
<span class="icon-x" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<span class="u-width-fit-content">
|
||||
Opt-in to experiments, new features, and more.
|
||||
</span>
|
||||
</div>
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.card {
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
.main-alert-wrapper {
|
||||
left: 1rem;
|
||||
z-index: 25;
|
||||
bottom: 1rem;
|
||||
position: fixed;
|
||||
max-width: 289px;
|
||||
}
|
||||
|
||||
.showcase-image {
|
||||
height: 132px;
|
||||
min-width: 273px;
|
||||
}
|
||||
|
||||
.feature-count-tag {
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
width: fit-content;
|
||||
margin-inline-start: 0.5rem;
|
||||
}
|
||||
|
||||
.icon-inline-tag {
|
||||
top: 1rem;
|
||||
right: 1rem;
|
||||
|
||||
background: #fff;
|
||||
position: absolute;
|
||||
display: inline-flex;
|
||||
padding: var(--space-2, 4px);
|
||||
border-radius: var(--border-radius-S, 8px);
|
||||
border: hsl(var(--color-neutral-10)) solid 1px;
|
||||
}
|
||||
|
||||
:global(.theme-dark) .icon-inline-tag {
|
||||
background: #1d1d21;
|
||||
border: hsl(var(--color-neutral-80)) solid 1px;
|
||||
}
|
||||
|
||||
.u-gap-10 {
|
||||
gap: 0.625rem;
|
||||
}
|
||||
|
||||
.icon-cheveron-left,
|
||||
.icon-cheveron-right {
|
||||
opacity: 0.5;
|
||||
color: #97979b;
|
||||
width: var(--icon-size-M, 20px);
|
||||
height: var(--icon-size-M, 20px);
|
||||
}
|
||||
|
||||
.active {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.main-alert-wrapper {
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
display: flex;
|
||||
min-width: 100%;
|
||||
min-height: 100%;
|
||||
max-width: 100vw;
|
||||
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
backdrop-filter: blur(6px);
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
.main-alert-wrapper.closed {
|
||||
backdrop-filter: unset;
|
||||
}
|
||||
|
||||
.notification-card {
|
||||
padding: var(--space-5, 10px) var(--space-6, 12px);
|
||||
}
|
||||
|
||||
.main-alert-wrapper:has(.card.notification-card) {
|
||||
bottom: 0;
|
||||
top: unset;
|
||||
min-height: auto;
|
||||
padding-inline: 0.5rem;
|
||||
}
|
||||
|
||||
.alert-container {
|
||||
max-width: 289px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -75,3 +75,4 @@ export { default as ModalSideCol } from './modalSideCol.svelte';
|
||||
export { default as EmptyCardImageCloud } from './emptyCardImageCloud.svelte';
|
||||
export { default as ImagePreview } from './imagePreview.svelte';
|
||||
export { default as MfaChallengeFormList } from './mfaChallengeFormList.svelte';
|
||||
export { default as BottomModalAlert } from './bottomModalAlert.svelte';
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { get } from 'svelte/store';
|
||||
import { user } from '$lib/stores/user';
|
||||
|
||||
export type NotificationPrefItem = {
|
||||
expiry: number;
|
||||
hideCount: number;
|
||||
state: 'hidden' | 'shown' | undefined;
|
||||
};
|
||||
|
||||
export type NotificationCoolOffOptions = {
|
||||
coolOffPeriod?: number;
|
||||
exponentialBackoff?: boolean;
|
||||
exponentialBackoffFactor?: number;
|
||||
};
|
||||
|
||||
const userPreferences = () => get(user).prefs;
|
||||
|
||||
const notificationPrefs = (): Record<string, NotificationPrefItem> => {
|
||||
const prefs = userPreferences();
|
||||
return prefs.notificationPrefs ? prefs.notificationPrefs : {};
|
||||
};
|
||||
|
||||
function updateNotificationPrefs(parsedPrefs: Record<string, NotificationPrefItem>) {
|
||||
const currentPrefs = userPreferences();
|
||||
|
||||
const newPrefs = {
|
||||
...currentPrefs,
|
||||
notificationPrefs: parsedPrefs
|
||||
};
|
||||
|
||||
sdk.forConsole.account.updatePrefs(newPrefs);
|
||||
}
|
||||
|
||||
/**
|
||||
* Hides the notification banner by marking it as 'hidden' and setting an expiry time.
|
||||
* Supports normal cool-off periods or exponential backoff based on the options passed.
|
||||
*
|
||||
* @param {string} id - The ID of the notification.
|
||||
* @param {NotificationCoolOffOptions} [options] - Configuration for cool-off behavior.
|
||||
* @param {number} [options.coolOffPeriod=24] - Cool-off period in hours, defaults to 24 hours.
|
||||
* @param {boolean} [options.exponentialBackoff=false] - If true, the cool-off period doubles with each hide. Consider using a smaller `coolOffPeriod` when this option is enabled.
|
||||
* @param {number} [options.exponentialBackoffFactor=2] - The factor by which the cool-off period is multiplied with each hide. Default is 2.
|
||||
*/
|
||||
export function hideNotification(id: string, options: NotificationCoolOffOptions = {}) {
|
||||
const {
|
||||
coolOffPeriod = 24,
|
||||
exponentialBackoff = false,
|
||||
exponentialBackoffFactor = 2
|
||||
} = options;
|
||||
|
||||
const parsedBannerPrefs = notificationPrefs();
|
||||
|
||||
let expiryTime = Date.now() + coolOffPeriod * 3600000;
|
||||
|
||||
let hideCount = parsedBannerPrefs[id]?.hideCount || 0;
|
||||
|
||||
if (exponentialBackoff) {
|
||||
hideCount += 1;
|
||||
expiryTime =
|
||||
Date.now() + coolOffPeriod * 3600000 * exponentialBackoffFactor ** (hideCount - 1);
|
||||
}
|
||||
|
||||
parsedBannerPrefs[id] = {
|
||||
hideCount,
|
||||
state: 'hidden',
|
||||
expiry: expiryTime
|
||||
};
|
||||
|
||||
updateNotificationPrefs(parsedBannerPrefs);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the notification preference for the given ID from the user's preferences.
|
||||
*
|
||||
* @param {string} id - The ID of the notification to remove from preferences.
|
||||
*/
|
||||
export function removeNotificationFromPrefs(id: string) {
|
||||
const parsedBannerPrefs = notificationPrefs();
|
||||
|
||||
if (!parsedBannerPrefs[id]) return;
|
||||
|
||||
delete parsedBannerPrefs[id];
|
||||
|
||||
updateNotificationPrefs(parsedBannerPrefs);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the notification banner should be shown based on the expiry time.
|
||||
*
|
||||
* @param {string} id - The ID of the notification.
|
||||
* @returns {boolean} - Returns true if the banner should be shown, false otherwise.
|
||||
*/
|
||||
export function shouldShowNotification(id: string): boolean {
|
||||
const parsedBannerPrefs = notificationPrefs();
|
||||
|
||||
const notificationPref = parsedBannerPrefs[id];
|
||||
|
||||
if (!notificationPref) return true;
|
||||
|
||||
if (Date.now() >= notificationPref.expiry) {
|
||||
notificationPref.state = 'shown';
|
||||
updateNotificationPrefs(parsedBannerPrefs);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import { writable } from 'svelte/store';
|
||||
import type { NotificationCoolOffOptions } from '$lib/helpers/notifications';
|
||||
|
||||
export type BottomModalAlertItem = {
|
||||
id: string;
|
||||
title: string;
|
||||
message: string;
|
||||
|
||||
src: Record<'dark' | 'light', string>;
|
||||
cta: Record<'text' | 'link', string>;
|
||||
plan: 'free' | 'pro' | 'scale' /*| 'enterprise'*/;
|
||||
learnMore?: Partial<Record<'text' | 'link', string>>;
|
||||
|
||||
show?: boolean;
|
||||
isHtml?: boolean;
|
||||
importance?: number;
|
||||
|
||||
closed?: () => void;
|
||||
notificationHideOptions?: NotificationCoolOffOptions;
|
||||
};
|
||||
|
||||
export const bottomModalAlerts = writable<BottomModalAlertItem[]>([]);
|
||||
|
||||
export const hideAllModalAlerts = () => {
|
||||
bottomModalAlerts.update((all) => all.map((t) => ({ ...t, show: false })));
|
||||
};
|
||||
|
||||
export const dismissBottomModalAlert = (id: string) => {
|
||||
bottomModalAlerts.update((all) => all.filter((t) => t.id !== id));
|
||||
};
|
||||
|
||||
export const showBottomModalAlert = (notification: BottomModalAlertItem) => {
|
||||
const defaults: Partial<BottomModalAlertItem> = {
|
||||
show: true,
|
||||
importance: 5,
|
||||
isHtml: false,
|
||||
...notification
|
||||
};
|
||||
|
||||
bottomModalAlerts.update((all) => {
|
||||
return [...all, defaults as BottomModalAlertItem];
|
||||
});
|
||||
};
|
||||
@@ -2,8 +2,14 @@ import { page } from '$app/stores';
|
||||
import { derived } from 'svelte/store';
|
||||
import type { Models } from '@appwrite.io/console';
|
||||
import { browser } from '$app/environment';
|
||||
import type { NotificationPrefItem } from '$lib/helpers/notifications';
|
||||
|
||||
export type Account = Models.User<{ organization?: string } & Record<string, string>>;
|
||||
export type Account = Models.User<
|
||||
{
|
||||
organization?: string;
|
||||
notificationPrefs: Record<string, NotificationPrefItem>;
|
||||
} & Record<string, string>
|
||||
>;
|
||||
|
||||
export const user = derived(page, ($page) => {
|
||||
if (browser) sessionStorage.setItem('account', JSON.stringify($page.data.account));
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { UploadBox } from '$lib/components';
|
||||
import { BottomModalAlert, MigrationBox, UploadBox } from '$lib/components';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { onMount } from 'svelte';
|
||||
import { project, stats } from './store';
|
||||
@@ -14,7 +14,6 @@
|
||||
teamSearcher,
|
||||
userSearcher
|
||||
} from '$lib/commandCenter/searchers';
|
||||
import { MigrationBox } from '$lib/components';
|
||||
import { page } from '$app/stores';
|
||||
import { base } from '$app/paths';
|
||||
import {
|
||||
@@ -106,3 +105,5 @@
|
||||
|
||||
<UploadBox />
|
||||
<MigrationBox />
|
||||
|
||||
<BottomModalAlert />
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
import { type BottomModalAlertItem, showBottomModalAlert } from '$lib/stores/bottom-alerts';
|
||||
|
||||
const listOfPromotions: BottomModalAlertItem[] = [];
|
||||
|
||||
export function addBottomModalAlerts() {
|
||||
listOfPromotions.forEach((promotion) => showBottomModalAlert(promotion));
|
||||
}
|
||||
Reference in New Issue
Block a user