Merge pull request #637 from appwrite/chore-prerelease-modal

feat: pre release modal
This commit is contained in:
Christy Jacob
2023-12-04 17:53:33 +04:00
committed by GitHub
9 changed files with 363 additions and 132 deletions
+2
View File
@@ -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
View File
@@ -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>
+67
View File
@@ -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>
+82
View File
@@ -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

+52 -1
View File
@@ -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}
+3 -1
View File
@@ -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);