Files
website/src/lib/animations/Products/Products.svelte

696 lines
23 KiB
Svelte

<script lang="ts" context="module">
import AuthShot from './(assets)/auth-shot.png?enhanced';
import DatabasesShot from './(assets)/db-shot.png?enhanced';
import FunctionsShot from './(assets)/fn-shot.png?enhanced';
import StorageShot from './(assets)/storage-shot.png?enhanced';
import RealtimeShot from './(assets)/realtime-shot.png?enhanced';
import MessagingShot from './(assets)/messaging-shot.png?enhanced';
import type { EnhancedImgAttributes } from '@sveltejs/enhanced-img';
export const elId = writable(0);
export function getElSelector(el: string) {
return `#${el}-${get(elId)}`;
}
/* Products infos */
const products = [
'auth',
'databases',
'storage',
'functions',
'messaging',
'realtime',
'post'
] as const;
type Product = (typeof products)[number];
type ProductInfo = {
icon: {
active: string;
inactive: string;
};
title: string;
subtitle: string;
description: string;
features: string[];
shot?: EnhancedImgAttributes['src'];
};
export const infos: { [K in Product]?: ProductInfo } = {
auth: {
icon: {
active: './images/icons/illustrated/dark/auth.png',
inactive: './images/icons/illustrated/dark/auth-transparent.png'
},
title: 'Auth',
subtitle: 'Secure login for all users',
description:
'Authenticate users securely with multiple login methods like Email/Password, SMS, OAuth, Anonymous, Magic URLs and more.',
features: [
'30+ login methods',
'Support for teams, roles and user labels',
'Rate-limits and advanced user protection',
'Custom SMTP and email templates'
],
shot: AuthShot
},
databases: {
icon: {
active: './images/icons/illustrated/dark/databases.png',
inactive: './images/icons/illustrated/dark/databases-transparent.png'
},
title: 'Databases',
subtitle: 'Store, query and manage data',
description: 'Scalable and robust database backed by your favorite technologies.',
features: [
'Never paused',
'Fast in-memory caching',
'Advanced permission models',
'Custom data validation',
'Relationships support'
],
shot: DatabasesShot
},
functions: {
icon: {
active: './images/icons/illustrated/dark/functions.png',
inactive: './images/icons/illustrated/dark/functions-transparent.png'
},
title: 'Functions',
subtitle: 'Customize and extend your backend',
description: 'Deploy and scale serverless functions in secure, isolated runtimes.',
features: [
'Automatic deployment from GitHub',
'Trigger using GitHub, CLI, Event Listeners or HTTP requests',
'Support for 30+ runtimes in 13 languages',
'Custom domain support'
],
shot: FunctionsShot
},
messaging: {
icon: {
active: './images/icons/illustrated/dark/messaging.png',
inactive: './images/icons/illustrated/dark/messaging-transparent.png'
},
title: 'Messaging',
subtitle: 'Communicate with your users',
description:
'Set up a full-functioning messaging service for your application that covers multiple channels under one unified platform.',
features: [
'Draft and preview your messages before delivery',
'Segment your users for specific targeting',
'Send push notifications, emails, and SMS',
'Supports real-time and location-based messaging'
],
shot: MessagingShot
},
storage: {
icon: {
active: './images/icons/illustrated/dark/storage.png',
inactive: './images/icons/illustrated/dark/storage-transparent.png'
},
title: 'Storage',
subtitle: 'Upload and manage files',
description:
'Securely store files with advanced compression, encryption and image transformations.',
features: [
'File encryption at rest and transit',
'Built-in image transformation capabilities',
'Advanced compression with WebP/Brotli support'
],
shot: StorageShot
},
realtime: {
icon: {
active: './images/icons/illustrated/dark/realtime.png',
inactive: './images/icons/illustrated/dark/realtime-transparent.png'
},
title: 'Realtime',
subtitle: 'Realtime events for every service',
description: 'Subscribe and react to any Appwrite event using the Realtime API.',
features: [
'Unlimited subscriptions',
'Built-in permission management',
'Support for DBs, Auth, Storage & Functions'
],
shot: RealtimeShot
}
};
</script>
<script lang="ts">
import { toScale, type Scale } from '$lib/utils/toScale';
import { fly, slide } from 'svelte/transition';
import { scroll } from '..';
import ScrollIndicator from '../scroll-indicator.svelte';
import { Auth, authController } from './auth';
import AnimatedBox from './AnimatedBox.svelte';
import { tick } from 'svelte';
import CodeWindow from '../CodeWindow/CodeWindow.svelte';
import { Databases, databasesController } from './databases';
import { objectKeys } from '$lib/utils/object';
import { get, writable } from 'svelte/store';
import { Storage, storageController } from './storage';
import { Functions, functionsController } from './functions';
import { Realtime, realtimeController } from './realtime';
import { Messaging, messagingController } from './messaging';
import { postController } from './post';
import Post from './post/post.svelte';
import { anyify } from '$lib/utils/anyify';
/* Basic Animation setup */
let scrollInfo = {
percentage: 0,
traversed: 0,
remaning: Infinity
};
const animScale: Scale = [0.075, 1];
const productsScales = products.map((_, idx) => {
const diff = animScale[1] - animScale[0];
const step = diff / products.length;
return [animScale[0] + step * idx, animScale[0] + step * (idx + 1)] as Scale;
});
$: active = (function getActiveInfo() {
let activeIdx = productsScales.findIndex(([min, max]) => {
return scrollInfo.percentage >= min && scrollInfo.percentage < max;
});
const product = products[activeIdx] as Product;
const scale = productsScales[activeIdx] as Scale;
const percent = scale ? toScale(scrollInfo.percentage, scale, [0, 1]) : 0;
return {
product,
scale,
percent
};
})();
let lastActive: Product | undefined = undefined;
const controllers = {
auth: authController,
databases: databasesController,
storage: storageController,
functions: functionsController,
messaging: messagingController,
realtime: realtimeController,
post: postController
};
$: (async () => {
const fixedLast = lastActive;
lastActive = active.product;
objectKeys(controllers).forEach(async (key) => {
const controller = controllers[key];
if (active.product === key && fixedLast !== key) {
if (!(fixedLast === 'realtime' && key === 'post')) {
elId.update((n) => n + 1);
}
await tick();
controller.execute();
}
});
})();
</script>
<div
id="products"
use:scroll
on:web-scroll={({ detail }) => {
scrollInfo = detail;
}}
>
<div class="sticky-wrapper">
<!-- <div class="debug">
<pre>{scrollInfo.percentage}</pre>
</div> -->
{#if scrollInfo.percentage < 0.075}
<div
class="main-text"
out:fly={{ duration: 250, y: -300 }}
in:fly={{ duration: 250, delay: 250, y: -300 }}
>
{#if scrollInfo.percentage > -0.1}
<span
class="web-badges text-micro !text-white uppercase"
transition:slide={{ axis: 'x' }}>Products_</span
>
<h2
class="text-display font-aeonik-pro text-primary"
transition:fly={{ y: 16, delay: 250 }}
>
Your backend, minus the hassle
</h2>
<p
class="text-description mx-auto max-w-[700px]"
transition:fly={{
y: 16,
delay: 400
}}
>
Build secure and scalable applications with less code. Add authentication,
databases, storage, and more using Appwrite's development platform.
</p>
{/if}
</div>
{:else}
<div
class="products"
out:fly={{ duration: 250, y: 300 }}
in:fly={{ duration: 500, delay: 250, y: 300 }}
data-active={scrollInfo.percentage > 0.075 ? '' : undefined}
>
<div class="text" id="pd-{$elId}">
<ScrollIndicator
percentage={toScale(scrollInfo.percentage, animScale, [0, 1])}
/>
<ul class="descriptions">
{#each products as product}
{@const copy = infos[product]}
{@const isActive = active.product === product}
{#if copy}
<li data-active={isActive ? '' : undefined}>
<h3>
<img
src={isActive ? copy.icon.active : copy.icon.inactive}
alt=""
width="32"
height="32"
/>
<span class="text-label">{copy.title}</span>
</h3>
{#if isActive}
<div transition:slide>
<h4 class="text-title font-aeonik-pro">
{copy.subtitle}
</h4>
<p>
{copy.description}
</p>
<ul class="features">
{#each copy.features as feature}
<li>{feature}</li>
{/each}
</ul>
</div>
{/if}
</li>
{/if}
{/each}
</ul>
</div>
<div class="animated">
<div class="box-wrapper" id="box-{$elId}">
<AnimatedBox>
<div class="top" slot="top">
<p class="title">
{#if active.product === 'auth'}
Users
{:else if active.product === 'databases'}
Tasks
{:else if active.product === 'storage'}
Files
{:else if active.product === 'functions'}
<!-- oblivion -->
{:else if active.product === 'messaging'}
Messages
{:else if active.product === 'realtime'}
Realtime
{/if}
</p>
</div>
{#if active.product === 'auth'}
<Auth.Box />
{:else if active.product === 'messaging'}
<Messaging.Box />
{:else if active.product === 'databases'}
<Databases.Box />
{:else if active.product === 'storage'}
<Storage.Box />
{/if}
</AnimatedBox>
</div>
<div class="code-window" id="code-{$elId}">
<CodeWindow>
{#if active.product === 'auth'}
<Auth.Code />
{:else if active.product === 'databases'}
<Databases.Code />
{:else if active.product === 'storage'}
<Storage.Code />
{:else if active.product === 'functions'}
<Functions.Code />
{:else if active.product === 'messaging'}
<Messaging.Code />
{/if}
</CodeWindow>
</div>
{#if active.product === 'auth'}
<div class="controls" id="controls-{$elId}">
<Auth.Controls />
</div>
{/if}
</div>
<div class="phone" id="phone-{$elId}">
<div class="inner">
{#if active.product === 'auth'}
<Auth.Phone />
{:else if active.product === 'databases'}
<Databases.Phone />
{:else if active.product === 'storage'}
<Storage.Phone />
{:else if active.product === 'messaging'}
<Messaging.Phone />
{:else if active.product === 'functions'}
<Functions.Phone />
{:else if !['auth', 'databases', 'storage', 'messaging', 'functions'].includes(active.product)}
<Realtime.Phone />
{/if}
</div>
</div>
{#if !['auth', 'databases', 'storage', 'functions', 'messaging', 'realtime'].includes(anyify(active.product))}
<Post />
{/if}
</div>
{/if}
</div>
</div>
<style lang="scss">
@use '$scss/abstract/mixins/border-gradient' as gradients;
#products {
min-height: 500vh;
height: fit-content;
position: relative;
--debug-bg: transparent;
display: none;
}
@media (min-width: 1400px) {
#products {
display: block;
}
}
.sticky-wrapper {
display: flex;
flex-direction: column;
align-items: center;
justify-content: space-around;
gap: 1rem;
position: sticky;
top: 0;
min-height: 50rem;
overflow: hidden;
padding-inline: 1.25rem;
width: 100%;
height: 60vh;
> .main-text {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
display: flex;
flex-direction: column;
align-items: center;
min-height: 5rem;
text-align: center;
h2 {
white-space: nowrap;
margin-top: 1.5rem;
}
p {
margin-top: 1.25rem;
max-width: 48.875rem;
}
@media (min-width: 1024px) {
h2 {
max-width: 61.375rem;
}
}
}
}
.products:not([data-active]) {
opacity: 0;
}
.products[data-active] {
opacity: 1;
}
.products {
background: var(--debug-bg, hsla(250, 50%, 50%, 0.25));
display: flex;
justify-content: space-between;
width: 100%;
max-width: 77.75rem;
position: relative;
transition: 200ms ease;
.text {
background: var(--debug-bg, hsla(200, 50%, 50%, 0.25));
display: flex;
flex-grow: 1;
max-width: 25rem;
position: relative;
.descriptions {
margin-inline-start: 2rem;
text-align: left;
position: absolute;
width: 100%;
> li {
&:not(:first-child) {
padding-block-start: 1.5rem;
}
transition: 100ms ease;
&[data-active] {
h3 {
color: hsl(var(--web-color-primary));
margin-block-end: 0.75rem;
}
}
}
h3 {
display: flex;
align-items: center;
gap: 0.75rem;
}
h4 {
color: hsl(var(--web-color-primary));
}
p {
margin-block-start: 1rem;
}
.features {
display: flex;
flex-direction: column;
gap: 0.75rem;
margin-block-start: 2rem;
li {
--marker-size: 1.25rem;
--margin-left: calc(var(--marker-size) + 0.75rem);
position: relative;
margin-inline-start: var(--margin-left);
&::before {
content: '';
position: absolute;
left: calc(var(--margin-left) * -1);
top: 50%;
width: var(--marker-size);
height: var(--marker-size);
transform: translateY(-50%);
background: url('/images/icons/colored/check.svg') no-repeat;
}
}
}
}
}
}
.animated {
background: var(--debug-bg, hsl(100, 50%, 50%, 0.25));
width: min(42rem, 50vw);
height: min(38.75rem, 90vh);
position: relative;
}
.phone {
@include gradients.border-gradient;
--m-border-size: 1px;
--m-border-radius: 2.5rem;
--m-border-gradient-after: linear-gradient(
180deg,
rgba(255, 255, 255, 0.12) 0%,
rgba(255, 255, 255, 0) 125.11%
);
background: rgba(255, 255, 255, 0.08);
//backdrop-filter: blur(8px);
padding: 0.5rem;
width: 275px;
height: 550px;
flex-shrink: 0;
position: absolute;
top: 0;
left: calc(50%);
z-index: 10;
opacity: 1;
.inner {
background-color: white;
border-radius: 2rem;
width: 100%;
height: 100%;
position: relative;
&::after {
content: '';
position: absolute;
left: 50%;
transform: translateX(-50%);
bottom: 0.5rem;
border-radius: 100rem;
background: var(--label-color-light-primary, #000);
width: 6.25rem;
height: 0.25rem;
}
}
}
.box-wrapper {
position: absolute;
top: 0;
z-index: 0;
opacity: 0;
width: 25rem;
transform: translateX(16.5rem) translateY(2rem);
}
.box-wrapper :global(.pseudo-table) {
:global(.header),
:global(.row) {
display: grid;
grid-template-columns: 10rem 1fr;
justify-content: space-between;
align-items: center;
gap: 1.5rem 3rem;
}
:global(.header) {
border-bottom: 1px solid hsl(var(--web-color-greyscale-700));
color: var(--greyscale-400, #adadb1);
text-transform: uppercase;
padding: 1rem;
}
:global(.row) {
padding-block: 0.5rem;
padding-inline: 1rem;
color: var(--greyscale-400, #adadb1);
font-family: Inter;
font-size: 0.875rem;
font-style: normal;
font-weight: 400;
line-height: 1.25rem; /* 142.857% */
}
:global(.avatar) {
background-color: hsl(var(--web-color-greyscale-700));
border-color: hsl(var(--web-color-greyscale-700));
}
:global(.truncated) {
display: block;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
}
}
.code-window {
position: absolute;
z-index: 20;
top: 0;
left: 0;
opacity: 0;
}
.controls {
@include gradients.border-gradient;
--m-border-radius: 1rem;
--m-border-gradient-before: linear-gradient(
180deg,
rgba(255, 255, 255, 0.12) 0%,
rgba(255, 255, 255, 0) 125.11%
);
position: absolute;
top: 0;
left: 0;
z-index: 10;
opacity: 0;
background: rgba(255, 255, 255, 0.08);
box-shadow:
0px 0px 0px 0px rgba(0, 0, 0, 0.06),
-2px 4px 9px 0px rgba(0, 0, 0, 0.06),
-8px 15px 17px 0px rgba(0, 0, 0, 0.05),
-19px 34px 23px 0px rgba(0, 0, 0, 0.03),
-33px 60px 27px 0px rgba(0, 0, 0, 0.01),
-52px 94px 30px 0px rgba(0, 0, 0, 0);
backdrop-filter: blur(20px);
}
</style>