{#if $page.data?.header}
diff --git a/src/lib/stores/bottom-alerts.ts b/src/lib/stores/bottom-alerts.ts index 634dee01fe..0e80a6dfa8 100644 --- a/src/lib/stores/bottom-alerts.ts +++ b/src/lib/stores/bottom-alerts.ts @@ -1,10 +1,14 @@ import { writable } from 'svelte/store'; -import type { NotificationCoolOffOptions } from '$lib/helpers/notifications'; -import type { Organization } from '$lib/stores/organization'; +import type { Component } from 'svelte'; import type { Models } from '@appwrite.io/console'; +import type { Organization } from '$lib/stores/organization'; +import type { NotificationCoolOffOptions } from '$lib/helpers/notifications'; export type BottomModalAlertAction = { text: string; + color?: Record<'light' | 'dark', string> | string; + background?: Record<'light' | 'dark', string> | string; + backgroundHover?: Record<'light' | 'dark', string> | string; hideOnClick?: boolean; link: (ctx: { organization: Organization; project: Models.Project }) => string; external?: boolean; @@ -32,7 +36,10 @@ export type BottomModalAlertItem = { title: string; message: string; - src: Record<'dark' | 'light', string>; + // use either of these! + src?: Record<'dark' | 'light', string>; + backgroundComponent?: Component; + cta: BottomModalAlertAction; learnMore?: BottomModalAlertAction; plan: 'free' | 'pro' | 'scale' /*| 'enterprise'*/; @@ -43,6 +50,12 @@ export type BottomModalAlertItem = { closed?: () => void; scope: 'organization' | 'project' | 'everywhere'; notificationHideOptions?: NotificationCoolOffOptions; + + /** + * if true, + * uses same title, message on mobile floating window. + */ + sameContentOnMobileLayout?: boolean; }; type BottomModalAlertState = { diff --git a/src/lib/stores/stripe.ts b/src/lib/stores/stripe.ts index d73f182845..9f4bc727bc 100644 --- a/src/lib/stores/stripe.ts +++ b/src/lib/stores/stripe.ts @@ -26,6 +26,10 @@ export const isStripeInitialized = writable(false); export async function initializeStripe(node: HTMLElement) { if (!get(stripe)) return; + + // cleanup any existing state + await unmountPaymentElement(); + isStripeInitialized.set(true); const methods = await sdk.forConsole.billing.listPaymentMethods(); @@ -54,10 +58,20 @@ export async function initializeStripe(node: HTMLElement) { export async function unmountPaymentElement() { isStripeInitialized.set(false); - paymentElement?.unmount(); + + if (paymentElement) { + try { + paymentElement.unmount(); + paymentElement.destroy(); + } catch (e) { + console.debug('Payment element cleanup:', e.message); + } + } + + elements = null; clientSecret = null; paymentMethod = null; - elements = null; + paymentElement = null; } export async function submitStripeCard(name: string, organizationId?: string) { diff --git a/src/routes/(console)/+layout.svelte b/src/routes/(console)/+layout.svelte index 481a37012a..7c571a75a9 100644 --- a/src/routes/(console)/+layout.svelte +++ b/src/routes/(console)/+layout.svelte @@ -336,7 +336,6 @@ { Query.offset(offset), Query.limit(limit), Query.orderDesc(''), - Query.equal('platform', resolvedProfile.organizationPlatform) + ...(isCloud ? [Query.equal('platform', resolvedProfile.organizationPlatform)] : []) ]; const organizations = !isCloud diff --git a/src/routes/(console)/bottomAlerts.ts b/src/routes/(console)/bottomAlerts.ts index 5a8f6e3531..ba94c9af83 100644 --- a/src/routes/(console)/bottomAlerts.ts +++ b/src/routes/(console)/bottomAlerts.ts @@ -1,33 +1,48 @@ import { isCloud } from '$lib/system'; import { isSameDay } from '$lib/helpers/date'; -import { type BottomModalAlertItem, showBottomModalAlert } from '$lib/stores/bottom-alerts'; -import AiSuggestionsDark from '$lib/images/promos/ai-suggestions-dark.png'; -import AiSuggestionsLight from '$lib/images/promos/ai-suggestions-light.png'; +import Imagine from '$lib/components/promos/imagine.svelte'; +import { + type BottomModalAlertItem, + setMobileSingleAlertLayout, + showBottomModalAlert +} from '$lib/stores/bottom-alerts'; + +const SHOW_IMAGINE_PROMO = false; import { ProfileMode, resolvedProfile } from '$lib/profiles/index.svelte'; const listOfPromotions: BottomModalAlertItem[] = []; -if (isCloud) { - const aiSuggestionsPromo: BottomModalAlertItem = { - id: 'modal:database_ai_suggestions_announcement', - src: { - dark: AiSuggestionsDark, - light: AiSuggestionsLight - }, - title: 'Announcing Database AI suggestions', - message: 'From table name to schema in one click.', - plan: 'free', +if (isCloud && SHOW_IMAGINE_PROMO) { + const imaginePromo: BottomModalAlertItem = { + id: 'modal:imagine.dev', + backgroundComponent: Imagine, + title: 'Introducing Imagine', + message: 'the most complete AI builder to date', importance: 8, - scope: 'project', + scope: 'everywhere', + plan: 'free', cta: { - text: 'Read announcement', - link: () => 'https://appwrite.io/blog/post/announcing-database-ai-suggestions', + text: 'Try it now', + color: { + light: '#FFFFFF', + dark: '#000000' + }, + background: { + light: '#000000', + dark: '#FFFFFF' + }, + backgroundHover: { + light: '#333333', + dark: '#CCCCCC' + }, + link: () => 'https://cloud.appwrite.io', external: true, hideOnClick: true }, show: true }; - listOfPromotions.push(aiSuggestionsPromo); + + listOfPromotions.push(imaginePromo); } export function addBottomModalAlerts() { @@ -35,6 +50,13 @@ export function addBottomModalAlerts() { if (resolvedProfile.id !== ProfileMode.CONSOLE) return; listOfPromotions.forEach((promotion) => showBottomModalAlert(promotion)); + + // only for imagine! + if (listOfPromotions.length > 0) { + const imaginePromo = listOfPromotions[0]; + const { cta, title, message } = imaginePromo; + setMobileSingleAlertLayout({ enabled: true, cta, title, message }); + } } // use this for time based promo handling diff --git a/src/routes/(console)/organization-[organization]/+layout.ts b/src/routes/(console)/organization-[organization]/+layout.ts index 17878b5bd1..6c56bb44ce 100644 --- a/src/routes/(console)/organization-[organization]/+layout.ts +++ b/src/routes/(console)/organization-[organization]/+layout.ts @@ -80,6 +80,7 @@ export const load: LayoutLoad = async ({ params, depends, parent }) => { ...prefs, [resolvedProfile.organizationPrefKey]: null }; + sdk.forConsole.account.updatePrefs({ prefs: newPrefs }); @@ -107,7 +108,9 @@ async function checkPlatformAndRedirect( prefs: Record ): Promise { // check if preloaded - let requestedOrg = organizations.teams.find((team) => team.$id === params.organization); + let requestedOrg = organizations.teams.find((team) => team.$id === params.organization) as + | Organization + | undefined; // not found, load! if (!requestedOrg) { @@ -120,6 +123,8 @@ async function checkPlatformAndRedirect( } } + if (!isCloud) return requestedOrg; + if (requestedOrg && requestedOrg.platform !== resolvedProfile.organizationPlatform) { const orgIdInPrefs = prefs[resolvedProfile.organizationPrefKey]; diff --git a/src/routes/(console)/organization-[organization]/billing/paymentMethods.svelte b/src/routes/(console)/organization-[organization]/billing/paymentMethods.svelte index 47352d3893..eab5686271 100644 --- a/src/routes/(console)/organization-[organization]/billing/paymentMethods.svelte +++ b/src/routes/(console)/organization-[organization]/billing/paymentMethods.svelte @@ -309,11 +309,11 @@ {#if showPayment && isCloud && hasStripePublicKey} { + onCardSubmit={(card) => { if (isSelectedBackup) { - addBackupPaymentMethod(e.detail.$id); + addBackupPaymentMethod(card.$id); } else { - addPaymentMethod(e.detail.$id); + addPaymentMethod(card.$id); } }} /> {/if} diff --git a/src/routes/(console)/organization-[organization]/billing/replaceCard.svelte b/src/routes/(console)/organization-[organization]/billing/replaceCard.svelte index a0d5de92da..ea4cc7d267 100644 --- a/src/routes/(console)/organization-[organization]/billing/replaceCard.svelte +++ b/src/routes/(console)/organization-[organization]/billing/replaceCard.svelte @@ -19,7 +19,7 @@ export let methods: PaymentList; let name: string; - let error: string; + let error: string = null; let selectedPaymentMethodId: string; let showState: boolean = false; let state: string = ''; diff --git a/src/routes/(console)/organization-[organization]/billing/retryPaymentModal.svelte b/src/routes/(console)/organization-[organization]/billing/retryPaymentModal.svelte index 22797e6df3..65e217d2c6 100644 --- a/src/routes/(console)/organization-[organization]/billing/retryPaymentModal.svelte +++ b/src/routes/(console)/organization-[organization]/billing/retryPaymentModal.svelte @@ -147,7 +147,6 @@ onSubmit={handleSubmit} size="big" title="Retry payment"> -

Your payment of {formatCurrency(invoice.grossAmount)} due on {toLocaleDate( invoice.dueAt diff --git a/src/routes/(console)/project-[region]-[project]/+layout.ts b/src/routes/(console)/project-[region]-[project]/+layout.ts index c865a2eaa8..019b442f7f 100644 --- a/src/routes/(console)/project-[region]-[project]/+layout.ts +++ b/src/routes/(console)/project-[region]-[project]/+layout.ts @@ -64,7 +64,7 @@ export const load: LayoutLoad = async ({ params, route, depends, parent }) => { // not the right organization project based on platform, // redirect to organization, and it should handle the rest! - if (organization.platform !== resolvedProfile.organizationPlatform) { + if (isCloud && organization.platform !== resolvedProfile.organizationPlatform) { redirect( 303, resolve('/(console)/organization-[organization]', { diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/subNavigation.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/subNavigation.svelte index 45b084abc1..1dd5059daa 100644 --- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/subNavigation.svelte +++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/subNavigation.svelte @@ -12,7 +12,8 @@ Layout, Link, Typography, - Divider + Divider, + Skeleton } from '@appwrite.io/pink-svelte'; import { IconChevronDown, @@ -37,6 +38,7 @@ const databaseId = $derived(page.params.database); let openBottomSheet = $state(false); + let loading = $state(true); let tables = $state({ total: 0, @@ -62,10 +64,14 @@ const tableContentPadding = $derived($headerAlert.top !== 0 ? '210px' : '140px'); async function loadTables() { - tables = await sdk.forProject(region, project).tablesDB.listTables({ - databaseId: databaseId, - queries: [Query.orderDesc(''), Query.limit(100)] - }); + try { + tables = await sdk.forProject(region, project).tablesDB.listTables({ + databaseId: databaseId, + queries: [Query.orderDesc(''), Query.limit(100)] + }); + } finally { + loading = false; + } } onMount(() => { @@ -94,7 +100,20 @@ {data.database?.name}

- {#if tables?.total} + {#if loading} +
    + {#each Array(2) as _} + +
  • +
    + +
    +
  • +
    + {/each} +
+ {:else if tables?.total}
    {#each sortedTables as table, index} {@const href = `${base}/project-${region}-${project}/databases/database-${databaseId}/table-${table.$id}`}