diff --git a/src/lib/components/bottomModalAlert.svelte b/src/lib/components/bottomModalAlert.svelte
index 3e98be3ca..949ddf408 100644
--- a/src/lib/components/bottomModalAlert.svelte
+++ b/src/lib/components/bottomModalAlert.svelte
@@ -3,6 +3,7 @@
import { hideNotification, shouldShowNotification } from '$lib/helpers/notifications';
import { app } from '$lib/stores/app';
import {
+ type BottomModalAlertAction,
type BottomModalAlertItem,
bottomModalAlertsConfig,
dismissBottomModalAlert,
@@ -111,6 +112,34 @@
}
}
+ // the button component cannot have both href and on:click!
+ function triggerWindowLink(alertAction: BottomModalAlertAction, event?: string) {
+ const shouldShowUpgrade = showUpgrade();
+ const url = shouldShowUpgrade
+ ? $upgradeURL
+ : alertAction.link({
+ organization: $organization,
+ project: $project
+ });
+
+ if (!shouldShowUpgrade && alertAction.external) {
+ window.open(url, '_blank');
+ } else {
+ goto(url);
+ }
+
+ if (alertAction?.hideOnClick === true) {
+ // be careful of this one.
+ // once clicked, its gone!
+ handleClose();
+ }
+
+ trackEvent(Click.PromoClick, {
+ promo: currentModalAlert.id,
+ type: shouldShowUpgrade ? 'upgrade' : (event ?? `cta_click_${currentModalAlert.id}`)
+ });
+ }
+
function showUpgrade() {
const plan = currentModalAlert.plan;
const organizationPlan = $organization?.billingPlan;
@@ -208,40 +237,23 @@
{#if currentModalAlert.learnMore}
+
@@ -332,20 +344,10 @@
{#if currentModalAlert.learnMore}
+
@@ -433,6 +436,7 @@
top: 1rem;
right: 1rem;
+ cursor: pointer;
background: #fff;
position: absolute;
display: inline-flex;
diff --git a/src/lib/images/promo-init3.png b/src/lib/images/promo-init3.png
deleted file mode 100644
index 842fa51f4..000000000
Binary files a/src/lib/images/promo-init3.png and /dev/null differ
diff --git a/src/lib/images/promos/init3-flutter.png b/src/lib/images/promos/init3-flutter.png
new file mode 100644
index 000000000..18470db09
Binary files /dev/null and b/src/lib/images/promos/init3-flutter.png differ
diff --git a/src/lib/images/promos/init3-images.png b/src/lib/images/promos/init3-images.png
new file mode 100644
index 000000000..26b0b5db7
Binary files /dev/null and b/src/lib/images/promos/init3-images.png differ
diff --git a/src/lib/images/promos/init3-keys.png b/src/lib/images/promos/init3-keys.png
new file mode 100644
index 000000000..c7a8f3907
Binary files /dev/null and b/src/lib/images/promos/init3-keys.png differ
diff --git a/src/lib/images/promos/init3-sites.png b/src/lib/images/promos/init3-sites.png
new file mode 100644
index 000000000..ae32a73c2
Binary files /dev/null and b/src/lib/images/promos/init3-sites.png differ
diff --git a/src/lib/images/promos/init3-tokens.png b/src/lib/images/promos/init3-tokens.png
new file mode 100644
index 000000000..1efd158ba
Binary files /dev/null and b/src/lib/images/promos/init3-tokens.png differ
diff --git a/src/lib/stores/bottom-alerts.ts b/src/lib/stores/bottom-alerts.ts
index aca0e067b..634dee01f 100644
--- a/src/lib/stores/bottom-alerts.ts
+++ b/src/lib/stores/bottom-alerts.ts
@@ -3,7 +3,7 @@ import type { NotificationCoolOffOptions } from '$lib/helpers/notifications';
import type { Organization } from '$lib/stores/organization';
import type { Models } from '@appwrite.io/console';
-type BottomModalAlertAction = {
+export type BottomModalAlertAction = {
text: string;
hideOnClick?: boolean;
link: (ctx: { organization: Organization; project: Models.Project }) => string;
diff --git a/src/routes/(console)/bottomAlerts.ts b/src/routes/(console)/bottomAlerts.ts
index 156a0dff8..8bc5a7c92 100644
--- a/src/routes/(console)/bottomAlerts.ts
+++ b/src/routes/(console)/bottomAlerts.ts
@@ -1,4 +1,8 @@
-import Init3Promo from '$lib/images/promo-init3.png';
+import Init3KeysPromo from '$lib/images/promos/init3-keys.png';
+import Init3SitesPromo from '$lib/images/promos/init3-sites.png';
+import Init3FlutterPromo from '$lib/images/promos/init3-flutter.png';
+import Init3ImagesPromo from '$lib/images/promos/init3-images.png';
+import Init3TokensPromo from '$lib/images/promos/init3-tokens.png';
import {
type BottomModalAlertItem,
@@ -9,41 +13,133 @@ import { isCloud } from '$lib/system';
const listOfPromotions: BottomModalAlertItem[] = [];
-if (isCloud) {
- const title = 'Join Init 19-23 May';
- const message =
- 'This release will change the way you build with Appwrite forever. Register for Init and join the giveaway.';
-
- const callToAction = {
- external: true,
- hideOnClick: true,
- text: 'Claim your ticket',
- link: () => 'https://apwr.dev/clcon'
+if (!isCloud) {
+ const getSrc = (image: string) => {
+ return {
+ dark: image,
+ light: image
+ };
};
- listOfPromotions.push({
- id: 'modal:init3',
- src: {
- dark: Init3Promo,
- light: Init3Promo
- },
- title,
- message,
- plan: 'free',
- importance: 8,
- scope: 'everywhere',
- cta: callToAction
+ const sharedCTA = (url: string) => ({
+ external: true,
+ link: () => url,
+ hideOnClick: true,
+ text: 'Read the announcement'
});
- // there's only one item.
- setMobileSingleAlertLayout({
- title,
- message,
- enabled: true,
- cta: callToAction
- });
+ const promos: BottomModalAlertItem[] = [
+ {
+ id: 'modal:init3_sites',
+ src: getSrc(Init3SitesPromo),
+ title: 'Announcing: Sites',
+ message:
+ 'The open-source Vercel alternative. With Sites, you deploy and host your websites and web apps right inside Appwrite.',
+ plan: 'free',
+ importance: 8,
+ scope: 'everywhere',
+ cta: sharedCTA('https://apwr.dev/sites1'),
+
+ // Day 1 = Monday 9 AM CET
+ show: isPromoLive('2025-05-19', '09:00')
+ },
+ {
+ id: 'modal:init3_flutter',
+ src: getSrc(Init3FlutterPromo),
+ title: 'Announcing: Hosting for Flutter Web',
+ message: 'Deploy your Flutter web apps directly from Appwrite Sites.',
+ plan: 'free',
+ importance: 8,
+ scope: 'everywhere',
+ cta: sharedCTA('https://apwr.dev/sitesfl'),
+
+ // Day 2 = Tuesday 3 PM CET
+ show: isPromoLive('2025-05-20', '15:00')
+ },
+ {
+ id: 'modal:init3_keys',
+ src: getSrc(Init3KeysPromo),
+ title: 'Announcing: Dev Keys',
+ message:
+ 'A new Appwrite feature that lets you bypass rate limits during local development.',
+ plan: 'free',
+ importance: 8,
+ scope: 'everywhere',
+ cta: sharedCTA('https://apwr.dev/devkeys'),
+
+ // Day 3 = Wednesday 3 PM CET
+ show: isPromoLive('2025-05-21', '15:00')
+ },
+ {
+ id: 'modal:init3_images',
+ src: getSrc(Init3ImagesPromo),
+ title: 'Announcing: New image formats',
+ message:
+ 'We have added support for two new image formats in Appwrite Storage: HEIC and AVIF.',
+ plan: 'free',
+ importance: 8,
+ scope: 'everywhere',
+ cta: sharedCTA('https://apwr.dev/imagefo'),
+
+ // Day 4 = Thursday 3 PM CET
+ show: isPromoLive('2025-05-22', '15:00')
+ },
+ {
+ id: 'modal:init3_file_tokens',
+ src: getSrc(Init3TokensPromo),
+ title: 'Announcing: File Tokens',
+ message:
+ 'File tokens let you share files easily and securely, without modifying permissions or changing project access.',
+ plan: 'free',
+ importance: 8,
+ scope: 'everywhere',
+ cta: sharedCTA('https://apwr.dev/filet'),
+
+ // Day 5 = Friday 3 PM CET
+ show: isPromoLive('2025-05-23', '15:00')
+ }
+ ];
+
+ const currentPromo = promos.find((promo) => promo.show);
+
+ if (currentPromo) {
+ listOfPromotions.push(currentPromo);
+
+ setMobileSingleAlertLayout({
+ title: currentPromo.title,
+ message: currentPromo.message,
+ enabled: true,
+ cta: currentPromo.cta
+ });
+ }
}
export function addBottomModalAlerts() {
listOfPromotions.forEach((promotion) => showBottomModalAlert(promotion));
}
+
+function isPromoLive(date: string, time: string, timeZone: string = 'Europe/Paris'): boolean {
+ const isoString = `${date}T${time}:00`;
+
+ const formatter = new Intl.DateTimeFormat('en', {
+ timeZone,
+ year: 'numeric',
+ month: '2-digit',
+ day: '2-digit',
+ hour: '2-digit',
+ minute: '2-digit',
+ hourCycle: 'h23'
+ });
+
+ const parts = formatter.formatToParts(new Date(isoString));
+ const map = Object.fromEntries(
+ parts.filter((p) => p.type !== 'literal').map((p) => [p.type, p.value])
+ );
+
+ const target = new Date(`${map.year}-${map.month}-${map.day}T${map.hour}:${map.minute}:00`);
+
+ const now = new Date();
+ const isSameDay = now.toISOString().slice(0, 10) === target.toISOString().slice(0, 10);
+
+ return isSameDay && now >= target;
+}
diff --git a/src/routes/(console)/organization-[organization]/+page.ts b/src/routes/(console)/organization-[organization]/+page.ts
index b6f1668b9..c39a468d8 100644
--- a/src/routes/(console)/organization-[organization]/+page.ts
+++ b/src/routes/(console)/organization-[organization]/+page.ts
@@ -22,8 +22,7 @@ export const load: PageLoad = async ({ params, url, route, depends, parent }) =>
Query.orderDesc('')
]);
- // set `default` or something else,
- // `undefined` in the url looks really off!
+ // set `default` if no region!
for (const project of projects.projects) {
project.region ??= 'default';
}