mirror of
https://github.com/appwrite/console.git
synced 2026-06-06 19:27:48 +00:00
Merge pull request #637 from appwrite/chore-prerelease-modal
feat: pre release modal
This commit is contained in:
@@ -60,3 +60,5 @@ export { default as SvgIcon } from './svgIcon.svelte';
|
||||
export { default as MigrationBox } from './migrationBox.svelte';
|
||||
export { default as FloatingActionBar } from './floatingActionBar.svelte';
|
||||
export { default as LoadingDots } from './loadingDots.svelte';
|
||||
export { default as ModalWrapper } from './modalWrapper.svelte';
|
||||
export { default as ModalSideCol } from './modalSideCol.svelte';
|
||||
|
||||
+66
-130
@@ -1,6 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher, onDestroy, onMount } from 'svelte';
|
||||
import { Alert } from '$lib/components';
|
||||
import { Alert, ModalWrapper } from '$lib/components';
|
||||
import { trackEvent } from '$lib/actions/analytics';
|
||||
import { Form } from '$lib/elements/forms';
|
||||
import { disableCommands } from '$lib/commandCenter';
|
||||
@@ -18,61 +17,8 @@
|
||||
export let title = '';
|
||||
export let description = '';
|
||||
|
||||
let dialog: HTMLDialogElement;
|
||||
let alert: HTMLElement;
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
onMount(() => {
|
||||
if (show) openModal();
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
if (show) closeModal();
|
||||
});
|
||||
|
||||
function handleBLur(event: MouseEvent) {
|
||||
if (event.target === dialog) {
|
||||
trackEvent('click_close_modal', {
|
||||
from: 'backdrop'
|
||||
});
|
||||
closeModal();
|
||||
}
|
||||
}
|
||||
function openModal() {
|
||||
if (dialog && !dialog.open) {
|
||||
dialog.showModal();
|
||||
document.documentElement.classList.add('u-overflow-hidden');
|
||||
}
|
||||
}
|
||||
|
||||
function closeModal() {
|
||||
if (closable) {
|
||||
if (dialog && dialog.open) {
|
||||
dispatch('close');
|
||||
dialog.close();
|
||||
show = false;
|
||||
document.documentElement.classList.remove('u-overflow-hidden');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handleKeydown(event: KeyboardEvent) {
|
||||
if (event.key === 'Escape') {
|
||||
event.preventDefault();
|
||||
trackEvent('click_close_modal', {
|
||||
from: 'escape'
|
||||
});
|
||||
closeModal();
|
||||
}
|
||||
}
|
||||
|
||||
$: if (show) {
|
||||
openModal();
|
||||
} else {
|
||||
closeModal();
|
||||
}
|
||||
|
||||
$: $disableCommands(show);
|
||||
|
||||
$: if (error) {
|
||||
@@ -80,82 +26,72 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:window on:mousedown={handleBLur} on:keydown={handleKeydown} />
|
||||
|
||||
<dialog
|
||||
class="modal"
|
||||
class:is-small={size === 'small'}
|
||||
class:is-big={size === 'big'}
|
||||
class:is-separate-header={headerDivider}
|
||||
bind:this={dialog}
|
||||
on:cancel|preventDefault>
|
||||
{#if show}
|
||||
<Form isModal {onSubmit}>
|
||||
<header class="modal-header">
|
||||
<div class="u-flex u-main-space-between u-cross-center u-gap-16">
|
||||
<div class="u-flex u-cross-center u-gap-16">
|
||||
{#if icon}
|
||||
<div
|
||||
class="avatar is-medium"
|
||||
class:is-success={state === 'success'}
|
||||
class:is-warning={state === 'warning'}
|
||||
class:is-danger={state === 'error'}
|
||||
class:is-info={state === 'info'}>
|
||||
<span class={`icon-${icon}`} aria-hidden="true" />
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<h4 class="modal-title heading-level-5">
|
||||
<slot name="title">
|
||||
{title}
|
||||
</slot>
|
||||
</h4>
|
||||
</div>
|
||||
{#if closable}
|
||||
<button
|
||||
type="button"
|
||||
class="button is-text is-only-icon"
|
||||
style="--button-size:1.5rem;"
|
||||
aria-label="Close Modal"
|
||||
title="Close Modal"
|
||||
on:click={() =>
|
||||
trackEvent('click_close_modal', {
|
||||
from: 'button'
|
||||
})}
|
||||
on:click={closeModal}>
|
||||
<span class="icon-x" aria-hidden="true" />
|
||||
</button>
|
||||
<ModalWrapper bind:show {size} {headerDivider} let:close>
|
||||
<Form isModal {onSubmit}>
|
||||
<header class="modal-header">
|
||||
<div class="u-flex u-main-space-between u-cross-center u-gap-16">
|
||||
<div class="u-flex u-cross-center u-gap-16">
|
||||
{#if icon}
|
||||
<div
|
||||
class="avatar is-medium"
|
||||
class:is-success={state === 'success'}
|
||||
class:is-warning={state === 'warning'}
|
||||
class:is-danger={state === 'error'}
|
||||
class:is-info={state === 'info'}>
|
||||
<span class={`icon-${icon}`} aria-hidden="true" />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<p class="u-margin-block-start-4">
|
||||
<slot name="description">
|
||||
{description}
|
||||
</slot>
|
||||
</p>
|
||||
</header>
|
||||
<div class="modal-content">
|
||||
{#if error}
|
||||
<div bind:this={alert}>
|
||||
<Alert
|
||||
dismissible
|
||||
type="warning"
|
||||
on:dismiss={() => {
|
||||
error = null;
|
||||
}}>
|
||||
{error}
|
||||
</Alert>
|
||||
</div>
|
||||
{/if}
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
{#if $$slots.footer}
|
||||
<div class="modal-footer">
|
||||
<div class="u-flex u-main-end u-gap-16">
|
||||
<slot name="footer" />
|
||||
</div>
|
||||
<h4 class="modal-title heading-level-5">
|
||||
<slot name="title">
|
||||
{title}
|
||||
</slot>
|
||||
</h4>
|
||||
</div>
|
||||
{#if closable}
|
||||
<button
|
||||
type="button"
|
||||
class="button is-text is-only-icon"
|
||||
style="--button-size:1.5rem;"
|
||||
aria-label="Close Modal"
|
||||
title="Close Modal"
|
||||
on:click={() =>
|
||||
trackEvent('click_close_modal', {
|
||||
from: 'button'
|
||||
})}
|
||||
on:click={close}>
|
||||
<span class="icon-x" aria-hidden="true" />
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
<p class="u-margin-block-start-4">
|
||||
<slot name="description">
|
||||
{description}
|
||||
</slot>
|
||||
</p>
|
||||
</header>
|
||||
<div class="modal-content">
|
||||
{#if error}
|
||||
<div bind:this={alert}>
|
||||
<Alert
|
||||
dismissible
|
||||
type="warning"
|
||||
on:dismiss={() => {
|
||||
error = null;
|
||||
}}>
|
||||
{error}
|
||||
</Alert>
|
||||
</div>
|
||||
{/if}
|
||||
</Form>
|
||||
{/if}
|
||||
</dialog>
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
{#if $$slots.footer}
|
||||
<div class="modal-footer">
|
||||
<div class="u-flex u-main-end u-gap-16">
|
||||
<slot name="footer" />
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</Form>
|
||||
</ModalWrapper>
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
<script lang="ts">
|
||||
import { ModalWrapper } from '$lib/components';
|
||||
import { trackEvent } from '$lib/actions/analytics';
|
||||
|
||||
export let show = false;
|
||||
export let title = '';
|
||||
export let description = '';
|
||||
export let style = '';
|
||||
</script>
|
||||
|
||||
<ModalWrapper bind:show let:close {style}>
|
||||
<div class="grid-1-1 u-width-full-line mk-grid">
|
||||
<div class="mk-grid-item-1 u-width-full-line">
|
||||
<slot name="side" />
|
||||
</div>
|
||||
<div class="u-padding-32 mk-grid-item-2">
|
||||
<header class="modal-header">
|
||||
<div class="u-flex u-main-space-between u-gap-16">
|
||||
<h4 class="modal-title heading-level-5">
|
||||
<slot name="title">
|
||||
{title}
|
||||
</slot>
|
||||
</h4>
|
||||
<button
|
||||
type="button"
|
||||
class="button is-text is-only-icon"
|
||||
style="--button-size:1.5rem;"
|
||||
aria-label="Close Modal"
|
||||
title="Close Modal"
|
||||
on:click={() =>
|
||||
trackEvent('click_close_modal', {
|
||||
from: 'button'
|
||||
})}
|
||||
on:click={close}>
|
||||
<span class="icon-x" aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
<p class="u-margin-block-start-4">
|
||||
<slot name="description">
|
||||
{description}
|
||||
</slot>
|
||||
</p>
|
||||
</header>
|
||||
<div class="modal-content">
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ModalWrapper>
|
||||
|
||||
<style lang="scss">
|
||||
.mk-grid {
|
||||
overflow: hidden;
|
||||
}
|
||||
@media screen and (max-width: 768px) {
|
||||
.mk-grid {
|
||||
&-item-1 {
|
||||
order: 2;
|
||||
max-height: 16rem;
|
||||
}
|
||||
|
||||
&-item-2 {
|
||||
order: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,82 @@
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher, onDestroy, onMount } from 'svelte';
|
||||
import { trackEvent } from '$lib/actions/analytics';
|
||||
import { disableCommands } from '$lib/commandCenter';
|
||||
|
||||
export let show = false;
|
||||
export let size: 'small' | 'big' = null;
|
||||
export let closable = true;
|
||||
export let headerDivider = true;
|
||||
export let style = '';
|
||||
|
||||
let dialog: HTMLDialogElement;
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
onMount(() => {
|
||||
if (show) openModal();
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
if (show) closeModal();
|
||||
});
|
||||
|
||||
function handleBLur(event: MouseEvent) {
|
||||
if (event.target === dialog) {
|
||||
trackEvent('click_close_modal', {
|
||||
from: 'backdrop'
|
||||
});
|
||||
closeModal();
|
||||
}
|
||||
}
|
||||
function openModal() {
|
||||
if (dialog && !dialog.open) {
|
||||
dialog.showModal();
|
||||
document.documentElement.classList.add('u-overflow-hidden');
|
||||
}
|
||||
}
|
||||
|
||||
function closeModal() {
|
||||
if (closable) {
|
||||
if (dialog && dialog.open) {
|
||||
dispatch('close');
|
||||
dialog.close();
|
||||
show = false;
|
||||
document.documentElement.classList.remove('u-overflow-hidden');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handleKeydown(event: KeyboardEvent) {
|
||||
if (event.key === 'Escape') {
|
||||
event.preventDefault();
|
||||
trackEvent('click_close_modal', {
|
||||
from: 'escape'
|
||||
});
|
||||
closeModal();
|
||||
}
|
||||
}
|
||||
|
||||
$: if (show) {
|
||||
openModal();
|
||||
} else {
|
||||
closeModal();
|
||||
}
|
||||
|
||||
$: $disableCommands(show);
|
||||
</script>
|
||||
|
||||
<svelte:window on:mousedown={handleBLur} on:keydown={handleKeydown} />
|
||||
|
||||
<dialog
|
||||
class="modal"
|
||||
class:is-small={size === 'small'}
|
||||
class:is-big={size === 'big'}
|
||||
class:is-separate-header={headerDivider}
|
||||
{style}
|
||||
bind:this={dialog}
|
||||
on:cancel|preventDefault>
|
||||
{#if show}
|
||||
<slot close={closeModal} />
|
||||
{/if}
|
||||
</dialog>
|
||||
@@ -0,0 +1,91 @@
|
||||
<script lang="ts">
|
||||
import { Box, ModalSideCol } from '$lib/components';
|
||||
import SideLight from './side-light.png';
|
||||
import SideDark from './side-dark.png';
|
||||
import { app } from '$lib/stores/app';
|
||||
import { Button } from '$lib/elements/forms';
|
||||
import { trackEvent } from '$lib/actions/analytics';
|
||||
|
||||
export let show = false;
|
||||
</script>
|
||||
|
||||
<ModalSideCol bind:show title="Get ready for Appwrite Pro" style="max-width: min(53rem,95%)">
|
||||
<svelte:fragment slot="side">
|
||||
{#if $app.themeInUse === 'dark'}
|
||||
<img
|
||||
src={SideDark}
|
||||
style="object-fit: cover; object-position: 30% 100%; height: 100%; width: 100%; aspect-ratio: 1 / 1;"
|
||||
alt="appwrite pro"
|
||||
aria-hidden="true"
|
||||
width="376" />
|
||||
{:else}
|
||||
<img
|
||||
src={SideLight}
|
||||
style="object-fit: cover; object-position: 30% 100%; height: 100%; width: 100%; aspect-ratio: 1 / 1;"
|
||||
alt="appwrite pro"
|
||||
aria-hidden="true"
|
||||
width="376" />
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
|
||||
<div class="u-flex u-flex-vertical u-gap-16">
|
||||
<p class="text">
|
||||
Appwrite will enable billing before the end of the year, which means access to our long
|
||||
awaited Pro plan. It also means you will be limited to <b
|
||||
>one free organization per account</b
|
||||
>.
|
||||
</p>
|
||||
<p class="text">
|
||||
Consider upgrading and take advantage of the extra resources that come with the Pro
|
||||
plan. You can also use our Migrations service to self-host some of your projects
|
||||
instead. Learn more in our <a
|
||||
class="link"
|
||||
href="https://appwrite.io/docs/advanced/migrations"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
on:click={() =>
|
||||
trackEvent('click_open_website', {
|
||||
source: 'pre_billing_release_modal',
|
||||
destination: 'docs_migration'
|
||||
})}>documentation</a
|
||||
>.
|
||||
</p>
|
||||
</div>
|
||||
<Box>
|
||||
<b class="body-text-1">Pro - $15 per member per billing period</b>
|
||||
|
||||
<ul class="list u-flex-vertical u-gap-8 u-margin-block-start-24">
|
||||
<li class="u-flex u-gap-8 u-cross-center">
|
||||
<span class="icon-check-circle" aria-hidden="true" />
|
||||
<span class="text">Unlimited org. members</span>
|
||||
</li>
|
||||
<li class="u-flex u-gap-8 u-cross-center">
|
||||
<span class="icon-check-circle" aria-hidden="true" />
|
||||
<span class="text">300GB bandwidth</span>
|
||||
</li>
|
||||
<li class="u-flex u-gap-8 u-cross-center">
|
||||
<span class="icon-check-circle" aria-hidden="true" />
|
||||
<span class="text">200,000 monthly active users</span>
|
||||
</li>
|
||||
<li class="u-flex u-gap-8 u-cross-center">
|
||||
<span class="icon-check-circle" aria-hidden="true" />
|
||||
<span class="text">Email support</span>
|
||||
</li>
|
||||
<li class="u-flex u-gap-8 u-cross-center">
|
||||
<span class="icon-check-circle" aria-hidden="true" />
|
||||
<span class="text">And more</span>
|
||||
</li>
|
||||
</ul>
|
||||
<Button
|
||||
fullWidth
|
||||
class="u-margin-block-start-24"
|
||||
href="https://appwrite.io/pricing"
|
||||
on:click={() =>
|
||||
trackEvent('click_open_website', {
|
||||
source: 'pre_billing_release_modal',
|
||||
destination: 'pricing'
|
||||
})}>
|
||||
Learn more about Pro
|
||||
</Button>
|
||||
</Box>
|
||||
</ModalSideCol>
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 619 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 507 KiB |
@@ -24,7 +24,9 @@
|
||||
import { openMigrationWizard } from './(migration-wizard)';
|
||||
import { project } from './project-[project]/store';
|
||||
import { feedback } from '$lib/stores/feedback';
|
||||
import { consoleVariables } from './store';
|
||||
import { consoleVariables, showPrereleaseModal } from './store';
|
||||
import { isCloud } from '$lib/system';
|
||||
import PreReleaseModal from './(billing-modal)/preReleaseModal.svelte';
|
||||
|
||||
function kebabToSentenceCase(str: string) {
|
||||
return str
|
||||
@@ -224,6 +226,9 @@
|
||||
|
||||
onMount(() => {
|
||||
loading.set(false);
|
||||
if (isCloud && !$page.url.pathname.includes('/console/onboarding')) {
|
||||
checkForPreReleaseProModal();
|
||||
}
|
||||
|
||||
setInterval(() => {
|
||||
checkForFeedback(INTERVAL);
|
||||
@@ -243,6 +248,48 @@
|
||||
}
|
||||
}
|
||||
|
||||
function checkForPreReleaseProModal() {
|
||||
const modalTime = localStorage.getItem('preReleaseProModal');
|
||||
const notificationTime = localStorage.getItem('preReleaseProNotification');
|
||||
const now = Date.now();
|
||||
// show the modal if it was never shown
|
||||
if (!modalTime) {
|
||||
localStorage.setItem('preReleaseProModal', Date.now().toString());
|
||||
localStorage.setItem('preReleaseProNotification', Date.now().toString());
|
||||
showPrereleaseModal.set(true);
|
||||
} else {
|
||||
const interval = 5 * 24 * 60 * 60 * 1000;
|
||||
const sinceLastModal = now - parseInt(modalTime);
|
||||
// show the modal if it was shown more than 5 days ago
|
||||
if (sinceLastModal > interval) {
|
||||
localStorage.setItem('preReleaseProModal', Date.now().toString());
|
||||
localStorage.setItem('preReleaseProNotification', Date.now().toString());
|
||||
showPrereleaseModal.set(true);
|
||||
}
|
||||
//if the modal has been shown more than 24 ago and the notification has not been shown for 24 hours
|
||||
else if (
|
||||
sinceLastModal > 24 * 60 * 60 * 1000 &&
|
||||
now - (notificationTime ? parseInt(notificationTime) : 0) > 24 * 60 * 60 * 1000
|
||||
) {
|
||||
localStorage.setItem('preReleaseProNotification', Date.now().toString());
|
||||
addNotification({
|
||||
type: 'warning',
|
||||
timeout: 10000,
|
||||
message:
|
||||
'Appwrite Pro is coming soon, which means you will be limited to one free organization per account. To avoid service disruptions in your projects, consider upgrading to Pro.',
|
||||
buttons: [
|
||||
{
|
||||
name: 'Learn more',
|
||||
method: () => {
|
||||
window.open('https://appwrite.io/pricing', '_blank');
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$: if (!$log.show) {
|
||||
$log.data = null;
|
||||
$log.func = null;
|
||||
@@ -281,3 +328,7 @@
|
||||
{#if $log.show}
|
||||
<Logs />
|
||||
{/if}
|
||||
|
||||
{#if isCloud && $showPrereleaseModal && !$page.url.pathname.includes('/console/onboarding')}
|
||||
<PreReleaseModal bind:show={$showPrereleaseModal} />
|
||||
{/if}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { page } from '$app/stores';
|
||||
import type { Models } from '@appwrite.io/console';
|
||||
import { derived } from 'svelte/store';
|
||||
import { derived, writable } from 'svelte/store';
|
||||
|
||||
export const version = derived(page, ($page) => $page.data.version as string | null);
|
||||
export const consoleVariables = derived(
|
||||
page,
|
||||
($page) => $page.data.consoleVariables as Models.ConsoleVariables
|
||||
);
|
||||
|
||||
export const showPrereleaseModal = writable(false);
|
||||
|
||||
Reference in New Issue
Block a user