mirror of
https://github.com/LukeHagar/website.git
synced 2025-12-06 04:22:07 +00:00
696 lines
23 KiB
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>
|