Repository: appwrite/console
Pull request #1849: Adds Init promos for each day
Author: login: ItzNotABug
Created: 2025-05-18T13:31:49Z | Merged: 2025-05-18T13:56:55Z

<!--
Thank you for sending the PR! We appreciate you spending the time to work on these changes.

Help us understand your motivation by explaining why you decided to make this change.

You can learn more about contributing to appwrite here: https://github.com/appwrite/appwrite/blob/master/CONTRIBUTING.md

Happy contributing!

-->

## What does this PR do?

(Provide a description of what this PR does.)

## Test Plan

(Write your test plan here. If you changed any code, please provide us with clear instructions on how you verified your changes work.)

## Related PRs and Issues

(If this PR is related to any other PR or resolves any issue or related to any issue link all related PR and issues here.)

### Have you read the [Contributing Guidelines on issues](https://github.com/appwrite/appwrite/blob/master/CONTRIBUTING.md)?

(Write your answer here.)


Diff:
diff --git a/src/lib/components/bottomModalAlert.svelte b/src/lib/components/bottomModalAlert.svelte
index 3e98be3cab..949ddf408c 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 @@
                         <div
                             class="buttons u-flex u-flex-vertical-mobile u-gap-4 u-padding-inline-8 u-padding-block-8">
                             <Button
+                                fullWidthMobile
                                 secondary={!hasOnlyPrimaryCta}
                                 class={`${hasOnlyPrimaryCta ? 'only-primary-cta' : ''}`}
-                                href={shouldShowUpgrade
-                                    ? $upgradeURL
-                                    : currentModalAlert.cta.link({
-                                          organization: $organization,
-                                          project: $project
-                                      })}
-                                external={!!currentModalAlert.cta.external}
-                                fullWidthMobile
-                                on:click={() => {
-                                    if (currentModalAlert.cta?.hideOnClick === true) {
-                                        // be careful of this one.
-                                        // once clicked, its gone!
-                                        handleClose();
-                                    }
-
-                                    trackEvent(Click.PromoClick, {
-                                        promo: currentModalAlert.id,
-                                        type: shouldShowUpgrade ? 'upgrade' : 'try_now'
-                                    });
-                                }}>
+                                on:click={() => triggerWindowLink(currentModalAlert.cta)}>
                                 {currentModalAlert.cta.text}
                             </Button>
 
                             {#if currentModalAlert.learnMore}
+                                <!-- docs, learn-more, etc always external -->
                                 <Button
                                     text
                                     class="button"
                                     external
                                     fullWidthMobile
                                     href={currentModalAlert.learnMore.link({
-                                        organization: $organization,
-                                        project: $project
+                                        project: $project,
+                                        organization: $organization
                                     })}>
                                     {currentModalAlert.learnMore.text}
                                 </Button>
@@ -332,20 +344,10 @@
                                 <Button
                                     secondary={!hasOnlyPrimaryCta}
                                     class="button"
-                                    href={shouldShowUpgrade
-                                        ? $upgradeURL
-                                        : currentModalAlert.cta.link({
-                                              organization: $organization,
-                                              project: $project
-                                          })}
-                                    external={!!currentModalAlert.cta.external}
                                     fullWidthMobile
                                     on:click={() => {
                                         openModalOnMobile = false;
-                                        trackEvent(Click.PromoClick, {
-                                            promo: currentModalAlert.id,
-                                            type: shouldShowUpgrade ? 'upgrade' : 'try_now'
-                                        });
+                                        triggerWindowLink(currentModalAlert.cta);
                                     }}>
                                     {shouldShowUpgrade
                                         ? 'Upgrade plan'
@@ -353,6 +355,7 @@
                                 </Button>
 
                                 {#if currentModalAlert.learnMore}
+                                    <!-- docs, learn-more, etc always external -->
                                     <Button
                                         text
                                         class="button"
@@ -360,8 +363,8 @@
                                         fullWidthMobile
                                         on:click={() => (openModalOnMobile = false)}
                                         href={currentModalAlert.learnMore.link({
-                                            organization: $organization,
-                                            project: $project
+                                            project: $project,
+                                            organization: $organization
                                         })}>
                                         {currentModalAlert.learnMore.text}
                                     </Button>
@@ -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 842fa51f4d..0000000000
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 0000000000..18470db093
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 0000000000..26b0b5db78
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 0000000000..c7a8f39074
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 0000000000..ae32a73c29
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 0000000000..1efd158ba6
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 aca0e067b5..634dee01fe 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 156a0dff8c..7cc9ce36ce 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,
@@ -6,44 +10,123 @@ import {
     showBottomModalAlert
 } from '$lib/stores/bottom-alerts';
 import { isCloud } from '$lib/system';
+import { isSameDay } from '$lib/helpers/date';
 
 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.';
+if (!isCloud) {
+    const getSrc = (image: string) => {
+        return {
+            dark: image,
+            light: image
+        };
+    };
 
-    const callToAction = {
+    const sharedCTA = (url: string) => ({
         external: true,
+        link: () => url,
         hideOnClick: true,
-        text: 'Claim your ticket',
-        link: () => 'https://apwr.dev/clcon'
-    };
+        text: 'Read the announcement'
+    });
 
-    listOfPromotions.push({
-        id: 'modal:init3',
-        src: {
-            dark: Init3Promo,
-            light: Init3Promo
+    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')
         },
-        title,
-        message,
-        plan: 'free',
-        importance: 8,
-        scope: 'everywhere',
-        cta: callToAction
-    });
+        {
+            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'),
 
-    // there's only one item.
-    setMobileSingleAlertLayout({
-        title,
-        message,
-        enabled: true,
-        cta: callToAction
-    });
+            // 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));
 }
+
+export function isPromoLive(
+    date: string,
+    time: string,
+    timeZone: string = 'Asia/Kolkata'
+): boolean {
+    const now = new Date();
+    const targetString = `${date}T${time}:00`;
+    const target = new Date(new Date(targetString).toLocaleString('en-US', { timeZone }));
+
+    return isSameDay(now, target) && now >= target;
+}
diff --git a/src/routes/(console)/organization-[organization]/+page.ts b/src/routes/(console)/organization-[organization]/+page.ts
index b6f1668b90..c39a468d84 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';
     }
diff --git a/src/routes/(console)/organization-[organization]/createProject.svelte b/src/routes/(console)/organization-[organization]/createProject.svelte
index 95b7741e29..2e45d77c23 100644
--- a/src/routes/(console)/organization-[organization]/createProject.svelte
+++ b/src/routes/(console)/organization-[organization]/createProject.svelte
@@ -36,7 +36,7 @@
                 type: 'success',
                 message: `${name} has been created`
             });
-            await goto(`${base}/project-${project.region}-${project.$id}`);
+            await goto(`${base}/project-${project.region ?? 'default'}-${project.$id}`);
         } catch (e) {
             error = e.message;
             trackError(e, Submit.ProjectCreate);


Reviews:
user: login: TorstenDittmann
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-18T13:37:47Z
user: login: TorstenDittmann
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-18T13:38:23Z
user: login: ItzNotABug
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-18T13:38:44Z
user: login: TorstenDittmann
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-18T13:47:15Z

Review comments:
diff_hunk: @@ -111,6 +112,34 @@
         }
     }
 
+    // the button component cannot have both href and on:click!
path: src/lib/components/bottomModalAlert.svelte
user: login: TorstenDittmann
body: how come? it should be able to 🤔 
created_at: 2025-05-18T13:34:34Z
author_association: CONTRIBUTOR
diff_hunk: @@ -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.';
+if (!isCloud) {
+    const getSrc = (image: string) => {
+        return {
+            dark: image,
+            light: image
+        };
+    };
 
-    const callToAction = {
+    const sharedCTA = (url: string) => ({
         external: true,
+        link: () => url,
         hideOnClick: true,
-        text: 'Claim your ticket',
-        link: () => 'https://apwr.dev/clcon'
-    };
+        text: 'Read the announcement'
+    });
 
-    listOfPromotions.push({
-        id: 'modal:init3',
-        src: {
-            dark: Init3Promo,
-            light: Init3Promo
+    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')
         },
-        title,
-        message,
-        plan: 'free',
-        importance: 8,
-        scope: 'everywhere',
-        cta: callToAction
-    });
+        {
+            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'),
 
-    // there's only one item.
-    setMobileSingleAlertLayout({
-        title,
-        message,
-        enabled: true,
-        cta: callToAction
-    });
+            // 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);
path: src/routes/(console)/bottomAlerts.ts
user: login: TorstenDittmann
body: i fell like this is a bit over complciated? cant we just do one date object to be passed here and compare?
created_at: 2025-05-18T13:37:25Z
author_association: CONTRIBUTOR
diff_hunk: @@ -111,6 +112,34 @@
         }
     }
 
+    // the button component cannot have both href and on:click!
path: src/lib/components/bottomModalAlert.svelte
user: login: TorstenDittmann
body: you can ignore this btw, its just something we should look up
created_at: 2025-05-18T13:38:23Z
author_association: CONTRIBUTOR
diff_hunk: @@ -111,6 +112,34 @@
         }
     }
 
+    // the button component cannot have both href and on:click!
path: src/lib/components/bottomModalAlert.svelte
user: login: ItzNotABug
body: didn't work for me when I used both.
created_at: 2025-05-18T13:38:44Z
author_association: CONTRIBUTOR

PR comments:
user: login: ItzNotABug
created_at: 2025-05-18T13:56:48Z
body: NOTE: e2e are supposed to fail and can be fixed later. Approved and confirmed by @TorstenDittmann.
Merging...
author_association: CONTRIBUTOR