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'; }