Merge branch 'main' into new-website-redesign-blog

This commit is contained in:
Darshan
2025-06-11 13:59:18 +05:30
committed by GitHub
1000 changed files with 27046 additions and 32868 deletions

View File

@@ -11,4 +11,16 @@ node_modules
pnpm-lock.yaml
package-lock.json
yarn.lock
*.scss
*.scss
# Assets
*.jpg
*.png
*.webp
*.avif
# Media
*.mp3
*.wav
*.mp4
*.mpv

View File

@@ -1,12 +1,10 @@
# Content Guidelines
## Table of Contents
### Basics
## Basics
Here are some essential markdoc syntax elements with code examples:
#### Paragraphs and Line Breaks
### Paragraphs and Line Breaks
To create a new paragraph, simply leave a blank line between lines of text.
@@ -16,7 +14,7 @@ This is the first paragraph.
This is the second paragraph.
```
#### Headers
### Headers
Headers are used to create section titles. Use hashtags (#) for headers, with more hashtags for lower-level headers.
@@ -34,7 +32,7 @@ Headers can also be given ID's so they can be linked to and are present in the T
# Header with ID {% #header-with-id %}
```
#### Lists
### Lists
Create ordered (numbered) and unordered (bulleted) lists using 1., \*, or -.
@@ -54,7 +52,7 @@ Create ordered (numbered) and unordered (bulleted) lists using 1., \*, or -.
- Cherry
```
#### Links
### Links
Create hyperlinks to other web pages or sections within your documentation.
@@ -63,7 +61,7 @@ Create hyperlinks to other web pages or sections within your documentation.
[Link to Section](#section-name)
```
#### Images
### Images
Embed images using the `![alt text](image URL)` syntax.
@@ -82,7 +80,7 @@ In most cases, we need images in both light and dark mode such as:
{% /only_light %}
```
#### Code Blocks
### Code Blocks
Format code blocks using triple backticks (```).
@@ -95,7 +93,7 @@ def hello_world():
Remember to use a specific language label if the code is using an Appwrite SDK. Find the [list of available labels here](https://github.com/appwrite/website/blob/41bb6c71a8647016c88393003d3cf6c4edba1f76/src/lib/utils/references.ts#L26).
#### Inline Code
### Inline Code
Highlight inline code with backticks (`) around the code snippet.
@@ -103,7 +101,7 @@ Highlight inline code with backticks (`) around the code snippet.
Use the `print()` function to display text.
```
#### Emphasis and Strong Text
### Emphasis and Strong Text
Use asterisks (\*) or underscores (\_) for emphasis and double asterisks or underscores for strong text.
@@ -114,7 +112,7 @@ _Italic Text_ or _Italic Text_
**Bold Text** or **Bold Text**
```
#### Tables
### Tables
Tables allow you to display structured data in your documentation. Use pipes (|) to separate columns and hyphens (-) to define the table header.
@@ -146,7 +144,7 @@ Alternatively, use markdoc tables.
{% /table %}
```
#### Block Quotes
### Block Quotes
Block quotes are used to emphasize or highlight text. To create a block quote, use the > symbol at the beginning of the quoted text.
@@ -157,9 +155,9 @@ Block quotes are used to emphasize or highlight text. To create a block quote, u
These are the fundamental Markdown syntax elements you'll need to create well-structured and formatted documentation.
### Components
## Components
#### Tabs
### Tabs
```md
{% tabs %}
@@ -173,7 +171,7 @@ Lorem ipsum dolor sit amet consectetur.
{% /tabs %}
```
#### Multicode Examples
### Multicode Examples
<pre>
{% multicode %}
@@ -191,7 +189,9 @@ print('test');
{% /multicode %}
</pre>
#### Sections
Remember to use a specific language label if the code is using an Appwrite SDK. Find the [list of available labels here](https://github.com/appwrite/website/blob/41bb6c71a8647016c88393003d3cf6c4edba1f76/src/lib/utils/references.ts#L26).
### Sections
Use sections when there is a clear step-by-step format to a page. This is used mainly in journey pages and tutorials.
@@ -209,7 +209,7 @@ Lorem ipsum dolor sit amet consectetur.
{% /section %}
```
#### Info
### Info
```
{% info title="Public Service Announcement" %}
@@ -217,7 +217,7 @@ Lorem ipsum dolor sit amet consectetur.
{% /info %}
```
#### Icon
### Icon
Available sizes are `s`, `m`, `l` and `xl`. Default: `s`.
@@ -228,7 +228,7 @@ Available sizes are `s`, `m`, `l` and `xl`. Default: `s`.
{% icon icon="github" size="xl" /%}
```
#### Icon Image
### Icon Image
Available sizes are `s`, `m`, `l` and `xl`. Default: `s`.
@@ -239,7 +239,7 @@ Available sizes are `s`, `m`, `l` and `xl`. Default: `s`.
{% icon_image src="/icon.png" alt="Icon" size="xl" /%}
```
#### Only Light/Dark Theme
### Only Light/Dark Theme
```
{% only_dark %}
@@ -250,7 +250,7 @@ Available sizes are `s`, `m`, `l` and `xl`. Default: `s`.
{% /only_light %}
```
#### Cards
### Cards
We use cards when we reference a list of links for navigation
@@ -276,7 +276,7 @@ Get started with Appwrite and SvelteKit
{% /cards %}
```
#### Cards with icons
### Cards with icons
We use cards when we reference a list of links for navigation, this variation has icons for extra hints visually.
@@ -294,7 +294,7 @@ Configure FCM for push notification to Android and Apple devices.
{% /cards %}
```
#### Accordions
### Accordions
Use accordions to reduce page size and collapse information that's not important when a reader is scrolling the page.

View File

@@ -41,4 +41,4 @@ Join our growing community around the world! See our official [Blog](https://app
## License
Appwrite website, docs and blog © 2024 by Appwrite is licensed under [CC BY-NC-ND 4.0](https://creativecommons.org/licenses/by-nc-nd/4.0/).
Appwrite website, docs and blog © 2025 by Appwrite is licensed under [CC BY-NC-ND 4.0](https://creativecommons.org/licenses/by-nc-nd/4.0/).

View File

@@ -332,6 +332,13 @@ or a page needs to be sufficiently different from existing pages, follow exisitn
If you are proposing a new type of page, discuss an outline in your PR and ask for the Appwrite team's review.
### Placeholders
Whenever there's a need for a placeholder such as for an ID, use angle brackets (<>) over square brackets ([]) because square brackets can be confused for an array.
-`client.setEndpoint("https://<REGION>.cloud.appwrite.io")`
-`client.setEndpoint("https://[REGION].cloud.appwrite.io")`
## Code snippets
For quick starts and tutorials, a developer must be able to follow code examples from beginning to end

View File

@@ -24,9 +24,8 @@
"optimize": "node ./scripts/optimize-assets.js",
"optimize:all": "node ./scripts/optimize-all.js"
},
"packageManager": "pnpm@10.8.1",
"packageManager": "pnpm@10.11.1",
"dependencies": {
"@number-flow/svelte": "^0.3.3",
"h3": "^1.14.0",
"posthog-js": "^1.210.2",
"sharp": "^0.33.5"
@@ -35,24 +34,29 @@
"@appwrite.io/console": "^0.6.4",
"@appwrite.io/pink": "~0.26.0",
"@appwrite.io/pink-icons": "~0.26.0",
"@appwrite.io/repo": "github:appwrite/appwrite#1.6.x",
"@appwrite.io/repo": "github:appwrite/appwrite#main",
"@eslint/compat": "^1.2.7",
"@eslint/js": "^9.21.0",
"@fingerprintjs/fingerprintjs": "^4.5.1",
"@internationalized/date": "3.5.0",
"@melt-ui/pp": "^0.3.2",
"@melt-ui/svelte": "^0.86.5",
"@number-flow/svelte": "^0.3.7",
"@playwright/test": "^1.50.0",
"@sveltejs/adapter-node": "^5.2.12",
"@sveltejs/enhanced-img": "^0.4.4",
"@sveltejs/kit": "^2.20.2",
"@sveltejs/vite-plugin-svelte": "^5.0.3",
"@tailwindcss/postcss": "^4.1.2",
"@tailwindcss/postcss": "^4.1.4",
"@turf/boolean-point-in-polygon": "^7.2.0",
"@types/compression": "^1.7.5",
"@types/glob": "^8.1.0",
"@types/jsdom": "^21.1.7",
"@types/markdown-it": "^13.0.9",
"@types/morgan": "^1.9.9",
"@types/proj4": "^2.5.6",
"analytics": "^0.8.16",
"appwrite": "^17.0.1",
"bits-ui": "^1.3.19",
"clsx": "^2.1.1",
"cva": "npm:class-variance-authority@^0.7.1",
@@ -68,10 +72,13 @@
"fuse.js": "^7.0.0",
"globals": "^15.14.0",
"highlight.js": "^11.11.1",
"linkedom": "^0.18.9",
"markdown-it": "^14.1.0",
"meilisearch": "^0.37.0",
"melt": "^0.29.2",
"motion": "^10.18.0",
"motion": "^12.7.4",
"node-appwrite": "^16.0.0",
"node-fetch": "^3.3.2",
"node-html-parser": "^6.1.13",
"openapi-types": "^12.1.3",
"oslllo-svg-fixer": "^3.0.0",
@@ -82,6 +89,7 @@
"prettier": "^3.4.2",
"prettier-plugin-svelte": "^3.3.3",
"prettier-plugin-tailwindcss": "^0.6.11",
"proj4": "^2.17.0",
"remeda": "^2.20.0",
"reodotdev": "^1.0.0",
"sass": "^1.83.4",
@@ -89,17 +97,20 @@
"svelte-check": "^4.0.0",
"svelte-markdoc-preprocess": "3.0.0",
"svelte-markdown": "^0.4.1",
"svg-dotted-map": "^2.0.1",
"svgtofont": "^4.2.3",
"tailwind-merge": "^3.0.2",
"tailwindcss": "^4.1.2",
"tailwindcss": "^4.1.4",
"tslib": "^2.8.1",
"typescript": "^5.8.2",
"typescript-eslint": "^8.21.0",
"vaul-svelte": "1.0.0-next.7",
"vite": "^6.2.4",
"vite-plugin-dynamic-import": "^1.6.0",
"vite-plugin-image-optimizer": "^1.1.8",
"vite-plugin-manifest-sri": "^0.2.0",
"vitest": "^3.1.1"
"vitest": "^3.1.1",
"zod": "^3.24.2"
},
"pnpm": {
"onlyBuiltDependencies": [

849
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
@import 'tailwindcss';
@import './styles/typography.css';
@variant dark (&:is(.dark *));
@custom-variant dark (&:where(.dark, .dark *));
@theme {
/* Colors */
@@ -16,6 +16,9 @@
--color-secondary: var(--color-secondary);
--color-accent: var(--color-secondary);
--color-smooth: var(--color-smooth);
--color-subtle: var(--color-subtle);
--color-tertiary: var(--color-tertiary);
--color-card: var(--color-card);
/* pink */
--color-pink-200: hsl(var(--color-pink-hue) 98% 84%);
@@ -35,33 +38,33 @@
/* mint */
--color-mint-200: hsl(var(--color-mint-hue) 56% 88%);
--color-mint-500: hsl(calc(var(--color-mint-hue) + 1), 54%, 69%);
--color-mint-700: hsl(calc(var(--color-mint-hue) + 2), 24%, 41%);
--color-mint-500: hsl(calc(var(--color-mint-hue) + 1) 54% 69%);
--color-mint-700: hsl(calc(var(--color-mint-hue) + 2) 24% 41%);
/* purple */
--color-purple-200: hsl(var(--color-purple-hue) 100% 88%);
--color-purple-500: hsl(calc(var(--color-purple-hue) - 1), 99%, 70%);
--color-purple-700: hsl(calc(var(--color-purple-hue) - 1), 42%, 42%);
--color-purple-500: hsl(calc(var(--color-purple-hue) - 1) 99% 70%);
--color-purple-700: hsl(calc(var(--color-purple-hue) - 1) 42% 42%);
/* yellow */
--color-yellow-200: hsl(var(--color-yellow-hue) 100% 88%);
--color-yellow-500: hsl(var(--color-yellow-hue) 99% 70%);
--color-yellow-700: hsl(calc(var(--color-yellow-hue) + 1), 42%, 42%);
--color-yellow-700: hsl(calc(var(--color-yellow-hue) + 1) 42% 42%);
/* blue */
--color-blue-200: hsl(var(--color-blue-hue) 100% 88%);
--color-blue-500: hsl(calc(var(--color-blue-hue) - 1), 99%, 70%);
--color-blue-700: hsl(calc(var(--color-blue-hue) - 1), 42%, 42%);
--color-blue-500: hsl(calc(var(--color-blue-hue) - 1) 99% 70%);
--color-blue-700: hsl(calc(var(--color-blue-hue) - 1) 42% 42%);
/* green */
--color-green-700: #0a714f;
/* secondary */
--color-secondary-100: hsl(var(--color-secondary-hue) 99% 66%);
--color-accent-200: hsl(var(--color-secondary-hue), 78%, 60%, 0.32);
--color-accent-200: hsl(var(--color-secondary-hue) 78% 60% / 0.32);
/* greyscale */
--color-offset: hsl(var(--color-greyscale-hue) 2%, 11%, 0.94);
--color-offset: hsl(var(--color-greyscale-hue) 2% 11% / 0.94);
--color-greyscale-25: hsl(var(--color-greyscale-hue) 11% 98%);
--color-greyscale-50: hsl(var(--color-greyscale-hue) 11% 94%);
--color-greyscale-100: hsl(var(--color-greyscale-hue) 6% 90%);
@@ -83,23 +86,41 @@
/* Animations */
--animate-scale-in: scale-in 200ms ease-out forwards;
--animate-scale-out: scale-out 200ms ease-out forwards;
--animate-caret-blink: caret-blink 1s ease-in-out infinite;
--animate-enter:
fade-in 0.75s ease-in-out both, blur 0.75s ease-in-out both, up 0.75s ease-in-out both;
--animate-scroll: scroll 60s linear infinite;
--animate-scroll-x: scroll-x var(--speed, 30s) linear infinite var(--direction, forwards);
--animate-scroll-y: scroll-y 30s linear infinite forwards;
--animate-fade-in: fade-in 0.5s ease-in-out both;
--animate-marquee: marquee var(--speed, 30s) linear infinite var(--direction, forwards);
--animate-fade-out: fade-out 0.5s ease-in-out both;
--animate-lighting: lighting 1.25s ease-out forwards;
--animate-menu-in: menu-in 0.25s ease-out forwards;
--animate-menu-out: menu-out 0.25s ease-out forwards;
--animate-enter-from-left: enter-from-left 0.2s ease;
--animate-enter-from-right: enter-from-right 0.2s ease;
--animate-exit-to-left: exit-to-left 0.2s ease;
--animate-exit-to-right: exit-to-right 0.2s ease;
--animate-wipe-in: wipe-in 2s ease-in-out;
/* Keyframes */
@keyframes scale-in {
0% {
transform: scale(0);
transform: rotateX(-10deg) scale(0.9);
}
100% {
transform: scale(1);
transform: rotateX(0) scale(1);
}
}
@keyframes scale-out {
0% {
transform: rotateX(0) scale(1);
}
100% {
transform: rotateX(-10deg) scale(0.9);
}
}
@@ -126,7 +147,7 @@
@keyframes up {
0% {
transform: translateY(8px);
transform: translateY(36px);
}
100% {
transform: translateY(0px);
@@ -142,7 +163,16 @@
}
}
@keyframes scroll {
@keyframes fade-out {
0% {
opacity: 1;
}
100% {
opacity: 0;
}
}
@keyframes scroll-deprecate {
0% {
transform: translateX(0);
}
@@ -151,9 +181,15 @@
}
}
@keyframes marquee {
@keyframes scroll-x {
to {
transform: translateX(-50%);
transform: translateX(-100%);
}
}
@keyframes scroll-y {
to {
transform: translateY(-100%);
}
}
@@ -196,6 +232,59 @@
}
}
@keyframes enter-from-right {
from {
opacity: 0;
transform: translateX(200px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
@keyframes enter-from-left {
from {
opacity: 0;
transform: translateX(-200px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
@keyframes exit-to-right {
from {
opacity: 1;
transform: translateX(0);
}
to {
opacity: 0;
transform: translateX(200px);
}
}
@keyframes exit-to-left {
from {
opacity: 1;
transform: translateX(0);
}
to {
opacity: 0;
transform: translateX(-200px);
}
}
@keyframes wipe-in {
0% {
clip-path: polygon(0% 0%, 0% 0%, 0% 100%, 0% 100%);
}
100% {
clip-path: polygon(0% 0%, 100% 0%, 100% 100%, 0% 100%);
}
}
/* Fonts */
--font-sans: 'Inter', arial, sans-serif;
--font-mono: 'Fira Code', monospace;
@@ -233,9 +322,15 @@
--text-title: clamp(2rem, 5vw, 2.5rem);
--text-title--line-height: clamp(2.125rem, 5.5vw, 2.75rem);
--text-title--letter-spacing: var(--tracking-squeezed);
--text-title-lg: clamp(2.85rem, 5vw, 3rem);
--text-title-lg--line-height: clamp(2.75rem, 5.5vw, 3.5rem);
--text-title-lg--letter-spacing: var(--tracking-squeezed);
--text-display: clamp(3rem, 7vw, 4rem);
--text-display--line-height: clamp(3.125rem, 7.5vw, 4.25rem);
--text-display--letter-spacing: var(--tracking-compressed);
--text-hero: clamp(3.2rem, 7vw, 4.5rem);
--text-hero--line-height: clamp(3.125rem, 7.5vw, 4.25rem);
--text-hero--letter-spacing: var(--tracking-compressed);
--text-headline: clamp(3.5rem, 8vw, 5.5rem);
--text-headline--line-height: clamp(3.5rem, 8.5vw, 5.75rem);
--text-headline--letter-spacing: var(--tracking-compressed);
@@ -251,19 +346,19 @@
}
@utility container {
max-width: 75rem;
margin-inline: auto;
padding-inline: 1.25rem;
max-width: 75rem;
}
@utility border-gradient {
--border-gradient-before: linear-gradient(
180deg,
var(--to, 180deg),
rgba(255, 255, 255, 0.16) 0%,
rgba(255, 255, 255, 0) 100%
);
--border-gradient-after: linear-gradient(
180deg,
var(--to, 180deg),
rgba(255, 255, 255, 0.12) 0%,
rgba(255, 255, 255, 0) 125.11%
);
@@ -292,16 +387,6 @@
}
}
@utility mask {
mask-image: linear-gradient(
to var(--mask-direction, top),
transparent,
black var(--mask-height, 32px),
black calc(100% - var(--mask-height, 32px)),
black
);
}
:root,
.light {
/* pink polyfills */
@@ -317,20 +402,28 @@
--color-yellow-hue: 42;
--color-blue-hue: 217;
--color-greyscale-hue: 240;
--color-primary-bg: var(--color-greyscale-25);
/* base */
--color-primary: var(--color-greyscale-900);
--color-secondary: var(--color-greyscale-700);
--color-accent: var(--color-pink-600);
--color-accent: var(--color-pink-500);
--carousel-gradient: transparent;
--color-badge-bg: var(--color-badge-bg-light);
--color-badge-border: var(--color-badge-border-light);
--color-smooth: hsl(var(--color-greyscale-hue) 6%, 10%, 0.04);
--color-smooth: hsl(var(--color-greyscale-hue) 6% 10% / 0.04);
--color-card: var(--color-greyscale-850);
--color-tertiary: hsl(var(--color-greyscale-600));
--color-offset: hsl(var(--color-greyscale-hue) 2% 11% / 0.94);
--color-subtle: var(--color-greyscale-850);
}
.dark {
--color-primary: var(--color-greyscale-100);
--color-secondary: var(--color-greyscale-300);
--color-badge-bg: var(--color-badge-bg-dark);
--color-badge-border: var(--color-badge-border-dark);
--color-smooth: hsl(0 0%, 100%, 0.06);
--carousel-gradient: 23, 23, 26;
--color-primary-bg: var(--color-greyscale-900);
--color-smooth: hsl(0 0% 100% / 0.06);
--color-tertiary: hsl(var(--color-greyscale-600));
--color-offset: hsl(0 0% 100% / 0.1);
}

16
src/app.d.ts vendored
View File

@@ -1,4 +1,7 @@
// See https://kit.svelte.dev/docs/types#app
import type { Account } from 'node-appwrite';
// for information about these interfaces
declare global {
namespace App {
@@ -8,6 +11,19 @@ declare global {
changelogEntries: number;
}
// interface Platform {}
interface Locals {
initUser: {
github: {
login: string;
name: string;
email: string;
avatar_url: string | undefined;
} | null;
appwrite: AppwriteUser | null;
};
account: Models.User<Models.Preferences>;
nonce: string;
}
}
}

View File

@@ -1,8 +1,14 @@
import type { Handle } from '@sveltejs/kit';
import type { Handle, RequestEvent } from '@sveltejs/kit';
import redirects from './redirects.json';
import { sequence } from '@sveltejs/kit/hooks';
import { BANNER_KEY } from '$lib/constants';
import { dev } from '$app/environment';
import { type GithubUser } from '$routes/(init)/init/(utils)/auth';
import {
createInitServerClient,
createInitSessionClient
} from '$routes/(init)/init/(utils)/appwrite';
import type { AppwriteUser } from '$lib/utils/console';
const redirectMap = new Map(redirects.map(({ link, redirect }) => [link, redirect]));
@@ -22,7 +28,7 @@ const redirecter: Handle = async ({ event, resolve }) => {
const securityheaders: Handle = async ({ event, resolve }) => {
const nonce = Buffer.from(crypto.randomUUID()).toString('base64');
(event.locals as { nonce: string }).nonce = nonce;
event.locals.nonce = nonce;
const response = await resolve(event, {
transformPageChunk: ({ html }) => {
@@ -31,7 +37,7 @@ const securityheaders: Handle = async ({ event, resolve }) => {
});
// `true` if deployed via Coolify.
const isPreview = !!process.env.COOLIFY_FQDN;
const isPreview = !!process.env.COOLIFY_FQDN || process.env.NODE_ENV === 'development';
// COOLIFY_FQDN already includes `http`.
const previewDomain = isPreview ? `${process.env.COOLIFY_FQDN}` : null;
const join = (arr: string[]) => arr.join(' ');
@@ -116,11 +122,66 @@ const securityheaders: Handle = async ({ event, resolve }) => {
return response;
};
const bannerRewriter: Handle = async ({ event, resolve }) => {
const response = await resolve(event, {
transformPageChunk: ({ html }) => html.replace('%aw_banner_key%', BANNER_KEY)
});
const initSession: Handle = async ({ event, resolve }) => {
const session = await createInitSessionClient(event.cookies);
const getGithubUser = async () => {
try {
const identitiesList = await session?.account.listIdentities();
if (!identitiesList?.total) return null;
const identity = identitiesList.identities[0];
const { providerAccessToken, provider, providerEmail } = identity;
if (provider !== 'github') return null;
const res = await fetch('https://api.github.com/user', {
method: 'GET',
headers: {
Authorization: `Bearer ${providerAccessToken}`
}
})
.then((res) => {
return res.json() as Promise<GithubUser>;
})
.then((user) => ({
login: user.login,
name: user.name,
email: providerEmail,
avatar_url: user.avatar_url
}));
if (!res.login) {
await session?.account.deleteSession('current');
return null;
}
return res;
} catch (e) {
console.error(e);
return null;
}
};
const getAppwriteUser = async (): Promise<AppwriteUser | null> => {
const appwriteUser = await session?.account
.get()
.then((res) => res)
.catch((e) => null);
return appwriteUser || null;
};
const getInitUser = async () => {
const [github, appwrite] = await Promise.all([getGithubUser(), getAppwriteUser()]);
return { github, appwrite };
};
event.locals.initUser = await getInitUser();
const response = await resolve(event);
return response;
};
export const handle = sequence(redirecter, bannerRewriter, securityheaders);
export const handle = sequence(redirecter, securityheaders, initSession);

View File

@@ -0,0 +1 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M13.750 2.914 C 13.517 2.971,13.319 3.079,13.067 3.285 C 12.947 3.383,11.791 4.525,10.497 5.823 C 7.967 8.360,8.038 8.279,7.918 8.749 C 7.847 9.023,7.846 10.546,7.916 10.876 C 8.045 11.485,8.515 11.955,9.124 12.084 C 9.454 12.154,10.977 12.153,11.251 12.082 C 11.724 11.961,11.628 12.046,14.286 9.387 C 15.793 7.880,16.802 6.845,16.874 6.733 C 17.208 6.214,17.216 5.562,16.893 5.058 C 16.714 4.778,15.221 3.285,14.940 3.106 C 14.603 2.890,14.144 2.816,13.750 2.914 M14.321 4.138 C 14.468 4.249,15.656 5.429,15.803 5.611 C 15.994 5.848,15.984 5.940,15.737 6.224 C 15.628 6.348,15.400 6.589,15.228 6.758 L 14.916 7.066 13.924 6.074 L 12.933 5.083 13.342 4.681 C 13.903 4.128,13.980 4.067,14.113 4.067 C 14.175 4.067,14.267 4.099,14.321 4.138 M4.733 4.438 C 4.093 4.525,3.540 5.078,3.435 5.733 C 3.388 6.028,3.387 14.966,3.434 15.267 C 3.536 15.920,4.077 16.460,4.733 16.565 C 5.028 16.612,13.966 16.613,14.267 16.566 C 14.920 16.464,15.460 15.923,15.565 15.267 C 15.586 15.138,15.600 14.352,15.600 13.324 C 15.600 11.492,15.598 11.463,15.440 11.289 C 15.174 10.995,14.666 11.062,14.472 11.417 C 14.423 11.505,14.415 11.730,14.400 13.341 L 14.383 15.165 14.274 15.274 L 14.165 15.383 9.500 15.383 L 4.835 15.383 4.726 15.274 L 4.617 15.165 4.617 10.500 L 4.617 5.835 4.726 5.726 L 4.835 5.617 6.742 5.600 C 8.591 5.584,8.654 5.581,8.767 5.515 C 9.173 5.276,9.156 4.699,8.736 4.477 L 8.590 4.400 6.770 4.404 C 5.769 4.406,4.852 4.422,4.733 4.438 M12.593 9.390 C 11.782 10.201,11.079 10.881,11.031 10.901 C 10.894 10.958,9.338 10.931,9.247 10.870 C 9.092 10.767,9.085 10.725,9.072 9.890 C 9.066 9.453,9.070 9.056,9.082 9.006 C 9.097 8.948,9.622 8.398,10.593 7.425 L 12.083 5.933 13.075 6.925 L 14.067 7.916 12.593 9.390 " stroke="none" fill-rule="evenodd" fill="black"></path></svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@@ -1 +1 @@
<svg width="21" height="20" viewBox="0 0 21 20" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M9.925 4.028 C 9.521 4.066,8.877 4.204,8.524 4.328 C 8.123 4.469,7.392 4.849,7.035 5.101 C 6.608 5.403,5.892 6.119,5.591 6.545 C 5.321 6.929,4.984 7.584,4.835 8.018 C 4.401 9.277,4.401 10.708,4.835 11.967 C 5.340 13.435,6.497 14.723,7.918 15.400 C 10.018 16.401,12.497 16.102,14.298 14.630 C 14.650 14.342,15.135 13.821,15.408 13.438 C 15.675 13.063,16.010 12.412,16.165 11.967 C 16.599 10.715,16.599 9.270,16.165 8.018 C 15.445 5.946,13.523 4.360,11.374 4.065 C 10.964 4.009,10.306 3.992,9.925 4.028 M11.305 5.269 C 12.589 5.477,13.793 6.266,14.514 7.372 C 15.950 9.575,15.343 12.526,13.153 13.986 C 12.377 14.505,11.454 14.781,10.500 14.781 C 9.605 14.781,8.772 14.552,8.033 14.104 C 6.863 13.395,6.055 12.239,5.786 10.891 C 5.702 10.471,5.702 9.514,5.786 9.094 C 6.040 7.821,6.785 6.700,7.840 6.005 C 8.366 5.658,9.088 5.368,9.663 5.271 C 10.068 5.204,10.894 5.203,11.305 5.269 " fill="currentColor" stroke="none" fill-rule="evenodd"></path></svg>
<svg width="21" height="20" viewBox="0 0 21 20" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M3.233 2.465 C 3.093 2.527,2.942 2.724,2.917 2.882 C 2.864 3.205,2.798 3.125,4.745 5.075 C 5.735 6.067,6.545 6.888,6.545 6.899 C 6.545 6.910,6.456 7.052,6.347 7.213 C 5.989 7.746,5.709 8.426,5.576 9.091 C 5.484 9.547,5.493 10.505,5.593 10.961 C 5.817 11.991,6.256 12.801,6.983 13.528 C 7.726 14.272,8.706 14.769,9.748 14.930 C 10.245 15.006,11.107 14.982,11.550 14.880 C 12.187 14.732,12.812 14.461,13.315 14.116 L 13.592 13.925 15.380 15.709 C 16.363 16.690,17.204 17.511,17.249 17.534 C 17.294 17.557,17.398 17.576,17.480 17.576 C 17.887 17.576,18.153 17.252,18.079 16.846 C 18.058 16.728,17.850 16.507,16.255 14.910 C 15.265 13.918,14.455 13.096,14.455 13.083 C 14.455 13.070,14.551 12.917,14.667 12.742 C 15.023 12.211,15.283 11.564,15.423 10.866 C 15.515 10.405,15.507 9.482,15.407 9.024 C 15.184 8.000,14.743 7.180,14.037 6.474 C 13.330 5.767,12.455 5.296,11.463 5.089 C 11.016 4.996,9.984 4.996,9.538 5.089 C 8.884 5.226,8.252 5.491,7.699 5.861 L 7.405 6.058 5.619 4.275 C 4.636 3.294,3.798 2.475,3.756 2.454 C 3.652 2.401,3.366 2.407,3.233 2.465 M10.982 6.231 C 11.798 6.333,12.514 6.681,13.131 7.273 C 13.710 7.829,14.079 8.505,14.225 9.275 C 14.302 9.677,14.296 10.365,14.214 10.763 C 13.933 12.115,12.893 13.248,11.581 13.628 C 10.701 13.883,9.800 13.826,8.963 13.463 C 8.213 13.138,7.483 12.444,7.114 11.708 C 6.840 11.160,6.720 10.638,6.720 9.992 C 6.720 9.159,6.926 8.493,7.392 7.816 C 7.594 7.523,8.085 7.039,8.375 6.848 C 9.162 6.329,10.062 6.116,10.982 6.231 " fill="#19191C" stroke="none" fill-rule="evenodd"></path></svg>

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@@ -0,0 +1 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M4.500 4.430 L 4.500 5.864 7.558 5.875 C 10.958 5.886,10.756 5.873,11.250 6.116 C 11.552 6.264,11.697 6.387,11.864 6.637 C 12.064 6.937,12.138 7.207,12.141 7.650 C 12.144 8.103,12.095 8.350,11.942 8.643 C 11.800 8.914,11.631 9.070,11.317 9.219 C 10.830 9.450,10.919 9.444,7.542 9.458 L 4.500 9.470 4.500 10.917 L 4.500 12.364 7.425 12.375 C 10.607 12.388,10.509 12.382,10.893 12.579 C 11.334 12.804,11.577 13.269,11.664 14.050 C 11.726 14.604,11.771 15.911,11.748 16.458 L 11.724 17.000 13.434 17.000 L 15.144 17.000 15.123 16.025 C 15.075 13.762,14.977 13.042,14.624 12.350 C 14.520 12.148,14.418 12.014,14.202 11.800 C 13.868 11.468,13.649 11.331,13.213 11.181 C 12.873 11.064,12.813 11.033,12.926 11.033 C 13.030 11.033,13.525 10.872,13.717 10.775 C 14.682 10.288,15.313 9.298,15.470 8.023 C 15.521 7.616,15.487 6.775,15.404 6.363 C 15.064 4.683,14.059 3.644,12.344 3.201 C 11.653 3.022,11.706 3.025,7.975 3.010 L 4.500 2.996 4.500 4.430 M4.500 15.916 L 4.500 17.000 6.652 17.000 L 8.804 17.000 8.794 16.158 L 8.783 15.317 8.692 15.161 C 8.642 15.076,8.544 14.971,8.475 14.928 L 8.350 14.850 6.425 14.841 L 4.500 14.831 4.500 15.916 " fill="#2D2D31" stroke="none" fill-rule="evenodd"></path></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -0,0 +1 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M10.359 3.533 C 10.347 3.579,10.328 3.729,10.317 3.867 C 10.231 4.927,9.870 6.002,9.308 6.867 C 8.254 8.489,6.294 9.627,4.050 9.919 C 3.830 9.947,3.590 9.983,3.517 9.998 L 3.383 10.025 3.562 10.029 C 3.809 10.035,4.375 10.120,4.833 10.221 C 5.428 10.352,6.067 10.579,6.650 10.865 C 7.440 11.252,7.890 11.560,8.429 12.081 C 9.566 13.183,10.161 14.485,10.331 16.247 C 10.370 16.646,10.400 16.743,10.400 16.468 C 10.400 16.020,10.630 14.922,10.849 14.319 C 11.096 13.642,11.417 13.020,11.796 12.485 C 12.064 12.105,12.659 11.502,13.067 11.195 C 13.456 10.902,14.234 10.497,14.683 10.353 C 15.214 10.184,15.999 10.035,16.375 10.034 C 16.647 10.033,16.548 9.983,16.190 9.940 C 15.261 9.830,14.172 9.448,13.373 8.953 C 11.565 7.831,10.538 5.988,10.391 3.600 C 10.383 3.480,10.377 3.467,10.359 3.533 " fill="#EDEDF0" stroke="none" fill-rule="evenodd"></path></svg>

After

Width:  |  Height:  |  Size: 961 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path d="M2.891 4.559 C 2.428 4.669,1.905 5.107,1.686 5.567 C 1.497 5.964,1.474 6.254,1.488 8.020 C 1.502 9.864,1.474 9.750,2.016 10.150 C 2.563 10.554,2.880 11.063,2.970 11.684 C 3.094 12.531,2.726 13.352,1.992 13.872 C 1.800 14.008,1.658 14.143,1.596 14.252 L 1.500 14.420 1.488 15.980 C 1.480 16.919,1.492 17.665,1.517 17.853 C 1.629 18.690,2.283 19.360,3.104 19.479 C 3.482 19.533,20.518 19.533,20.896 19.479 C 21.717 19.360,22.371 18.690,22.483 17.853 C 22.508 17.665,22.520 16.919,22.512 15.980 L 22.500 14.420 22.404 14.252 C 22.342 14.143,22.200 14.008,22.008 13.872 C 20.670 12.926,20.670 11.074,22.008 10.128 C 22.200 9.992,22.342 9.857,22.404 9.748 L 22.500 9.580 22.512 8.020 C 22.526 6.254,22.503 5.964,22.314 5.567 C 22.144 5.210,21.789 4.857,21.420 4.677 L 21.140 4.540 12.080 4.534 C 7.097 4.531,2.962 4.542,2.891 4.559 M15.760 6.457 C 15.760 6.968,15.789 7.082,15.970 7.269 C 16.252 7.562,16.748 7.562,17.030 7.269 C 17.211 7.082,17.240 6.968,17.240 6.457 L 17.240 6.000 18.958 6.000 C 20.205 6.000,20.705 6.013,20.781 6.048 C 20.992 6.144,21.000 6.203,21.000 7.643 L 21.000 8.970 20.667 9.295 C 19.891 10.053,19.509 10.945,19.509 12.000 C 19.509 13.055,19.891 13.947,20.667 14.705 L 21.000 15.030 21.000 16.358 C 21.000 17.738,20.994 17.793,20.825 17.929 C 20.747 17.992,20.588 17.998,18.990 17.999 L 17.240 18.000 17.240 17.543 C 17.240 17.043,17.210 16.919,17.047 16.745 C 16.907 16.595,16.726 16.520,16.504 16.520 C 16.280 16.520,16.132 16.575,15.985 16.713 C 15.799 16.887,15.760 17.030,15.760 17.539 L 15.760 18.000 9.510 17.999 C 3.649 17.998,3.255 17.994,3.175 17.929 C 3.006 17.793,3.000 17.738,3.000 16.358 L 3.000 15.031 3.334 14.703 C 4.114 13.935,4.491 13.055,4.491 12.000 C 4.491 10.944,4.107 10.050,3.332 9.297 L 3.000 8.974 3.001 7.637 C 3.003 6.208,3.009 6.164,3.204 6.056 C 3.283 6.012,4.436 6.003,9.530 6.001 L 15.760 6.000 15.760 6.457 M16.167 9.084 C 16.068 9.134,15.948 9.241,15.887 9.333 C 15.785 9.487,15.779 9.520,15.768 10.097 C 15.755 10.779,15.783 10.877,16.055 11.096 C 16.324 11.314,16.818 11.273,17.045 11.014 C 17.213 10.824,17.240 10.700,17.240 10.135 C 17.240 9.657,17.229 9.563,17.155 9.410 C 16.982 9.052,16.525 8.901,16.167 9.084 M7.167 12.086 C 6.848 12.243,6.682 12.662,6.798 13.013 C 6.868 13.227,7.110 13.424,7.369 13.478 C 7.520 13.509,8.393 13.519,10.263 13.511 C 12.693 13.501,12.951 13.494,13.064 13.432 C 13.229 13.342,13.433 13.105,13.463 12.969 C 13.476 12.909,13.493 12.838,13.501 12.812 C 13.530 12.717,13.410 12.355,13.315 12.253 C 13.263 12.196,13.148 12.116,13.060 12.076 C 12.909 12.006,12.737 12.002,10.120 12.001 C 7.357 12.000,7.339 12.001,7.167 12.086 M16.152 12.842 C 16.071 12.885,15.954 12.986,15.892 13.067 C 15.783 13.210,15.780 13.227,15.767 13.808 C 15.755 14.354,15.761 14.418,15.842 14.584 C 15.971 14.850,16.187 14.980,16.500 14.980 C 16.811 14.980,17.029 14.850,17.155 14.590 C 17.229 14.438,17.240 14.343,17.240 13.866 C 17.240 13.287,17.197 13.116,17.014 12.955 C 16.781 12.750,16.416 12.702,16.152 12.842 M7.420 14.259 C 7.398 14.267,7.331 14.284,7.271 14.297 C 7.211 14.310,7.095 14.379,7.015 14.450 C 6.595 14.818,6.699 15.443,7.220 15.684 C 7.367 15.752,7.507 15.758,9.000 15.758 C 10.493 15.758,10.633 15.752,10.780 15.684 C 11.075 15.547,11.240 15.303,11.240 15.000 C 11.240 14.799,11.181 14.647,11.047 14.506 C 10.810 14.255,10.854 14.261,9.080 14.252 C 8.189 14.248,7.442 14.251,7.420 14.259 " stroke="none" fill-rule="evenodd" fill="black"></path></svg>

After

Width:  |  Height:  |  Size: 3.5 KiB

View File

@@ -15,42 +15,49 @@ $web-icon-chevron-up: "\ea0e";
$web-icon-close: "\ea0f";
$web-icon-command: "\ea10";
$web-icon-copy: "\ea11";
$web-icon-daily-dev: "\ea12";
$web-icon-dark: "\ea13";
$web-icon-discord: "\ea14";
$web-icon-divider-vertical: "\ea15";
$web-icon-download: "\ea16";
$web-icon-ext-link: "\ea17";
$web-icon-firebase: "\ea18";
$web-icon-github: "\ea19";
$web-icon-google: "\ea1a";
$web-icon-hamburger-menu: "\ea1b";
$web-icon-instagram: "\ea1c";
$web-icon-light: "\ea1d";
$web-icon-linkedin: "\ea1e";
$web-icon-location: "\ea1f";
$web-icon-logout-left: "\ea20";
$web-icon-logout-right: "\ea21";
$web-icon-mailgun: "\ea22";
$web-icon-mcp: "\ea23";
$web-icon-message: "\ea24";
$web-icon-microsoft: "\ea25";
$web-icon-minus: "\ea26";
$web-icon-nuxt: "\ea27";
$web-icon-platform: "\ea28";
$web-icon-play: "\ea29";
$web-icon-plus: "\ea2a";
$web-icon-product-hunt: "\ea2b";
$web-icon-refine: "\ea2c";
$web-icon-rest: "\ea2d";
$web-icon-search: "\ea2e";
$web-icon-sendgrid: "\ea2f";
$web-icon-star: "\ea30";
$web-icon-system: "\ea31";
$web-icon-textmagic: "\ea32";
$web-icon-tiktok: "\ea33";
$web-icon-twitter: "\ea34";
$web-icon-vue: "\ea35";
$web-icon-x: "\ea36";
$web-icon-ycombinator: "\ea37";
$web-icon-youtube: "\ea38";
$web-icon-customize: "\ea12";
$web-icon-daily-dev: "\ea13";
$web-icon-dark: "\ea14";
$web-icon-discord: "\ea15";
$web-icon-divider-vertical: "\ea16";
$web-icon-download: "\ea17";
$web-icon-edge: "\ea18";
$web-icon-ext-link: "\ea19";
$web-icon-firebase: "\ea1a";
$web-icon-github: "\ea1b";
$web-icon-google: "\ea1c";
$web-icon-hamburger-menu: "\ea1d";
$web-icon-instagram: "\ea1e";
$web-icon-light: "\ea1f";
$web-icon-linkedin: "\ea20";
$web-icon-location: "\ea21";
$web-icon-logout-left: "\ea22";
$web-icon-logout-right: "\ea23";
$web-icon-mailgun: "\ea24";
$web-icon-mcp: "\ea25";
$web-icon-message: "\ea26";
$web-icon-microsoft: "\ea27";
$web-icon-minus: "\ea28";
$web-icon-nuxt: "\ea29";
$web-icon-platform: "\ea2a";
$web-icon-play: "\ea2b";
$web-icon-plus: "\ea2c";
$web-icon-pop-locations: "\ea2d";
$web-icon-product-hunt: "\ea2e";
$web-icon-refine: "\ea2f";
$web-icon-regions: "\ea30";
$web-icon-remix: "\ea31";
$web-icon-rest: "\ea32";
$web-icon-search: "\ea33";
$web-icon-sendgrid: "\ea34";
$web-icon-sparkle: "\ea35";
$web-icon-star: "\ea36";
$web-icon-system: "\ea37";
$web-icon-textmagic: "\ea38";
$web-icon-ticket: "\ea39";
$web-icon-tiktok: "\ea3a";
$web-icon-twitter: "\ea3b";
$web-icon-vue: "\ea3c";
$web-icon-x: "\ea3d";
$web-icon-ycombinator: "\ea3e";
$web-icon-youtube: "\ea3f";

View File

@@ -101,238 +101,280 @@
"className": "web-icon-copy",
"unicode": "&#59921;"
},
"daily-dev": {
"customize": {
"encodedCode": "\\ea12",
"prefix": "web-icon",
"className": "web-icon-daily-dev",
"className": "web-icon-customize",
"unicode": "&#59922;"
},
"dark": {
"daily-dev": {
"encodedCode": "\\ea13",
"prefix": "web-icon",
"className": "web-icon-dark",
"className": "web-icon-daily-dev",
"unicode": "&#59923;"
},
"discord": {
"dark": {
"encodedCode": "\\ea14",
"prefix": "web-icon",
"className": "web-icon-discord",
"className": "web-icon-dark",
"unicode": "&#59924;"
},
"divider-vertical": {
"discord": {
"encodedCode": "\\ea15",
"prefix": "web-icon",
"className": "web-icon-divider-vertical",
"className": "web-icon-discord",
"unicode": "&#59925;"
},
"download": {
"divider-vertical": {
"encodedCode": "\\ea16",
"prefix": "web-icon",
"className": "web-icon-download",
"className": "web-icon-divider-vertical",
"unicode": "&#59926;"
},
"ext-link": {
"download": {
"encodedCode": "\\ea17",
"prefix": "web-icon",
"className": "web-icon-ext-link",
"className": "web-icon-download",
"unicode": "&#59927;"
},
"firebase": {
"edge": {
"encodedCode": "\\ea18",
"prefix": "web-icon",
"className": "web-icon-firebase",
"className": "web-icon-edge",
"unicode": "&#59928;"
},
"github": {
"ext-link": {
"encodedCode": "\\ea19",
"prefix": "web-icon",
"className": "web-icon-github",
"className": "web-icon-ext-link",
"unicode": "&#59929;"
},
"google": {
"firebase": {
"encodedCode": "\\ea1a",
"prefix": "web-icon",
"className": "web-icon-google",
"className": "web-icon-firebase",
"unicode": "&#59930;"
},
"hamburger-menu": {
"github": {
"encodedCode": "\\ea1b",
"prefix": "web-icon",
"className": "web-icon-hamburger-menu",
"className": "web-icon-github",
"unicode": "&#59931;"
},
"instagram": {
"google": {
"encodedCode": "\\ea1c",
"prefix": "web-icon",
"className": "web-icon-instagram",
"className": "web-icon-google",
"unicode": "&#59932;"
},
"light": {
"hamburger-menu": {
"encodedCode": "\\ea1d",
"prefix": "web-icon",
"className": "web-icon-light",
"className": "web-icon-hamburger-menu",
"unicode": "&#59933;"
},
"linkedin": {
"instagram": {
"encodedCode": "\\ea1e",
"prefix": "web-icon",
"className": "web-icon-linkedin",
"className": "web-icon-instagram",
"unicode": "&#59934;"
},
"location": {
"light": {
"encodedCode": "\\ea1f",
"prefix": "web-icon",
"className": "web-icon-location",
"className": "web-icon-light",
"unicode": "&#59935;"
},
"logout-left": {
"linkedin": {
"encodedCode": "\\ea20",
"prefix": "web-icon",
"className": "web-icon-logout-left",
"className": "web-icon-linkedin",
"unicode": "&#59936;"
},
"logout-right": {
"location": {
"encodedCode": "\\ea21",
"prefix": "web-icon",
"className": "web-icon-logout-right",
"className": "web-icon-location",
"unicode": "&#59937;"
},
"mailgun": {
"logout-left": {
"encodedCode": "\\ea22",
"prefix": "web-icon",
"className": "web-icon-mailgun",
"className": "web-icon-logout-left",
"unicode": "&#59938;"
},
"mcp": {
"logout-right": {
"encodedCode": "\\ea23",
"prefix": "web-icon",
"className": "web-icon-mcp",
"className": "web-icon-logout-right",
"unicode": "&#59939;"
},
"message": {
"mailgun": {
"encodedCode": "\\ea24",
"prefix": "web-icon",
"className": "web-icon-message",
"className": "web-icon-mailgun",
"unicode": "&#59940;"
},
"microsoft": {
"mcp": {
"encodedCode": "\\ea25",
"prefix": "web-icon",
"className": "web-icon-microsoft",
"className": "web-icon-mcp",
"unicode": "&#59941;"
},
"minus": {
"message": {
"encodedCode": "\\ea26",
"prefix": "web-icon",
"className": "web-icon-minus",
"className": "web-icon-message",
"unicode": "&#59942;"
},
"nuxt": {
"microsoft": {
"encodedCode": "\\ea27",
"prefix": "web-icon",
"className": "web-icon-nuxt",
"className": "web-icon-microsoft",
"unicode": "&#59943;"
},
"platform": {
"minus": {
"encodedCode": "\\ea28",
"prefix": "web-icon",
"className": "web-icon-platform",
"className": "web-icon-minus",
"unicode": "&#59944;"
},
"play": {
"nuxt": {
"encodedCode": "\\ea29",
"prefix": "web-icon",
"className": "web-icon-play",
"className": "web-icon-nuxt",
"unicode": "&#59945;"
},
"plus": {
"platform": {
"encodedCode": "\\ea2a",
"prefix": "web-icon",
"className": "web-icon-plus",
"className": "web-icon-platform",
"unicode": "&#59946;"
},
"product-hunt": {
"play": {
"encodedCode": "\\ea2b",
"prefix": "web-icon",
"className": "web-icon-product-hunt",
"className": "web-icon-play",
"unicode": "&#59947;"
},
"refine": {
"plus": {
"encodedCode": "\\ea2c",
"prefix": "web-icon",
"className": "web-icon-refine",
"className": "web-icon-plus",
"unicode": "&#59948;"
},
"rest": {
"pop-locations": {
"encodedCode": "\\ea2d",
"prefix": "web-icon",
"className": "web-icon-rest",
"className": "web-icon-pop-locations",
"unicode": "&#59949;"
},
"search": {
"product-hunt": {
"encodedCode": "\\ea2e",
"prefix": "web-icon",
"className": "web-icon-search",
"className": "web-icon-product-hunt",
"unicode": "&#59950;"
},
"sendgrid": {
"refine": {
"encodedCode": "\\ea2f",
"prefix": "web-icon",
"className": "web-icon-sendgrid",
"className": "web-icon-refine",
"unicode": "&#59951;"
},
"star": {
"regions": {
"encodedCode": "\\ea30",
"prefix": "web-icon",
"className": "web-icon-star",
"className": "web-icon-regions",
"unicode": "&#59952;"
},
"system": {
"remix": {
"encodedCode": "\\ea31",
"prefix": "web-icon",
"className": "web-icon-system",
"className": "web-icon-remix",
"unicode": "&#59953;"
},
"textmagic": {
"rest": {
"encodedCode": "\\ea32",
"prefix": "web-icon",
"className": "web-icon-textmagic",
"className": "web-icon-rest",
"unicode": "&#59954;"
},
"tiktok": {
"search": {
"encodedCode": "\\ea33",
"prefix": "web-icon",
"className": "web-icon-tiktok",
"className": "web-icon-search",
"unicode": "&#59955;"
},
"twitter": {
"sendgrid": {
"encodedCode": "\\ea34",
"prefix": "web-icon",
"className": "web-icon-twitter",
"className": "web-icon-sendgrid",
"unicode": "&#59956;"
},
"vue": {
"sparkle": {
"encodedCode": "\\ea35",
"prefix": "web-icon",
"className": "web-icon-vue",
"className": "web-icon-sparkle",
"unicode": "&#59957;"
},
"x": {
"star": {
"encodedCode": "\\ea36",
"prefix": "web-icon",
"className": "web-icon-x",
"className": "web-icon-star",
"unicode": "&#59958;"
},
"ycombinator": {
"system": {
"encodedCode": "\\ea37",
"prefix": "web-icon",
"className": "web-icon-ycombinator",
"className": "web-icon-system",
"unicode": "&#59959;"
},
"youtube": {
"textmagic": {
"encodedCode": "\\ea38",
"prefix": "web-icon",
"className": "web-icon-youtube",
"className": "web-icon-textmagic",
"unicode": "&#59960;"
},
"ticket": {
"encodedCode": "\\ea39",
"prefix": "web-icon",
"className": "web-icon-ticket",
"unicode": "&#59961;"
},
"tiktok": {
"encodedCode": "\\ea3a",
"prefix": "web-icon",
"className": "web-icon-tiktok",
"unicode": "&#59962;"
},
"twitter": {
"encodedCode": "\\ea3b",
"prefix": "web-icon",
"className": "web-icon-twitter",
"unicode": "&#59963;"
},
"vue": {
"encodedCode": "\\ea3c",
"prefix": "web-icon",
"className": "web-icon-vue",
"unicode": "&#59964;"
},
"x": {
"encodedCode": "\\ea3d",
"prefix": "web-icon",
"className": "web-icon-x",
"unicode": "&#59965;"
},
"ycombinator": {
"encodedCode": "\\ea3e",
"prefix": "web-icon",
"className": "web-icon-ycombinator",
"unicode": "&#59966;"
},
"youtube": {
"encodedCode": "\\ea3f",
"prefix": "web-icon",
"className": "web-icon-youtube",
"unicode": "&#59967;"
}
}

View File

@@ -71,120 +71,141 @@
.web-icon-copy:before {
content: '\ea11';
}
.web-icon-daily-dev:before {
.web-icon-customize:before {
content: '\ea12';
}
.web-icon-dark:before {
.web-icon-daily-dev:before {
content: '\ea13';
}
.web-icon-discord:before {
.web-icon-dark:before {
content: '\ea14';
}
.web-icon-divider-vertical:before {
.web-icon-discord:before {
content: '\ea15';
}
.web-icon-download:before {
.web-icon-divider-vertical:before {
content: '\ea16';
}
.web-icon-ext-link:before {
.web-icon-download:before {
content: '\ea17';
}
.web-icon-firebase:before {
.web-icon-edge:before {
content: '\ea18';
}
.web-icon-github:before {
.web-icon-ext-link:before {
content: '\ea19';
}
.web-icon-google:before {
.web-icon-firebase:before {
content: '\ea1a';
}
.web-icon-hamburger-menu:before {
.web-icon-github:before {
content: '\ea1b';
}
.web-icon-instagram:before {
.web-icon-google:before {
content: '\ea1c';
}
.web-icon-light:before {
.web-icon-hamburger-menu:before {
content: '\ea1d';
}
.web-icon-linkedin:before {
.web-icon-instagram:before {
content: '\ea1e';
}
.web-icon-location:before {
.web-icon-light:before {
content: '\ea1f';
}
.web-icon-logout-left:before {
.web-icon-linkedin:before {
content: '\ea20';
}
.web-icon-logout-right:before {
.web-icon-location:before {
content: '\ea21';
}
.web-icon-mailgun:before {
.web-icon-logout-left:before {
content: '\ea22';
}
.web-icon-mcp:before {
.web-icon-logout-right:before {
content: '\ea23';
}
.web-icon-message:before {
.web-icon-mailgun:before {
content: '\ea24';
}
.web-icon-microsoft:before {
.web-icon-mcp:before {
content: '\ea25';
}
.web-icon-minus:before {
.web-icon-message:before {
content: '\ea26';
}
.web-icon-nuxt:before {
.web-icon-microsoft:before {
content: '\ea27';
}
.web-icon-platform:before {
.web-icon-minus:before {
content: '\ea28';
}
.web-icon-play:before {
.web-icon-nuxt:before {
content: '\ea29';
}
.web-icon-plus:before {
.web-icon-platform:before {
content: '\ea2a';
}
.web-icon-product-hunt:before {
.web-icon-play:before {
content: '\ea2b';
}
.web-icon-refine:before {
.web-icon-plus:before {
content: '\ea2c';
}
.web-icon-rest:before {
.web-icon-pop-locations:before {
content: '\ea2d';
}
.web-icon-search:before {
.web-icon-product-hunt:before {
content: '\ea2e';
}
.web-icon-sendgrid:before {
.web-icon-refine:before {
content: '\ea2f';
}
.web-icon-star:before {
.web-icon-regions:before {
content: '\ea30';
}
.web-icon-system:before {
.web-icon-remix:before {
content: '\ea31';
}
.web-icon-textmagic:before {
.web-icon-rest:before {
content: '\ea32';
}
.web-icon-tiktok:before {
.web-icon-search:before {
content: '\ea33';
}
.web-icon-twitter:before {
.web-icon-sendgrid:before {
content: '\ea34';
}
.web-icon-vue:before {
.web-icon-sparkle:before {
content: '\ea35';
}
.web-icon-x:before {
.web-icon-star:before {
content: '\ea36';
}
.web-icon-ycombinator:before {
.web-icon-system:before {
content: '\ea37';
}
.web-icon-youtube:before {
.web-icon-textmagic:before {
content: '\ea38';
}
.web-icon-ticket:before {
content: '\ea39';
}
.web-icon-tiktok:before {
content: '\ea3a';
}
.web-icon-twitter:before {
content: '\ea3b';
}
.web-icon-vue:before {
content: '\ea3c';
}
.web-icon-x:before {
content: '\ea3d';
}
.web-icon-ycombinator:before {
content: '\ea3e';
}
.web-icon-youtube:before {
content: '\ea3f';
}

Binary file not shown.

View File

@@ -33,45 +33,52 @@
.web-icon-close:before { content: "\ea0f"; }
.web-icon-command:before { content: "\ea10"; }
.web-icon-copy:before { content: "\ea11"; }
.web-icon-daily-dev:before { content: "\ea12"; }
.web-icon-dark:before { content: "\ea13"; }
.web-icon-discord:before { content: "\ea14"; }
.web-icon-divider-vertical:before { content: "\ea15"; }
.web-icon-download:before { content: "\ea16"; }
.web-icon-ext-link:before { content: "\ea17"; }
.web-icon-firebase:before { content: "\ea18"; }
.web-icon-github:before { content: "\ea19"; }
.web-icon-google:before { content: "\ea1a"; }
.web-icon-hamburger-menu:before { content: "\ea1b"; }
.web-icon-instagram:before { content: "\ea1c"; }
.web-icon-light:before { content: "\ea1d"; }
.web-icon-linkedin:before { content: "\ea1e"; }
.web-icon-location:before { content: "\ea1f"; }
.web-icon-logout-left:before { content: "\ea20"; }
.web-icon-logout-right:before { content: "\ea21"; }
.web-icon-mailgun:before { content: "\ea22"; }
.web-icon-mcp:before { content: "\ea23"; }
.web-icon-message:before { content: "\ea24"; }
.web-icon-microsoft:before { content: "\ea25"; }
.web-icon-minus:before { content: "\ea26"; }
.web-icon-nuxt:before { content: "\ea27"; }
.web-icon-platform:before { content: "\ea28"; }
.web-icon-play:before { content: "\ea29"; }
.web-icon-plus:before { content: "\ea2a"; }
.web-icon-product-hunt:before { content: "\ea2b"; }
.web-icon-refine:before { content: "\ea2c"; }
.web-icon-rest:before { content: "\ea2d"; }
.web-icon-search:before { content: "\ea2e"; }
.web-icon-sendgrid:before { content: "\ea2f"; }
.web-icon-star:before { content: "\ea30"; }
.web-icon-system:before { content: "\ea31"; }
.web-icon-textmagic:before { content: "\ea32"; }
.web-icon-tiktok:before { content: "\ea33"; }
.web-icon-twitter:before { content: "\ea34"; }
.web-icon-vue:before { content: "\ea35"; }
.web-icon-x:before { content: "\ea36"; }
.web-icon-ycombinator:before { content: "\ea37"; }
.web-icon-youtube:before { content: "\ea38"; }
.web-icon-customize:before { content: "\ea12"; }
.web-icon-daily-dev:before { content: "\ea13"; }
.web-icon-dark:before { content: "\ea14"; }
.web-icon-discord:before { content: "\ea15"; }
.web-icon-divider-vertical:before { content: "\ea16"; }
.web-icon-download:before { content: "\ea17"; }
.web-icon-edge:before { content: "\ea18"; }
.web-icon-ext-link:before { content: "\ea19"; }
.web-icon-firebase:before { content: "\ea1a"; }
.web-icon-github:before { content: "\ea1b"; }
.web-icon-google:before { content: "\ea1c"; }
.web-icon-hamburger-menu:before { content: "\ea1d"; }
.web-icon-instagram:before { content: "\ea1e"; }
.web-icon-light:before { content: "\ea1f"; }
.web-icon-linkedin:before { content: "\ea20"; }
.web-icon-location:before { content: "\ea21"; }
.web-icon-logout-left:before { content: "\ea22"; }
.web-icon-logout-right:before { content: "\ea23"; }
.web-icon-mailgun:before { content: "\ea24"; }
.web-icon-mcp:before { content: "\ea25"; }
.web-icon-message:before { content: "\ea26"; }
.web-icon-microsoft:before { content: "\ea27"; }
.web-icon-minus:before { content: "\ea28"; }
.web-icon-nuxt:before { content: "\ea29"; }
.web-icon-platform:before { content: "\ea2a"; }
.web-icon-play:before { content: "\ea2b"; }
.web-icon-plus:before { content: "\ea2c"; }
.web-icon-pop-locations:before { content: "\ea2d"; }
.web-icon-product-hunt:before { content: "\ea2e"; }
.web-icon-refine:before { content: "\ea2f"; }
.web-icon-regions:before { content: "\ea30"; }
.web-icon-remix:before { content: "\ea31"; }
.web-icon-rest:before { content: "\ea32"; }
.web-icon-search:before { content: "\ea33"; }
.web-icon-sendgrid:before { content: "\ea34"; }
.web-icon-sparkle:before { content: "\ea35"; }
.web-icon-star:before { content: "\ea36"; }
.web-icon-system:before { content: "\ea37"; }
.web-icon-textmagic:before { content: "\ea38"; }
.web-icon-ticket:before { content: "\ea39"; }
.web-icon-tiktok:before { content: "\ea3a"; }
.web-icon-twitter:before { content: "\ea3b"; }
.web-icon-vue:before { content: "\ea3c"; }
.web-icon-x:before { content: "\ea3d"; }
.web-icon-ycombinator:before { content: "\ea3e"; }
.web-icon-youtube:before { content: "\ea3f"; }
$web-icon-apple: "\ea01";
$web-icon-appwrite: "\ea02";
@@ -90,42 +97,49 @@ $web-icon-chevron-up: "\ea0e";
$web-icon-close: "\ea0f";
$web-icon-command: "\ea10";
$web-icon-copy: "\ea11";
$web-icon-daily-dev: "\ea12";
$web-icon-dark: "\ea13";
$web-icon-discord: "\ea14";
$web-icon-divider-vertical: "\ea15";
$web-icon-download: "\ea16";
$web-icon-ext-link: "\ea17";
$web-icon-firebase: "\ea18";
$web-icon-github: "\ea19";
$web-icon-google: "\ea1a";
$web-icon-hamburger-menu: "\ea1b";
$web-icon-instagram: "\ea1c";
$web-icon-light: "\ea1d";
$web-icon-linkedin: "\ea1e";
$web-icon-location: "\ea1f";
$web-icon-logout-left: "\ea20";
$web-icon-logout-right: "\ea21";
$web-icon-mailgun: "\ea22";
$web-icon-mcp: "\ea23";
$web-icon-message: "\ea24";
$web-icon-microsoft: "\ea25";
$web-icon-minus: "\ea26";
$web-icon-nuxt: "\ea27";
$web-icon-platform: "\ea28";
$web-icon-play: "\ea29";
$web-icon-plus: "\ea2a";
$web-icon-product-hunt: "\ea2b";
$web-icon-refine: "\ea2c";
$web-icon-rest: "\ea2d";
$web-icon-search: "\ea2e";
$web-icon-sendgrid: "\ea2f";
$web-icon-star: "\ea30";
$web-icon-system: "\ea31";
$web-icon-textmagic: "\ea32";
$web-icon-tiktok: "\ea33";
$web-icon-twitter: "\ea34";
$web-icon-vue: "\ea35";
$web-icon-x: "\ea36";
$web-icon-ycombinator: "\ea37";
$web-icon-youtube: "\ea38";
$web-icon-customize: "\ea12";
$web-icon-daily-dev: "\ea13";
$web-icon-dark: "\ea14";
$web-icon-discord: "\ea15";
$web-icon-divider-vertical: "\ea16";
$web-icon-download: "\ea17";
$web-icon-edge: "\ea18";
$web-icon-ext-link: "\ea19";
$web-icon-firebase: "\ea1a";
$web-icon-github: "\ea1b";
$web-icon-google: "\ea1c";
$web-icon-hamburger-menu: "\ea1d";
$web-icon-instagram: "\ea1e";
$web-icon-light: "\ea1f";
$web-icon-linkedin: "\ea20";
$web-icon-location: "\ea21";
$web-icon-logout-left: "\ea22";
$web-icon-logout-right: "\ea23";
$web-icon-mailgun: "\ea24";
$web-icon-mcp: "\ea25";
$web-icon-message: "\ea26";
$web-icon-microsoft: "\ea27";
$web-icon-minus: "\ea28";
$web-icon-nuxt: "\ea29";
$web-icon-platform: "\ea2a";
$web-icon-play: "\ea2b";
$web-icon-plus: "\ea2c";
$web-icon-pop-locations: "\ea2d";
$web-icon-product-hunt: "\ea2e";
$web-icon-refine: "\ea2f";
$web-icon-regions: "\ea30";
$web-icon-remix: "\ea31";
$web-icon-rest: "\ea32";
$web-icon-search: "\ea33";
$web-icon-sendgrid: "\ea34";
$web-icon-sparkle: "\ea35";
$web-icon-star: "\ea36";
$web-icon-system: "\ea37";
$web-icon-textmagic: "\ea38";
$web-icon-ticket: "\ea39";
$web-icon-tiktok: "\ea3a";
$web-icon-twitter: "\ea3b";
$web-icon-vue: "\ea3c";
$web-icon-x: "\ea3d";
$web-icon-ycombinator: "\ea3e";
$web-icon-youtube: "\ea3f";

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 185 KiB

After

Width:  |  Height:  |  Size: 212 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 78 KiB

After

Width:  |  Height:  |  Size: 89 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,28 @@
<svg
width="20"
height="20"
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M8.46377 5L5.6 5C5.03995 5 4.75992 5 4.54601 5.10899C4.35785 5.20487 4.20487 5.35785 4.10899 5.54601C4 5.75992 4 6.03995 4 6.6V14.4C4 14.9601 4 15.2401 4.10899 15.454C4.20487 15.6422 4.35785 15.7951 4.54601 15.891C4.75992 16 5.03995 16 5.6 16H13.4C13.9601 16 14.2401 16 14.454 15.891C14.6422 15.7951 14.7951 15.6422 14.891 15.454C15 15.2401 15 14.9601 15 14.4V11.6957"
stroke="currentColor"
stroke-width="1.2"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M12.99 4.18163C13.386 3.78562 13.584 3.58761 13.8123 3.51342C14.0132 3.44816 14.2295 3.44816 14.4304 3.51342C14.6587 3.58761 14.8567 3.78562 15.2527 4.18163L15.8184 4.74732C16.2144 5.14334 16.4124 5.34134 16.4866 5.56967C16.5519 5.77052 16.5519 5.98686 16.4866 6.18771C16.4124 6.41603 16.2144 6.61404 15.8184 7.01006L11.7615 11.0669C11.5886 11.2399 11.5021 11.3263 11.4012 11.3882C11.3117 11.443 11.2142 11.4834 11.1121 11.5079C10.9971 11.5355 10.8748 11.5355 10.6302 11.5355H10.0645C9.50443 11.5355 9.2244 11.5355 9.01049 11.4266C8.82233 11.3307 8.66935 11.1777 8.57348 10.9895C8.46448 10.7756 8.46448 10.4956 8.46448 9.93554V9.36986C8.46448 9.12527 8.46448 9.00297 8.49211 8.88788C8.51661 8.78585 8.55701 8.6883 8.61184 8.59883C8.67369 8.49791 8.76016 8.41144 8.93311 8.23849L12.99 4.18163Z"
stroke="currentColor"
stroke-width="1.2"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M12 5L15 8"
stroke="currentColor"
stroke-width="1.2"
stroke-linejoin="round"
/>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@@ -1,14 +1,5 @@
<svg
width="21"
height="20"
viewBox="0 0 21 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<svg width="21" height="20" viewBox="0 0 21 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M10.5 14.8C13.151 14.8 15.3 12.651 15.3 10C15.3 7.34903 13.151 5.2 10.5 5.2C7.84903 5.2 5.7 7.34903 5.7 10C5.7 12.651 7.84903 14.8 10.5 14.8ZM10.5 16C13.8137 16 16.5 13.3137 16.5 10C16.5 6.68629 13.8137 4 10.5 4C7.18629 4 4.5 6.68629 4.5 10C4.5 13.3137 7.18629 16 10.5 16Z"
fill="currentColor"
/>
d="M3.07619 2.57566C3.28117 2.37086 3.59729 2.3449 3.83009 2.49851L3.92384 2.57566L7.41505 6.06687C8.26493 5.39934 9.3355 4.99948 10.5 4.99948L10.7569 5.00632C13.3987 5.14004 15.4998 7.32442 15.5 9.99948L15.4932 10.2563C15.4393 11.3213 15.0517 12.2984 14.4336 13.0854L17.9238 16.5757L18.001 16.6694C18.1547 16.9022 18.1287 17.2183 17.9238 17.4233C17.7188 17.6283 17.4028 17.6543 17.1699 17.5005L17.0762 17.4233L13.586 13.9331C12.7361 14.6009 11.6648 14.9995 10.5 14.9995L10.2432 14.9926C7.68619 14.8633 5.63624 12.8133 5.50685 10.2563L5.50001 9.99948C5.50011 8.83482 5.89864 7.76343 6.56642 6.91355L3.07619 3.42331L2.99904 3.32956C2.84545 3.0967 2.87123 2.78061 3.07619 2.57566ZM10.5 6.19968C8.40148 6.19968 6.70044 7.901 6.70021 9.99948C6.70021 12.0982 8.40133 13.7993 10.5 13.7993C12.5987 13.7992 14.2998 12.0981 14.2998 9.99948C14.2996 7.90103 12.5985 6.19972 10.5 6.19968Z"
fill="#19191C" />
</svg>

Before

Width:  |  Height:  |  Size: 509 B

After

Width:  |  Height:  |  Size: 1.0 KiB

3
src/icons/svg/remix.svg Normal file
View File

@@ -0,0 +1,3 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10.5876 3C13.8692 3 15.5 4.71409 15.5 7.45218C15.5 9.50017 14.3524 10.8358 12.8022 11.0584C14.1108 11.3478 14.8758 12.1715 15.0168 13.7965L15.0372 14.0953L15.055 14.3747L15.0704 14.636L15.0811 14.8331L15.0926 15.0656L15.0985 15.1983L15.107 15.4091L15.1138 15.6082L15.1172 15.7226L15.1218 15.906L15.1258 16.1156L15.1272 16.2169L15.1298 16.5105L15.1306 16.8168L15.1307 17H11.7465L11.7467 16.9236L11.7479 16.7752L11.7499 16.6307L11.7558 16.2806L11.7572 16.1633L11.7584 15.9792L11.7582 15.8615L11.7573 15.7381L11.7559 15.6413L11.7531 15.5057L11.7491 15.3618L11.7437 15.2089L11.7368 15.046L11.7327 14.9605L11.7258 14.827L11.7178 14.6866L11.7055 14.4883L11.6949 14.3307C11.6918 14.2821 11.6883 14.2347 11.6843 14.1883L11.6756 14.0972C11.5639 13.0207 11.1748 12.5807 10.5084 12.434L10.4523 12.4224C10.4144 12.4152 10.3757 12.4088 10.3361 12.4032L10.2761 12.3955C10.2659 12.3943 10.2558 12.3931 10.2456 12.392L10.1836 12.3861L10.1204 12.3812L10.0559 12.3774L9.99079 12.3746L9.92444 12.3728L9.85685 12.3719L4.5 12.3718V9.45568H9.98364C10.0733 9.45568 10.1602 9.45381 10.2443 9.45L10.3275 9.44555L10.4088 9.43978L10.4883 9.43269C10.5014 9.43139 10.5145 9.43004 10.5274 9.42864L10.6041 9.41952C10.6546 9.41299 10.7039 9.40555 10.752 9.39717L10.8232 9.3839C11.713 9.20685 12.1579 8.68846 12.1579 7.67686C12.1579 6.54159 11.4332 5.85355 9.98364 5.85355H4.5V3H10.5876ZM8.07828 14.8261C8.52228 14.8261 8.70805 15.0966 8.77423 15.3556L8.78296 15.3928L8.79013 15.4297L8.79479 15.4588L8.79679 15.4732L8.80013 15.5017L8.80264 15.5295L8.80361 15.5432L8.80497 15.57L8.80539 15.5831L8.80572 15.6086V17H4.5V14.8261H8.07828Z" fill="#2D2D31"/>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@@ -0,0 +1,5 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M3 10C5.45614 10 10.3684 8.6 10.3684 3C10.3684 8.6 14.7895 10 17 10C11.6947 10 10.3684 14.6667 10.3684 17C10.3684 11.4 5.45614 10 3 10Z"
fill="#EDEDF0" />
</svg>

After

Width:  |  Height:  |  Size: 286 B

5
src/icons/svg/ticket.svg Normal file
View File

@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5"
stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round"
d="M16.5 6v.75m0 3v.75m0 3v.75m0 3V18m-9-5.25h5.25M7.5 15h3M3.375 5.25c-.621 0-1.125.504-1.125 1.125v3.026a2.999 2.999 0 0 1 0 5.198v3.026c0 .621.504 1.125 1.125 1.125h17.25c.621 0 1.125-.504 1.125-1.125v-3.026a2.999 2.999 0 0 1 0-5.198V6.375c0-.621-.504-1.125-1.125-1.125H3.375Z" />
</svg>

After

Width:  |  Height:  |  Size: 473 B

View File

@@ -1,12 +1,11 @@
import { Analytics, type AnalyticsPlugin } from 'analytics';
import Plausible from 'plausible-tracker';
import posthogEvent from 'posthog-js';
import { get } from 'svelte/store';
import { page } from '$app/stores';
import { page } from '$app/state';
import { ENV } from '$lib/system';
import { browser } from '$app/environment';
import posthogEvent from 'posthog-js';
import Plausible from 'plausible-tracker';
import { Analytics, type AnalyticsPlugin } from 'analytics';
type Payload = {
payload: {
event: string;
@@ -55,30 +54,20 @@ const analytics = Analytics({
plugins: [plausible('appwrite.io')]
});
export type TrackEventArgs = {
plausible?: { name: string; data?: object };
posthog?: { name: string };
};
export type TrackEventArgs = { name: string; data?: object };
export const trackEvent = async (platforms: TrackEventArgs) => {
if (!isTrackingAllowed()) {
export const trackEvent = (eventArgs?: string | TrackEventArgs): void => {
if (!eventArgs || ENV.TEST) return;
const path = page.route.id?.replace(/\(([^()]*)\)/g, '') ?? '';
const name = typeof eventArgs === 'string' ? eventArgs : eventArgs.name;
const data = typeof eventArgs === 'string' ? { path } : { ...eventArgs.data, path };
if (ENV.DEV || ENV.PREVIEW) {
console.log(`[Analytics] Event:`, name, data);
return;
}
const currentPage = get(page);
const path = currentPage.route.id ?? '';
if (ENV.DEV || ENV.PREVIEW) {
console.log(`[Analytics] Event`, platforms.plausible, platforms.posthog);
} else {
if (platforms.plausible) {
await analytics.track(platforms.plausible.name, { ...platforms.plausible.data, path });
}
if (platforms.posthog) {
posthogEvent.capture(platforms.posthog.name);
}
}
posthogEvent.capture(name, data);
analytics.track(name, data).then();
};
export const isTrackingAllowed = () => !ENV.TEST;

View File

@@ -1,17 +1,13 @@
import { inView, type InViewOptions } from 'motion';
import { inView } from 'motion';
import { writable } from 'svelte/store';
export const useAnimateInView = ({ options }: { options?: InViewOptions }) => {
export const useAnimateInView = () => {
let animate = writable<boolean>(false);
const action = (node: HTMLElement) => {
inView(
node,
() => {
animate.set(true);
},
{ ...options }
);
inView(node, () => {
animate.set(true);
});
};
return {

View File

@@ -0,0 +1,41 @@
import { hover } from 'motion';
import { writable } from 'svelte/store';
export interface Position {
x: number;
y: number;
}
export const useMousePosition = () => {
let position = $state<Position>({
x: 0,
y: 0
});
const action = (node: HTMLElement | SVGSVGElement) => {
const handleMouseMove = (event: MouseEvent) => {
const { clientX, clientY } = event;
position = {
x: clientX - 12, // Remove rect.left
y: clientY + -350 // Remove rect.top
};
};
hover(node, () => {
document.addEventListener('mousemove', handleMouseMove);
});
return {
destroy() {
document.removeEventListener('mousemove', handleMouseMove);
}
};
};
return {
action,
position: () => {
return position;
}
};
};

View File

@@ -1,34 +0,0 @@
import { inView } from 'motion';
import { type Writable, writable } from 'svelte/store';
export const useMousePosition = () => {
let position = writable<{ x: number; y: number }>({
x: 0,
y: 0
});
const action = (node: HTMLElement) => {
const handleMouseMove = (event: MouseEvent) => {
position.set({
x: event.clientX,
y: event.clientY
});
};
inView(
node,
() => {
node.addEventListener('mousemove', handleMouseMove);
},
{ amount: 'any' }
);
return {
destroy() {
node.removeEventListener('mousemove', handleMouseMove);
}
};
};
return { action, position };
};

View File

@@ -1,34 +0,0 @@
<script lang="ts">
import { rect } from '$lib/actions';
import { writable } from 'svelte/store';
const bodyRect = writable<DOMRect | null>(null);
</script>
<div class="relative">
<div
class="true-body"
style:width={`${$bodyRect?.width ?? 0}px`}
style:height={`${$bodyRect?.height ?? 0}px`}
></div>
<div class="body" use:rect={bodyRect}>
<slot />
</div>
</div>
<style lang="scss">
.relative {
position: relative;
overflow: hidden;
}
.body {
position: absolute;
left: 0;
top: 0;
}
.true-body {
transition: 0.2s ease;
}
</style>

View File

@@ -1,11 +0,0 @@
<script lang="ts">
import '$scss/hljs.css';
import { getCodeHtml } from '$lib/utils/code';
export let content: string;
$: codeHtml = getCodeHtml({ content, language: 'js' });
</script>
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
{@html codeHtml}

View File

@@ -1,96 +0,0 @@
<script>
import AutoBox from '../AutoBox.svelte';
import Code from './Code.svelte';
</script>
<div class="code-console">
<div class="header">
<div class="ellipse"></div>
<div class="ellipse-2"></div>
<div class="ellipse-3"></div>
</div>
<div class="block">
<AutoBox>
<slot {Code} />
</AutoBox>
</div>
<div id="code-bottom"></div>
</div>
<style lang="scss">
@use '$scss/abstract/mixins/border-gradient' as gradients;
.code-console {
@include gradients.border-gradient;
--p-radius: 16px;
display: flex;
flex-direction: column;
background-color: hsl(var(--web-color-card));
border-radius: var(--p-radius);
--m-border-radius: var(--p-radius);
--m-border-gradient-before: linear-gradient(
180deg,
rgba(255, 255, 255, 0.12) 0%,
rgba(255, 255, 255, 0) 125.11%
);
backdrop-filter: blur(8px);
min-width: 330px;
width: fit-content;
padding: 0 0.25rem 0.25rem;
}
.block {
width: 100%;
flex-grow: 1;
text-align: left;
border-radius: 12px;
background: linear-gradient(129deg, rgba(0, 0, 0, 0.48) 22.38%, rgba(0, 0, 0, 0) 136.5%);
padding: 20px;
position: relative;
}
.header {
position: relative;
display: flex;
align-items: center;
gap: 0.375rem;
padding: 0.75rem;
.ellipse {
background-color: #ec6a5e;
width: 8px;
height: 8px;
top: 12px;
border-radius: 4px;
}
.ellipse-2 {
background-color: #f5bf4f;
width: 8px;
height: 8px;
top: 12px;
border-radius: 4px;
}
.ellipse-3 {
background-color: #61c554;
width: 8px;
height: 8px;
top: 12px;
border-radius: 4px;
}
}
.code-console :global(code) {
white-space: pre;
tab-size: 4;
}
</style>

View File

@@ -1,395 +0,0 @@
<script lang="ts">
import { toScale, type Scale } from '$lib/utils/toScale';
import { spring, type AnimationListOptions, type SpringOptions } from 'motion';
import { animation, createScrollHandler, scroll, type Animation } from '.';
import { SOCIAL_STATS } from '$lib/constants';
const springOptions: SpringOptions = {
stiffness: 58.78,
mass: 1,
damping: 17.14
};
const animationOptions: AnimationListOptions = {
x: { easing: spring(springOptions) },
y: { easing: spring(springOptions) }
};
const animations: {
mobile: {
main: Animation;
reversed: Animation;
};
desktop: {
main: Animation;
reversed: Animation;
};
}[] = [
{
mobile: {
main: animation(
'#oss-discord',
{ x: 0, y: [1200, 0], rotate: 1 },
animationOptions
),
reversed: animation('#oss-discord', { y: 1200, x: 0, rotate: 1 }, animationOptions)
},
desktop: {
main: animation(
'#oss-discord',
{ x: 20, y: '-75vh', rotate: 15 },
animationOptions
),
reversed: animation(
'#oss-discord',
{ x: -100, y: '0vh', rotate: 15 },
animationOptions
)
}
},
{
mobile: {
main: animation(
'#oss-github',
{ x: 0, y: [1200, -10], rotate: -2 },
animationOptions
),
reversed: animation('#oss-github', { y: 1200, x: 10, rotate: -2 }, animationOptions)
},
desktop: {
main: animation(
'#oss-github',
{ x: -100, y: '-50vh', rotate: 6.26 },
animationOptions
),
reversed: animation(
'#oss-github',
{ x: 0, y: '0vh', rotate: 6.26 },
animationOptions
)
}
},
{
mobile: {
main: animation(
'#oss-twitter',
{ x: 0, y: [1200, 10], rotate: -3 },
animationOptions
),
reversed: animation(
'#oss-twitter',
{ y: 1200, x: -10, rotate: -3 },
animationOptions
)
},
desktop: {
main: animation(
'#oss-twitter',
{ x: 100, y: '-65vh', rotate: -15 },
animationOptions
),
reversed: animation(
'#oss-twitter',
{ x: 0, y: '0vh', rotate: -15 },
animationOptions
)
}
},
{
mobile: {
main: animation(
'#oss-youtube',
{ x: 0, y: [1200, 5], rotate: 2 },
animationOptions
),
reversed: animation('#oss-youtube', { y: 1200, x: -5, rotate: 2 }, animationOptions)
},
desktop: {
main: animation(
'#oss-youtube',
{ x: -100, y: '-50vh', rotate: -3.77 },
animationOptions
),
reversed: animation(
'#oss-youtube',
{ x: 0, y: '0vh', rotate: -3.77 },
animationOptions
)
}
},
{
mobile: {
main: animation(
'#oss-commits',
{ x: 0, y: [1200, -4], rotate: -1 },
animationOptions
),
reversed: animation('#oss-commits', { y: 1200, x: 4, rotate: -1 }, animationOptions)
},
desktop: {
main: animation(
'#oss-commits',
{ x: 100, y: '-70vh', rotate: -10.2 },
animationOptions
),
reversed: animation(
'#oss-commits',
{ x: 0, y: '0vh', rotate: -10.2 },
animationOptions
)
}
}
];
function isMobile(): boolean {
if (typeof window === 'undefined') return false;
return window?.innerWidth < 1024;
}
const animScale: Scale = [0, animations.length - 1];
const percentScale: Scale = [0.1, 0.8];
const scrollHandler = createScrollHandler(
animations.map(({ mobile, desktop }, i) => {
return {
percentage: isMobile() ? toScale(i, animScale, percentScale) : 0.1,
whenAfter() {
const { main, reversed } = isMobile() ? mobile : desktop;
main.play();
return reversed.play;
}
};
})
);
</script>
<div
id="open-source"
use:scroll
on:web-scroll={({ detail }) => {
const { percentage } = detail;
scrollHandler(percentage);
}}
on:web-resize={({ detail }) => {
scrollHandler.reset();
const { percentage } = detail;
scrollHandler(percentage);
}}
>
<div class="sticky-wrapper">
<h3 class="text-display font-aeonik-pro text-primary">Powered by Open Source</h3>
<div class="cards-wrapper">
<a
href={SOCIAL_STATS.DISCORD.LINK}
target="_blank"
rel="noopener noreferrer"
class="web-card is-white web-u-min-block-size-320 oss-card flex flex-col"
id="oss-discord"
>
<div class="flex flex-col justify-between gap-8">
<span
class="web-icon-discord web-u-font-size-40"
aria-hidden="true"
aria-label="Discord"
></span>
</div>
<div class="text-title font-aeonik-pro mt-auto">
{SOCIAL_STATS.DISCORD.STAT} Discord Members
</div>
</a>
<a
class="web-card is-white web-u-min-block-size-320 oss-card flex flex-col"
id="oss-github"
href={SOCIAL_STATS.GITHUB.LINK}
>
<div class="flex flex-col justify-between gap-8">
<span
class="web-icon-github web-u-font-size-40"
aria-hidden="true"
aria-label="GitHub"
></span>
</div>
<div class="text-title font-aeonik-pro mt-auto">
{SOCIAL_STATS.GITHUB.STAT} GitHub Stars
</div>
</a>
<a
href={SOCIAL_STATS.TWITTER.LINK}
class="web-card is-white web-u-min-block-size-320 oss-card flex flex-col"
id="oss-twitter"
>
<div class="flex flex-col justify-between gap-8">
<span
class="web-icon-x web-u-font-size-40"
aria-hidden="true"
aria-label="Twitter"
></span>
</div>
<div class="text-title font-aeonik-pro mt-auto">
{SOCIAL_STATS.TWITTER.STAT} Twitter Followers
</div>
</a>
<a
href={SOCIAL_STATS.YOUTUBE.LINK}
class="web-card is-white web-u-min-block-size-320 oss-card flex flex-col"
id="oss-youtube"
>
<div class="flex flex-col justify-between gap-8">
<span
class="web-icon-youtube web-u-font-size-40"
aria-hidden="true"
aria-label="YouTube"
></span>
</div>
<div class="text-title font-aeonik-pro mt-auto">
{SOCIAL_STATS.YOUTUBE.STAT} Youtube Subscribers
</div>
</a>
<a
class="web-card is-white web-u-min-block-size-320 oss-card flex flex-col"
id="oss-commits"
href={SOCIAL_STATS.GITHUB.LINK}
>
<div class="flex flex-col justify-between gap-8">
<span
class="web-icon-github web-u-font-size-40"
aria-hidden="true"
aria-label="GitHub"
></span>
</div>
<div class="text-title font-aeonik-pro mt-auto">
{SOCIAL_STATS.GITHUB.EXTRA?.COMMITS} Code Commits
</div>
</a>
</div>
</div>
</div>
<style lang="scss">
#open-source {
min-height: 150vh;
height: 3500px;
position: relative;
@media (min-width: 1024px) {
height: 1500px;
}
}
.sticky-wrapper {
position: sticky;
top: -15vh;
overflow: hidden;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 1.25rem;
padding-inline: 1.25rem;
width: 100%;
height: 130vh;
text-align: center;
&::after {
background: linear-gradient(
to top,
hsl(var(--web-color-background)) 0%,
hsl(var(--web-color-background) / 0) 5%
);
position: absolute;
inset: 0;
}
.cards-wrapper {
position: relative;
height: 22.5rem;
margin-top: 80px;
}
@media (min-width: 1024px) {
h3 {
max-width: 61.375rem;
}
.cards-wrapper {
position: absolute;
height: 100vh;
width: clamp(1024px, 90vw, 1440px);
top: 0;
left: 50%;
transform: translateX(-50%);
margin-top: 0;
}
}
}
.oss-card {
background: linear-gradient(
106deg,
rgba(255, 255, 255, 0.72) 0%,
rgba(255, 255, 255, 0.8) 41.9%,
rgba(255, 255, 255, 0.6) 100%
);
backdrop-filter: blur(10px);
--card-padding: 2rem;
--w: clamp(306px, 50vw, 22.125rem);
--h: 20.125rem;
width: var(--w);
height: var(--h);
text-align: left;
display: flex;
flex-direction: column;
justify-content: space-between;
position: absolute;
left: calc(50% - calc(var(--w) / 2));
transform: translateX(-1200px);
[class*='icon'] {
opacity: 48%;
}
}
@media (min-width: 1024px) {
.oss-card {
left: unset;
transform: unset;
}
#oss-discord {
bottom: -400px;
left: 1%;
transform: rotate(15deg);
}
#oss-github {
bottom: -400px;
left: 19%;
}
#oss-twitter {
bottom: -400px;
left: clamp(20%, 22vw, 29%);
}
#oss-youtube {
bottom: -400px;
left: clamp(64%, calc(1024px - 10vw), 70%);
}
#oss-commits {
bottom: -400px;
right: 10%;
}
}
</style>

View File

@@ -1,9 +0,0 @@
<script lang="ts">
export let id: string | undefined = undefined;
</script>
<div class="phone" {id}>
<div class="inner">
<slot />
</div>
</div>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 111 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 120 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 130 KiB

View File

@@ -1,55 +0,0 @@
<script lang="ts">
import AutoHeight from '../AutoBox.svelte';
</script>
<div class="anim-box">
<div class="top"><slot name="top" /></div>
<div class="content">
<AutoHeight>
<slot />
</AutoHeight>
</div>
</div>
<style lang="scss">
@use '$scss/abstract/mixins/border-gradient' as gradients;
.anim-box {
@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%
);
border-radius: var(--m-border-radius);
background: hsl(var(--web-color-card));
backdrop-filter: blur(8px);
padding: 0.5rem;
padding-block-start: 0;
text-align: left;
.top {
color: var(--greyscale-50, #ededf0);
font-family: Aeonik Pro;
font-size: 1.25rem;
font-style: normal;
font-weight: 400;
line-height: 2rem; /* 160% */
letter-spacing: -0.0125rem;
padding: 1rem;
text-align: left;
}
.content {
border-radius: 0.75rem;
background: rgba(255, 255, 255, 0.04);
backdrop-filter: blur(30px);
position: relative;
}
}
</style>

View File

@@ -1,695 +0,0 @@
<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>

View File

@@ -1,218 +0,0 @@
<script>
import { objectKeys } from '$lib/utils/object';
import { infos } from './Products.svelte';
</script>
<div class="outside">
<div class="wrapper">
<span class="web-badges text-micro !text-white uppercase">Products_</span>
<h2 class="text-display font-aeonik-pro text-primary mt-4">
Your backend, minus the hassle
</h2>
<p class="text-description mt-4">
Build secure and scalable applications with less code. Add authentication, databases,
storage, and more using Appwrite's development platform.
</p>
<div class="infos">
{#each objectKeys(infos) as prod, i}
{@const info = infos[prod]}
{@const isLast = i === objectKeys(infos).length - 1}
{#if info}
<div class="info">
<h3>
<img src={info.icon.active} alt="" />
<span class="text-label text-primary">{info.title}</span>
</h3>
<h4 class="text-title font-aeonik-pro">{info.subtitle}</h4>
<p>
{info.description}
</p>
<ul class="features">
{#each info.features as feature}
<li>{feature}</li>
{/each}
</ul>
{#if info.shot}
<enhanced:img class="img" src={info.shot} alt="" />
{/if}
</div>
{#if !isLast}
<hr />
{/if}
{/if}
{/each}
</div>
<div class="post-wrapper">
<img src="/images/products/post.png" alt="" />
<h2>See your products grow</h2>
<p>
Keep track of your projects progress on the Appwrite Console and see them grow into
products users love and use every day.
</p>
</div>
</div>
<div class="img-overlay"></div>
</div>
<style lang="scss">
.outside {
position: relative;
overflow: hidden;
display: none;
.img-overlay {
content: '';
background: linear-gradient(to bottom, transparent 0%, black 40%);
position: absolute;
bottom: 0;
width: 100vw;
height: 30rem;
z-index: 10;
}
}
@media (max-width: 1399px) {
.outside {
display: block;
}
}
.wrapper {
--padding-inline: 1.25rem;
padding-block-start: 5rem;
padding-inline: var(--padding-inline);
max-width: 600px;
margin-inline: auto;
}
.infos {
margin-block-start: 3rem;
display: flex;
flex-direction: column;
gap: 3rem;
.info {
h3 {
display: flex;
align-items: center;
gap: 0.75rem;
}
h4 {
color: hsl(var(--web-color-primary));
margin-block-start: 0.75rem;
}
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;
}
}
}
.img {
inline-size: 100%;
block-size: auto;
margin-block-start: 2.5rem;
}
}
hr {
border: 1px solid hsl(var(--web-color-smooth));
margin-inline: calc(var(--padding-inline) * -1);
}
}
.post-wrapper {
display: flex;
flex-direction: column;
align-items: center;
overflow: visible;
position: relative;
width: 100%;
/* overflow: hidden; */
padding-block-start: 35rem;
padding-block-end: 5rem;
img {
display: block;
max-block-size: unset;
max-inline-size: unset;
top: 5rem;
left: 50%;
transform: translateX(-50%);
width: 37.5rem;
position: absolute;
}
h2 {
color: var(--greyscale-50, #ededf0);
text-align: center;
/* Responsive/Display */
font-family: Aeonik Pro;
font-size: 48px;
font-style: normal;
font-weight: 400;
line-height: 50px; /* 104.167% */
letter-spacing: -0.48px;
max-width: 20rem;
position: relative;
z-index: 100;
}
p {
color: var(--greyscale-400, #97979b);
text-align: center;
font-family: Inter;
font-size: 16px;
font-style: normal;
font-weight: 500;
line-height: 22px; /* 137.5% */
letter-spacing: -0.072px;
margin-block-start: 1rem;
max-width: 20rem;
z-index: 100;
}
}
</style>

View File

@@ -1,61 +0,0 @@
<script lang="ts">
import { createCheckbox, melt } from '@melt-ui/svelte';
export let checked = false;
const {
elements: { root },
states: { checked: localChecked },
helpers: { isChecked }
} = createCheckbox({
onCheckedChange({ next }) {
if (typeof next === 'boolean') {
checked = next;
}
return next;
}
});
$: localChecked.set(checked);
</script>
<div class="wrapper">
<button use:melt={$root} class="anim-checkbox">
{#if $isChecked}
<span class="web-icon-check"></span>
{/if}
</button>
</div>
<style lang="scss">
.wrapper {
display: grid;
place-items: center;
}
.anim-checkbox {
width: 1rem;
height: 1rem;
flex-shrink: 0;
border-radius: 0.125rem;
border: 1.5px solid var(--greyscale-500, #818186);
position: relative;
&:global(.anim-checkbox[data-state='checked']) {
background-color: #7c67fe;
border-color: #7c67fe;
}
}
[class*='icon-'] {
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
color: white;
font-size: 1rem;
}
</style>

View File

@@ -1,64 +0,0 @@
<script lang="ts">
import { getInitials } from '$lib/animations';
import { fly } from 'svelte/transition';
import { authController } from '.';
import { flip } from '$lib/utils/flip';
const { state } = authController;
type AuthEntry = {
avatar: string;
name: string;
email: string;
id: number;
};
$: authData = [
$state.submitted
? {
avatar: getInitials($state.name),
name: $state.name,
email: $state.email,
id: 0
}
: undefined,
{
avatar: 'BD',
name: 'Benjamin Davis',
email: 'benjamin.davis@example.com',
id: 1
},
{
avatar: 'OS',
name: 'Olivia Smith',
email: 'olivia.smith@example.com',
id: 2
},
{
avatar: 'EW',
name: 'Ethan Wilson',
email: 'ethan.wilson@example.com',
id: 3
}
].filter(Boolean) as AuthEntry[];
</script>
<div class="pseudo-table">
<div class="header">
<span class="text-micro uppercase">Name</span>
<span class="text-micro uppercase">Identifier</span>
</div>
{#each authData as user (user.id)}
<div
class="row"
in:fly={{ duration: 100, x: -16, delay: 100 }}
out:fly={{ duration: 100, x: -16 }}
animate:flip={{ duration: 150 }}
>
<div class="flex items-center gap-3">
<div class="avatar is-size-small">{user.avatar}</div>
<span class="truncated">{user.name}</span>
</div>
<span class="truncated">{user.email}</span>
</div>
{/each}
</div>

View File

@@ -1,16 +0,0 @@
<script lang="ts">
import Code from '$lib/animations/CodeWindow/Code.svelte';
import { authController } from '.';
const { state } = authController;
$: content = `
const result = account.create(
ID.unique(),
'${$state.email}',
'${$state.password}',
"${$state.name}"
);`.trim();
</script>
<Code {content} />

View File

@@ -1,81 +0,0 @@
<script lang="ts">
import { Switch } from '$lib/components';
import { objectKeys } from '$lib/utils/object';
import { authController } from '.';
const { state } = authController;
const getIcon = (provider: string) => {
return `web-icon-${provider.toLowerCase()}`;
};
</script>
<div class="auth-controls">
{#each objectKeys($state.controls) as provider, i}
{@const isLast = i === objectKeys($state.controls).length - 1}
<div>
<span class={getIcon(provider)}></span>
<span>{provider}</span>
<Switch bind:checked={$state.controls[provider]} />
</div>
{#if !isLast}
<div class="sep"></div>
{/if}
{/each}
</div>
<style lang="scss">
.auth-controls {
display: flex;
flex-direction: column;
padding: 0.75rem;
width: 12.5rem;
> div {
display: flex;
align-items: center;
> :nth-child(2) {
margin-left: 0.75rem;
color: hsl(var(--web-color-white));
font-family: Inter;
font-size: 0.875rem;
font-style: normal;
font-weight: 500;
line-height: 1.375rem; /* 157.143% */
letter-spacing: -0.00394rem;
}
> :global(:nth-child(3)) {
margin-left: auto;
}
}
.sep {
width: 100%;
height: 1px;
background-color: rgba(255, 255, 255, 0.12);
margin-block: 0.5rem;
}
[class*='icon-'] {
--size: 2rem;
font-size: var(--size);
width: var(--size);
height: var(--size);
color: hsl(var(--web-color-greayscale-50));
position: relative;
&::before {
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
}
}
}
</style>

View File

@@ -1,95 +0,0 @@
import Box from './box.svelte';
import Code from './code.svelte';
import Controls from './controls.svelte';
import Phone from './phone.svelte';
export const Auth = {
Phone,
Box,
Code,
Controls
};
import { safeAnimate, sleep, write } from '$lib/animations';
import { createResettable } from '$lib/utils/resettable';
import { getElSelector } from '../Products.svelte';
type State = {
email: string;
password: string;
name: string;
showControls: boolean;
submitted: boolean;
controls: {
GitHub: boolean;
Google: boolean;
Apple: boolean;
Microsoft: boolean;
};
};
const state = createResettable<State>({
email: '',
password: '',
name: "Walter O'Brien",
showControls: false,
submitted: false,
controls: {
GitHub: true,
Google: false,
Apple: false,
Microsoft: false
}
});
const emailToSet = 'walterobrian@example.com';
const passwordToSet = 'password';
const execute = async () => {
const phone = getElSelector('phone');
const box = getElSelector('box');
const code = getElSelector('code');
const controls = getElSelector('controls');
// Reset
const { update } = state.reset();
await Promise.all([
safeAnimate(box, { x: 310, y: 140, opacity: 0 }, { duration: 0.5 })?.finished,
safeAnimate(code, { x: 200, y: 460, opacity: 0 }, { duration: 0.5 })?.finished,
safeAnimate(phone, { x: 0, y: 0 }, { duration: 0.5 })?.finished,
safeAnimate(controls, { x: 420, y: 0, opacity: 0 }, { duration: 0.5 })?.finished
]);
// Start
await safeAnimate(box, { y: [48, 140], opacity: 1 }, { duration: 0.25, delay: 0.25 })?.finished;
await sleep(50);
await write(emailToSet, (v) => update((p) => ({ ...p, email: v })), 300);
await sleep(50);
await write(passwordToSet, (v) => update((p) => ({ ...p, password: v })), 300);
await sleep(50);
await safeAnimate(
code,
{ x: [200, 200], y: [460 + 16, 460], opacity: [0, 1] },
{ duration: 0.25 }
)?.finished;
await sleep(350);
update((p) => ({ ...p, submitted: true }));
await sleep(1000);
update((p) => ({ ...p, showControls: true }));
safeAnimate(controls, { x: [420, 420], y: [16, 0], opacity: 1 }, { duration: 0.5 });
};
export const authController = {
execute,
state
};

View File

@@ -1,220 +0,0 @@
<script lang="ts">
import { fade } from 'svelte/transition';
import { authController } from '.';
import { objectKeys } from '$lib/utils/object';
import { flip } from '$lib/utils/flip';
const { state } = authController;
$: controlsEnabled = $state.showControls && Object.values($state.controls).some(Boolean);
</script>
<div data-theme-ignore class="inner-phone light">
<p class="title">Create an Account</p>
<p class="subtitle">Please enter your details</p>
<div class="inputs">
<fieldset>
<label for="name">Your Name</label>
<input type="name" id="name" placeholder="Enter your name" bind:value={$state.name} />
</fieldset>
<fieldset>
<label for="email">Your Email</label>
<input
type="email"
id="email"
placeholder="Enter your email"
bind:value={$state.email}
/>
</fieldset>
<fieldset>
<label for="password">Create Password</label>
<input
type="password"
id="password"
placeholder="Enter Password"
bind:value={$state.password}
/>
</fieldset>
</div>
<button class="sign-up">Sign Up</button>
{#if controlsEnabled}
<span class="with-sep" transition:fade={{ duration: 100 }}>or sign up with</span>
<div class="oauth-btns" transition:fade={{ duration: 100 }}>
{#each objectKeys($state.controls).filter((p) => $state.controls[p]) as provider (provider)}
<button
class="oauth"
transition:fade={{ duration: 100 }}
animate:flip={{ duration: 250 }}
>
<div class="inner">
<span class="web-icon-{provider.toLowerCase()}"></span>
<span>{provider}</span>
</div>
</button>
{/each}
</div>
{/if}
</div>
<style lang="scss">
.inner-phone {
padding-block: 3rem;
padding-inline: 1rem;
color: rgba(67, 67, 71, 1);
text-align: left;
.title {
color: #434347;
font-family: Inter;
font-size: 16px;
font-style: normal;
font-weight: 600;
line-height: 22px; /* 137.5% */
letter-spacing: -0.224px;
}
.subtitle {
color: var(--greyscale-700, var(--color-greyscale-700, #56565c));
font-family: Inter;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 20px; /* 142.857% */
letter-spacing: -0.196px;
}
.inputs {
display: flex;
flex-direction: column;
gap: 0.75rem;
margin-block-start: 1.5rem;
fieldset {
display: flex;
flex-direction: column;
gap: 0.3125rem;
width: 100%;
label {
color: var(--color-greyscale-700, #56565c);
font-family: Inter;
font-size: 12px;
font-style: normal;
font-weight: 400;
line-height: 16px; /* 133.333% */
letter-spacing: -0.168px;
}
input {
all: unset;
display: flex;
padding: 8px 12px;
align-items: flex-start;
align-self: stretch;
border-radius: 8px;
border: 1px solid #d8d8db;
color: #434347;
font-family: Inter;
font-size: 12px;
font-style: normal;
font-weight: 400;
line-height: 16px; /* 133.333% */
letter-spacing: -0.168px;
}
}
}
.sign-up {
padding: 0.375rem 0.75rem;
text-align: center;
width: 100%;
margin-block-start: 1.25rem;
border-radius: 0.5rem;
background: var(--appwrite-purple, #7c67fe);
box-shadow: 0px 4px 8px 0px rgba(0, 0, 0, 0.06);
color: var(--color-bw-white, #fff);
text-align: center;
/* Responsive/SubBody-500 */
font-family: Inter;
font-size: 14px;
font-style: normal;
font-weight: 500;
line-height: 22px; /* 157.143% */
letter-spacing: -0.07px;
}
.with-sep {
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
font-family: Inter;
font-size: 0.75rem;
font-style: normal;
font-weight: 400;
line-height: 1.25rem; /* 166.667% */
letter-spacing: -0.0105rem;
color: hsl(var(--web-color-greyscale-500));
margin-block-start: 0.75rem;
&::before,
&::after {
content: '';
height: 1px;
flex-grow: 1;
background-color: hsl(var(--web-color-greyscale-200));
}
}
.oauth-btns {
--gap: 0.5rem;
display: flex;
flex-wrap: wrap;
gap: var(--gap);
margin-block-start: 0.75rem;
}
.oauth {
display: flex;
justify-content: center;
align-items: center;
border-radius: 0.5rem;
border: 1px solid #d9d9d9;
color: hsl(var(--web-color-greyscale-750));
text-align: center;
/* Responsive/Caption-500 */
font-family: Inter;
font-size: 0.875rem;
font-style: normal;
font-weight: 500;
line-height: 1.375rem; /* 157.143% */
letter-spacing: -0.01575rem;
flex: 1 1 calc(50% - var(--gap));
padding-block: 0.375rem;
position: relative;
height: 2.125rem;
.inner {
position: absolute;
left: 50%;
top: 50%;
display: flex;
justify-content: center;
align-items: center;
gap: 0.5rem;
transform: translate(-50%, -50%) scale(var(--inverse-sx, 1), var(--inverse-sy, 1));
}
}
}
</style>

View File

@@ -1,54 +0,0 @@
<script lang="ts">
import { slide } from 'svelte/transition';
import { databasesController } from '.';
import { flip } from '$lib/utils/flip';
const { state } = databasesController;
</script>
<div class="pseudo-table">
<div class="header">
<span class="text-micro uppercase">Document ID</span>
<span class="text-micro uppercase">Task</span>
</div>
{#each $state.tasks.slice(0, $state.tableSlice) as task (task.id)}
<div class="row" transition:slide={{ duration: 150 }} animate:flip={{ duration: 150 }}>
<div class="copy-button">
<span class="web-icon-copy"></span>
<span>{task.id}</span>
</div>
<span class="truncated">{task.title}</span>
</div>
{/each}
</div>
<style lang="scss">
.copy-button {
display: flex;
padding: 0.25rem 0.5rem;
align-items: center;
gap: 0.375rem;
border-radius: 62.4375rem;
border: 1px solid rgba(255, 255, 255, 0.08);
background: rgba(255, 255, 255, 0.04);
backdrop-filter: blur(2.6666667461395264px);
[class*='icon-'] {
font-size: 1.25rem;
color: hsl(var(--web-color-greyscale-600));
}
span:not([class*='icon-']) {
color: var(--greyscale-400, #adadb1);
font-family: Inter;
font-size: 0.875rem;
font-style: normal;
font-weight: 400;
line-height: 1.25rem; /* 142.857% */
overflow: hidden;
text-overflow: ellipsis;
}
}
</style>

View File

@@ -1,16 +0,0 @@
<script lang="ts">
import Code from '$lib/animations/CodeWindow/Code.svelte';
let content = `
const result = databases.createDocument(
'Your-tasks',
tasks,
ID.unique(),
{
'description': 'Research user needs',
'tags': ['UX', 'design'],
}
);`.trim();
</script>
<Code {content} />

View File

@@ -1,94 +0,0 @@
import Box from './box.svelte';
import Code from './code.svelte';
import Phone from './phone.svelte';
import { safeAnimate, sleep } from '$lib/animations';
import { createResettable } from '$lib/utils/resettable';
import { getElSelector } from '../Products.svelte';
type Task = {
id: string;
title: string;
checked: boolean;
};
type State = {
tasks: Task[];
tableSlice: number;
};
const state = createResettable<State>({
tasks: [
{
id: '3397fecdedb13397fecdedb1',
title: 'Research user needs',
checked: true
}
],
tableSlice: 1
});
const execute = async () => {
const phone = getElSelector('phone');
const box = getElSelector('box');
const code = getElSelector('code');
const { update } = state.reset();
await Promise.all([
safeAnimate(phone, { x: 390, y: 0 }, { duration: 0.5 })?.finished,
safeAnimate(box, { x: 0, y: 32, opacity: 1 }, { duration: 0.5 })?.finished,
safeAnimate(code, { x: 80, y: 320, opacity: 1 }, { duration: 0.5 })?.finished
]);
await sleep(250);
update((p) => ({
...p,
tasks: [
...p.tasks,
{
id: '3397fecdedb13397fecdedb2',
title: 'Create wireframes',
checked: false
}
]
}));
await sleep(250);
update((p) => ({
...p,
tableSlice: p.tableSlice + 1
}));
await sleep(250);
update((p) => ({
...p,
tasks: [
...p.tasks,
{
id: '3397fecdedb13397fecdedb3',
title: 'Create visual design',
checked: false
}
]
}));
await sleep(250);
update((p) => ({
...p,
tableSlice: p.tableSlice + 1
}));
};
export const databasesController = {
execute,
state
};
export const Databases = {
Phone,
Box,
Code
};

View File

@@ -1,127 +0,0 @@
<script lang="ts">
import { fly } from 'svelte/transition';
import { databasesController } from '.';
import TaskCheckbox from '../TaskCheckbox.svelte';
const { state } = databasesController;
</script>
<div data-theme-ignore class="inner-phone light">
<div class="header">
<p class="title">Your tasks</p>
<span class="icon-menu" aria-label="menu"></span>
</div>
<div class="date">Today</div>
<div class="tasks">
{#each $state.tasks as task (task.id)}
<div class="task" data-checked={task.checked ? '' : undefined} in:fly={{ x: -16 }}>
<TaskCheckbox bind:checked={task.checked} />
<span class="title">{task.title}</span>
</div>
{/each}
</div>
<div class="add-btn">
<span class="web-icon-plus"></span>
</div>
</div>
<style lang="scss">
.inner-phone {
padding-block: 3rem;
padding-inline: 1rem;
color: rgba(67, 67, 71, 1);
text-align: left;
position: relative;
height: 100%;
.header {
display: flex;
justify-content: space-between;
align-items: center;
.title {
color: var(--color-greyscale-800, #2d2d31);
font-family: Inter;
font-size: 1rem;
font-style: normal;
font-weight: 600;
line-height: 1.375rem; /* 137.5% */
letter-spacing: -0.014rem;
}
[class*='icon-'] {
font-size: 1.25rem;
color: hsl(var(--web-color-greyscale-500));
}
}
.date {
margin-block-start: 3rem;
color: hsl(var(--web-color-greyscale-600));
font-family: Inter;
font-size: 0.75rem;
font-style: normal;
font-weight: 500;
line-height: 1.25rem; /* 166.667% */
}
.tasks {
margin-block-start: 0.5rem;
display: flex;
flex-direction: column;
gap: 0.5rem;
.task {
display: flex;
align-items: center;
gap: 0.75rem;
border-radius: 0.5rem;
border: 1px solid hsl(var(--web-color-greyscale-50));
background: hsl(var(--web-color-white));
color: var(--greyscale-700, var(--color-greyscale-700, #56565c));
padding-block: 0.55rem;
padding-inline: 0.88rem;
/* Responsive/SubBody-400 */
font-family: Inter;
font-size: 0.875rem;
font-style: normal;
font-weight: 400;
line-height: 1.375rem; /* 157.143% */
letter-spacing: -0.00394rem;
transition: opacity 200ms ease;
&[data-checked] {
opacity: 0.6;
}
}
}
.add-btn {
position: absolute;
right: 1rem;
bottom: 2.5rem;
width: 2.5rem;
height: 2.5rem;
flex-shrink: 0;
box-shadow: 0px 2px 6px 0px rgba(0, 0, 0, 0.12);
background-color: rgba(124, 103, 254, 1);
color: rgba(237, 237, 240, 1);
font-size: 1.5rem;
display: grid;
place-items: center;
border-radius: 100%;
}
}
</style>

View File

@@ -1,56 +0,0 @@
<script lang="ts">
import { portal } from '$lib/actions';
import Code from '$lib/animations/CodeWindow/Code.svelte';
import { fade } from 'svelte/transition';
import { functionsController } from '.';
let content = `
const userId = req.headers['user-id'];
if (req.path === '/subscribe') {
const session = await stripe.checkout(userId);
return res.redirect(session.url, 303);
}
if (req.path === '/webhook') {
await appwrite.addSubscriberLabel(userId);
}
return res.json({ success: true });`.trim();
const { state } = functionsController;
</script>
<Code {content} />
<div use:portal={{ target: '#code-bottom' }} class="bottom">
{#if $state.submit !== 'idle'}
<span class="web-icon-github" in:fade></span>
{/if}
{#if $state.submit === 'loading'}
<span in:fade>Pushing to GitHub...</span>
<div class="loader is-small" in:fade></div>
{:else if $state.submit === 'success'}
<span>Deployed to Appwrite Cloud</span>
<span class="web-icon-check"></span>
{/if}
</div>
<style lang="scss">
.bottom {
display: flex;
align-items: center;
gap: 0.5rem;
height: 3rem;
padding-inline: 1rem;
color: var(--color-bw-white, #fff);
font-family: Inter;
font-size: 0.875rem;
font-style: normal;
font-weight: 400;
line-height: 150%; /* 1.3125rem */
letter-spacing: -0.00875rem;
}
</style>

View File

@@ -1,58 +0,0 @@
import Code from './code.svelte';
import Phone from './phone.svelte';
import { safeAnimate, sleep } from '$lib/animations';
import { createResettable } from '$lib/utils/resettable';
import { getElSelector } from '../Products.svelte';
type State = {
submit: 'idle' | 'loading' | 'success';
};
const state = createResettable<State>({
submit: 'idle'
});
const execute = async () => {
const phone = getElSelector('phone');
const box = getElSelector('box');
const code = getElSelector('code');
const { update } = state.reset();
await Promise.all([
safeAnimate(phone, { x: 430, y: 0, width: '275px' }, { duration: 0.5 })?.finished,
safeAnimate(code, { x: 0, y: 200, opacity: 0 }, { duration: 0.5 })?.finished,
safeAnimate(box, { opacity: 0 }, { duration: 0.5 })?.finished
]);
await sleep(250);
await safeAnimate(code, { zIndex: 0 }, { duration: 0 })?.finished;
await safeAnimate(code, { y: [200 - 16, 200], opacity: 1 }, { duration: 0.5 })?.finished;
await sleep(250);
update((p) => ({
...p,
submit: 'loading'
}));
await sleep(1500);
update((p) => ({
...p,
submit: 'success'
}));
};
export const functionsController = {
execute,
state
};
export const Functions = {
Phone,
Code
};

View File

@@ -1,325 +0,0 @@
<script lang="ts">
import { flip } from '$lib/utils/flip';
import { scale, slide } from 'svelte/transition';
import { functionsController } from '.';
const { state } = functionsController;
type Method = {
icon: string;
label: string;
};
$: methods = [
$state.submit === 'success' && {
icon: '/images/animations/stripe.png',
label: 'Stripe'
},
{
icon: '/images/animations/credit-card.svg',
label: 'Card'
},
{
icon: '/images/animations/paypal.svg',
label: 'PayPal'
},
{
icon: '/images/animations/apple.svg',
label: 'Apple'
}
].filter(Boolean) as Method[];
</script>
<div data-theme-ignore class="inner-phone light">
<div class="header">
<p class="title">Upgrade plan</p>
<span class="icon-menu" aria-label="menu"></span>
</div>
<div class="plan">
<p class="title">Premium plan</p>
<div class="subscription">
<p class="price">$20</p>
<p class="period">/month</p>
</div>
<ul>
<li>Premium plan</li>
<li>Premium plan</li>
<li>Premium plan</li>
</ul>
</div>
<ul class="methods">
{#each methods as method, i (method.label)}
<li
in:scale={{ delay: 150 }}
animate:flip={{ duration: 500 }}
data-active={i == 0 ? '' : undefined}
>
<img src={method.icon} alt="" />
<p>{method.label}</p>
</li>
{/each}
</ul>
{#if $state.submit !== 'success'}
<div class="form">
<p>Card information</p>
<div class="bordered">
<div>
<p>placeholder</p>
<img src="/images/animations/visa.png" alt="" />
<img src="/images/animations/mastercard.png" alt="" />
</div>
<div>
<p>MM/YY</p>
<p>CVV</p>
</div>
</div>
</div>
{/if}
<button>
Pay $20.00
{#if $state.submit === 'success'}
<span in:slide={{ axis: 'x' }}>on Stripe</span>
{/if}
</button>
</div>
<style lang="scss">
.inner-phone {
padding-block: 3rem 1.5rem;
padding-inline: 1rem;
color: rgba(67, 67, 71, 1);
text-align: left;
position: relative;
height: 100%;
overflow: visible;
display: flex;
flex-direction: column;
.header {
display: flex;
justify-content: space-between;
align-items: center;
.title {
color: #434347;
font-family: Inter;
font-size: 1rem;
font-style: normal;
font-weight: 600;
line-height: 1.375rem; /* 137.5% */
letter-spacing: -0.014rem;
}
[class*='icon-'] {
font-size: 1.25rem;
color: hsl(var(--web-color-greyscale-500));
}
}
.plan {
display: flex;
padding: 0.75rem 1rem;
flex-direction: column;
justify-content: center;
align-items: flex-start;
border-radius: 0.75rem;
background: rgba(237, 237, 240, 0.5);
margin-block-start: 1rem;
.title {
color: var(--color-greyscale-700, #56565c);
font-family: Inter;
font-size: 0.75rem;
font-style: normal;
font-weight: 700;
line-height: 1.25rem; /* 166.667% */
}
.subscription {
display: flex;
align-items: baseline;
margin-block-start: 0.15rem;
.price {
color: var(--greyscale-700, var(--color-greyscale-700, #56565c));
font-family: Inter;
font-size: 1.125rem;
font-style: normal;
font-weight: 700;
line-height: 1.25rem; /* 111.111% */
}
.period {
color: var(--greyscale-700, var(--color-greyscale-700, #56565c));
font-family: Inter;
font-size: 0.75rem;
font-style: normal;
font-weight: 400;
line-height: 1.25rem;
}
}
ul {
display: flex;
flex-direction: column;
gap: 0.125rem;
margin-block-start: 0.75rem;
li {
color: var(--color-greyscale-500, #818186);
font-family: Inter;
font-size: 0.75rem;
font-style: normal;
font-weight: 400;
line-height: 1.25rem; /* 166.667% */
padding-inline-start: 1.5rem;
position: relative;
&::before {
content: '';
display: block;
position: absolute;
left: 0;
width: 1rem;
height: 1rem;
top: 50%;
transform: translateY(-50%);
background-image: url('/images/animations/check-circle.svg');
}
}
}
}
.methods {
margin-block-start: 1.25rem;
display: flex;
gap: 0.75rem;
overflow: hidden;
margin-inline: -1rem;
padding-inline: 1rem;
li {
flex-shrink: 0;
display: flex;
width: 5.5rem;
padding: 0.75rem 0.75rem 0.625rem 0.75rem;
flex-direction: column;
justify-content: center;
align-items: flex-start;
gap: 0.125rem;
border-radius: 0.75rem;
border: 1px solid var(--greyscale-50, #ededf0);
color: var(--greyscale-700, var(--color-greyscale-700, #56565c));
font-family: Inter;
font-size: 0.75rem;
font-style: normal;
font-weight: 400;
line-height: 1.25rem; /* 166.667% */
&[data-active] {
border-color: var(--appwrite-purple, #7c67fe);
}
}
}
.form {
margin-block-start: 1.25rem;
> p {
color: var(--dark-neutrals-150, #373b4d);
font-family: Inter;
font-size: 0.75rem;
font-style: normal;
font-weight: 400;
line-height: 150%; /* 1.125rem */
}
.bordered {
margin-block-start: 0.25rem;
border-radius: 0.5rem;
border: 1px solid #ededf0;
background: var(--color-bw-white, #fff);
> div:first-child {
display: flex;
gap: 0.25rem;
padding: 0.65rem 0.75rem;
border-bottom: 1px solid #ededf0;
> :nth-child(2) {
margin-inline-start: auto;
}
}
> div:nth-child(2) {
display: flex;
> p {
padding: 0.65rem 0.75rem;
}
> p:first-child {
flex-grow: 3;
}
> p:last-child {
flex-grow: 1;
border-inline-start: 1px solid #ededf0;
}
}
p {
color: var(--light-neutrals-50, #c4c6d7);
font-family: Inter;
font-size: 0.75rem;
font-style: normal;
font-weight: 400;
line-height: 150%; /* 1.125rem */
}
}
}
> button {
display: flex;
text-align: center;
padding: 0.5rem 1rem;
justify-content: center;
align-items: center;
gap: 0.25rem;
border-radius: 0.5rem;
background: var(--appwrite-purple, #7c67fe);
box-shadow: 0px 4px 8px 0px rgba(0, 0, 0, 0.06);
color: var(--color-bw-white, #fff);
text-align: center;
font-family: Inter;
font-size: 0.875rem;
font-style: normal;
font-weight: 500;
line-height: 1.375rem; /* 157.143% */
letter-spacing: -0.01225rem;
margin-block-start: auto;
white-space: nowrap;
}
}
</style>

View File

@@ -1,116 +0,0 @@
<script lang="ts">
import { fade, slide } from 'svelte/transition';
import { messagingController } from '.';
import { flip } from '$lib/utils/flip';
const { state } = messagingController;
</script>
<div class="pseudo-table">
<div class="header">
<span class="text-micro uppercase">Message ID</span>
<span class="text-micro uppercase">Type</span>
<span class="text-micro uppercase" style:text-align="center">Status</span>
</div>
{#each $state.messages.slice(0, $state.tableSlice) as task (task.id)}
<div class="row" transition:slide={{ duration: 150 }} animate:flip={{ duration: 150 }}>
<div class="copy-button">
<span class="web-icon-copy"></span>
<span>{task.id}</span>
</div>
<div class="icon-button">
<div class="icon">
<img src={task.icon} alt="" width="16" height="16" />
</div>
<span class="truncated">{task.type}</span>
</div>
<div class="status-indicator">
{#if task.status === 'sending'}
<div class="loader is-small" in:fade></div>
{:else}
<span class="web-icon-check"></span>
{/if}
</div>
</div>
{/each}
</div>
<style lang="scss">
.header,
.row {
grid-template-columns: 7rem 1fr 1fr !important;
gap: 1.5rem 3rem;
}
.copy-button {
display: flex;
padding: 0.25rem 0.5rem;
align-items: center;
justify-content: space-between;
gap: 0.375rem;
border-radius: 62.4375rem;
border: 1px solid rgba(255, 255, 255, 0.08);
background: rgba(255, 255, 255, 0.04);
backdrop-filter: blur(2.6666667461395264px);
[class*='icon-'] {
font-size: 1.25rem;
color: hsl(var(--web-color-greyscale-600));
}
span:not([class*='icon-']) {
color: var(--greyscale-400, #adadb1);
font-family: Inter;
font-size: 0.875rem;
font-style: normal;
font-weight: 400;
line-height: 1.25rem; /* 142.857% */
overflow: hidden;
text-overflow: ellipsis;
}
}
.icon-button {
display: flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
.icon {
border: 1px solid rgba(255, 255, 255, 0.08);
background: rgba(255, 255, 255, 0.04);
backdrop-filter: blur(2.6666667461395264px);
border-radius: 100%;
height: 2rem;
width: 2rem;
display: flex;
align-items: center;
justify-content: center;
}
span:not([class*='icon-']) {
color: var(--greyscale-400, #adadb1);
font-family: Inter;
font-size: 0.875rem;
font-style: normal;
font-weight: 400;
line-height: 1.25rem; /* 142.857% */
overflow: hidden;
text-overflow: ellipsis;
}
}
.status-indicator {
display: flex;
align-items: center;
justify-content: center;
[class*='icon-'] {
font-size: 1.25rem;
color: hsl(var(--web-color-greyscale-600));
}
}
</style>

View File

@@ -1,15 +0,0 @@
<script lang="ts">
import { messagingController } from '.';
import Code from '$lib/animations/CodeWindow/Code.svelte';
const { state } = messagingController;
$: content = `
await messaging.createPush(
ID.unique(),
'${$state.heading}',
'${$state.message}',
);`.trim();
</script>
<Code {content} />

View File

@@ -1,164 +0,0 @@
import Box from './box.svelte';
import Code from './code.svelte';
import Phone from './phone.svelte';
import { safeAnimate, sleep, write } from '$lib/animations';
import { createResettable } from '$lib/utils/resettable';
import { getElSelector } from '../Products.svelte';
type Task = {
id: string;
title: string;
checked: boolean;
};
type Message = {
id: string;
type: string;
icon: string;
status: 'sending' | 'sent';
};
type State = {
heading: string;
message: string;
tasks: Task[];
messages: Message[];
tableSlice: number;
submit: 'loading' | 'success';
};
const state = createResettable<State>({
heading: '',
message: '',
tasks: [
{
id: '3397fecdedb13397fecdedb1',
title: 'Research user needs',
checked: true
}
],
messages: [
{
id: '...3397fecdedb1',
type: 'SMS',
icon: './images/icons/illustrated/dark/sms.svg',
status: 'sent'
},
{
id: '...2224gabjger4',
type: 'Email',
icon: './images/icons/illustrated/dark/email.svg',
status: 'sent'
}
],
tableSlice: 2,
submit: 'loading'
});
const execute = async () => {
const phone = getElSelector('phone');
const box = getElSelector('box');
const code = getElSelector('code');
const { update } = state.reset();
await Promise.all([
safeAnimate(phone, { x: 365, y: 0, width: '275px' }, { duration: 0.5 })?.finished,
safeAnimate(code, { x: 80, y: 325, opacity: 0, zIndex: 100 }, { duration: 0.5 })?.finished,
safeAnimate(box, { x: 0, y: 32, opacity: 1 }, { duration: 0.5, delay: 1 })?.finished
]);
await sleep(250);
update((p) => ({
...p,
tasks: [
...p.tasks,
{
id: '3397fecdedb13397fecdedb2',
title: 'Create wireframes',
checked: false
}
]
}));
await sleep(250);
update((p) => ({
...p,
tableSlice: p.tableSlice + 1
}));
await sleep(250);
update((p) => ({
...p,
tasks: [
...p.tasks,
{
id: '3397fecdedb13397fecdedb3',
title: 'Create visual design',
checked: false
}
]
}));
await sleep(250);
update((p) => ({
...p,
tableSlice: p.tableSlice + 1
}));
await sleep(250);
safeAnimate(code, { opacity: 1 }, { duration: 0.5 })?.finished, await sleep(250);
await write(
'New task assigned to you',
(v) => {
state.update((n) => ({ ...n, heading: v }));
},
300
);
await write(
'You were assigned a new task in your board. Tap to check it out.',
(v) => {
state.update((n) => ({ ...n, message: v }));
},
300
);
await sleep(250);
update((p) => ({
...p,
messages: [
...p.messages,
{
id: '...5689fdoerre2',
type: 'Push',
icon: './images/icons/illustrated/dark/push.svg',
status: 'sending'
}
]
}));
await sleep(1250);
update((p) => ({
...p,
submit: 'success',
messages: p.messages.map((m) => (m.id === '...5689fdoerre2' ? { ...m, status: 'sent' } : m))
}));
};
export const messagingController = {
execute,
state
};
export const Messaging = {
Phone,
Box,
Code
};

View File

@@ -1,205 +0,0 @@
<script lang="ts">
import { fly } from 'svelte/transition';
import { messagingController } from '.';
import TaskCheckbox from '../TaskCheckbox.svelte';
const { state } = messagingController;
</script>
{#if $state.submit === 'success'}
<div class="push-notification" in:fly={{ y: -20 }}>
<div class="icon"></div>
<div class="content">
<div class="header">
<h3 class="title">New task assigned to you</h3>
<span class="time">now</span>
</div>
<p class="message">You were assigned a new task in your board. Tap to check it out.</p>
</div>
</div>
{/if}
<div data-theme-ignore class="inner-phone light">
<div class="header">
<p class="title">Your tasks</p>
<span class="icon-menu" aria-label="menu"></span>
</div>
<div class="date">Today</div>
<div class="tasks">
{#each $state.tasks as task (task.id)}
<div class="task" data-checked={task.checked ? '' : undefined} in:fly={{ x: -16 }}>
<TaskCheckbox bind:checked={task.checked} />
<span class="title">{task.title}</span>
</div>
{/each}
</div>
<div class="add-btn">
<span class="web-icon-plus"></span>
</div>
</div>
<style lang="scss">
.push-notification {
position: absolute;
display: flex;
justify-content: space-between;
align-items: center;
top: 20px;
padding: 0.5rem;
margin: 0 auto;
width: 125%;
height: 60px;
gap: 0.75rem;
left: 50%;
transform: translateX(-50%);
z-index: 1000;
border-radius: 20px;
background: rgba(255, 255, 255, 0.8);
backdrop-filter: blur(8px);
box-shadow: 3px -8px 32px 0px rgba(0, 0, 0, 0.24);
.icon {
height: 38px;
width: 38px;
flex-shrink: 0;
border-radius: 10px;
background-image: linear-gradient(180deg, #7c67fe, #4a3e98);
}
.header {
display: flex;
justify-content: space-between;
.title {
color: var(--color-greyscale-800, #2d2d31);
font-family: Inter;
font-size: 0.75rem;
font-style: normal;
font-weight: 600;
line-height: 1rem; /* 137.5% */
letter-spacing: -0.014rem;
}
.time {
color: var(--color-greyscale-700, #56565c);
font-family: Inter;
font-size: 0.75rem;
font-style: normal;
font-weight: 400;
line-height: 1rem; /* 137.5% */
letter-spacing: -0.014rem;
}
}
.message {
color: var(--color-greyscale-700, #56565c);
font-family: Inter;
font-size: 0.75rem;
font-style: normal;
font-weight: 400;
line-height: 1rem; /* 137.5% */
letter-spacing: -0.014rem;
}
}
.inner-phone {
padding-block: 3rem 1.5rem;
padding-inline: 1rem;
color: rgba(67, 67, 71, 1);
text-align: left;
position: relative;
height: 100%;
overflow: visible;
display: flex;
flex-direction: column;
.header {
display: flex;
justify-content: space-between;
align-items: center;
.title {
color: var(--color-greyscale-800, #2d2d31);
font-family: Inter;
font-size: 1rem;
font-style: normal;
font-weight: 600;
line-height: 1.375rem; /* 137.5% */
letter-spacing: -0.014rem;
}
[class*='icon-'] {
font-size: 1.25rem;
color: hsl(var(--web-color-greyscale-500));
}
}
.date {
margin-block-start: 3rem;
color: hsl(var(--web-color-greyscale-600));
font-family: Inter;
font-size: 0.75rem;
font-style: normal;
font-weight: 500;
line-height: 1.25rem; /* 166.667% */
}
.tasks {
margin-block-start: 0.5rem;
display: flex;
flex-direction: column;
gap: 0.5rem;
.task {
display: flex;
align-items: center;
gap: 0.75rem;
border-radius: 0.5rem;
border: 1px solid hsl(var(--web-color-greyscale-50));
background: hsl(var(--web-color-white));
color: var(--greyscale-700, var(--color-greyscale-700, #56565c));
padding-block: 0.55rem;
padding-inline: 0.88rem;
/* Responsive/SubBody-400 */
font-family: Inter;
font-size: 0.875rem;
font-style: normal;
font-weight: 400;
line-height: 1.375rem; /* 157.143% */
letter-spacing: -0.00394rem;
transition: opacity 200ms ease;
&[data-checked] {
opacity: 0.6;
}
}
}
.add-btn {
position: absolute;
right: 1rem;
bottom: 2.5rem;
width: 2.5rem;
height: 2.5rem;
flex-shrink: 0;
box-shadow: 0px 2px 6px 0px rgba(0, 0, 0, 0.12);
background-color: rgba(124, 103, 254, 1);
color: rgba(237, 237, 240, 1);
font-size: 1.5rem;
display: grid;
place-items: center;
border-radius: 100%;
}
}
</style>

View File

@@ -1,53 +0,0 @@
import { safeAnimate } from '$lib/animations';
import { createResettable } from '$lib/utils/resettable';
import { animate } from 'motion';
import { getElSelector } from '../Products.svelte';
const requests = createResettable(0);
const databases = createResettable(0);
const authentication = createResettable(0);
const storage = createResettable(0);
const bandwidth = createResettable(0);
const executions = createResettable(0);
const realtime = createResettable(0);
const execute = async () => {
const phone = getElSelector('phone');
const pd = getElSelector('pd');
const graphBox = getElSelector('graph-box');
const boxesAndStates = [
{ box: getElSelector('post-auth'), state: authentication.reset() },
{ box: getElSelector('post-storage'), state: storage.reset() },
{ box: getElSelector('post-bandwidth'), state: bandwidth.reset() },
{ box: getElSelector('post-functions'), state: executions.reset() },
{ box: getElSelector('post-databases'), state: databases.reset() },
{ box: getElSelector('post-realtime'), state: realtime.reset() },
{ box: getElSelector('post-requests'), state: requests.reset() }
];
await Promise.all([
safeAnimate(pd, { opacity: 0, y: -16 }, { duration: 0.5 })?.finished,
safeAnimate(graphBox, { opacity: 0, visibility: 'hidden' }, { duration: 0.5 })?.finished,
safeAnimate(phone, { x: '-50%', width: '660px' }, { duration: 1, delay: 0.5 })?.finished
]);
boxesAndStates.forEach(({ box, state }, i) => {
safeAnimate(box, { opacity: 1, y: [1200, 0] }, { duration: 0.5, delay: i * 0.1 })?.finished;
animate(state.set, { duration: 2, delay: (i + 1) * 0.25 });
});
};
export const postController = {
execute,
state: {
requests,
databases,
authentication,
storage,
bandwidth,
executions,
realtime
}
};

View File

@@ -1,308 +0,0 @@
<script lang="ts">
import { toScale } from '$lib/utils/toScale';
import { postController } from '.';
import { elId } from '../Products.svelte';
const {
state: { authentication, bandwidth, databases, executions, requests, storage, realtime }
} = postController;
const formatK = (num: number) => {
if (num > 999) {
return `${(num / 1000).toFixed(1)}K`;
}
return Math.floor(num);
};
</script>
<div class="gradient-box auth" id="post-auth-{$elId}">
<div class="flex items-center gap-2">
<p class="icon-user-group"></p>
<p class="f-eyebrow">Authentication</p>
</div>
<p class="f-display mbs-16">
{formatK(toScale($authentication, [0, 1], [0, 4000]))}
</p>
<div class="mbs-4 flex items-center justify-between">
<p class="f-sub">Users</p>
<p class="f-idk">Sessions: 20K</p>
</div>
</div>
<div class="gradient-box storage" id="post-storage-{$elId}">
<div class="flex items-center gap-2">
<p class="icon-folder"></p>
<p class="f-eyebrow">Storage</p>
</div>
<p class="f-display mbs-16">
{toScale($storage, [0, 1], [0, 8]).toFixed(1)}
<span class="f-tiny-display">GB</span>
</p>
<div class="mbs-4 flex items-center justify-between">
<p class="f-sub">Storage</p>
<p class="f-idk">Buckets: 44</p>
</div>
</div>
<div class="gradient-box bandwidth" id="post-bandwidth-{$elId}">
<p class="f-display">
{toScale($bandwidth, [0, 1], [0, 1.2]).toFixed(2)}
<span class="f-tiny-display">GB</span>
</p>
<p class="f-sub">Bandwidth</p>
<img class="mbs-16" src="./images/animations/bandwidth-graph.svg" alt="" />
</div>
<div class="gradient-box functions" id="post-functions-{$elId}">
<div class="flex items-center gap-2">
<p class="icon-lightning-bolt"></p>
<p class="f-eyebrow">Functions</p>
</div>
<p class="f-display mbs-16">
{toScale($executions, [0, 1], [0, 846]).toFixed(0)}
</p>
<div class="mbs-4 flex items-center justify-between">
<p class="f-sub">Executions</p>
</div>
</div>
<div class="gradient-box databases" id="post-databases-{$elId}">
<div class="flex items-center gap-2">
<p class="icon-database"></p>
<p class="f-eyebrow">Databases</p>
</div>
<p class="f-display mbs-16">
{toScale($databases, [0, 1], [0, 8]).toFixed(0)}
</p>
<div class="mbs-4 flex items-center justify-between">
<p class="f-sub">Databases</p>
<p class="f-idk">Documents: 20</p>
</div>
</div>
<div class="gradient-box requests" id="post-requests-{$elId}">
<p class="f-display">{formatK(toScale($requests, [0, 1], [0, 6849]))}</p>
<p class="f-sub">Requests</p>
<img class="mbs-16" src="./images/animations/requests-graph.svg" alt="" />
</div>
<div class="gradient-box realtime" id="post-realtime-{$elId}">
<p class="f-display">{formatK(toScale($realtime, [0, 1], [0, 100000]))}</p>
<p class="f-sub">Realtime connections</p>
<img class="mbs-16" src="./images/animations/realtime-graph.svg" alt="" />
</div>
<div class="gradient-overlay flex flex-col">
<h3>See your products grow</h3>
<p>
Keep track of your projects progress on the Appwrite Console and see them grow into products
users love and use every day.
</p>
</div>
<style lang="scss">
@use '$scss/abstract/mixins/border-gradient' as gradients;
// Utilities
.f-eyebrow {
color: #adadb0;
/* Eyebrow headings/level 3 */
font-family: Inter;
font-size: 0.75rem;
font-style: normal;
font-weight: 500;
line-height: 150%; /* 1.125rem */
letter-spacing: 0.09rem;
text-transform: uppercase;
}
.f-display {
color: #ededf0;
font-family: Aeonik Pro;
font-size: 2rem;
font-style: normal;
font-weight: 500;
line-height: 2.25rem; /* 112.5% */
}
.f-tiny-display {
color: var(--greyscale-50, #ededf0);
text-align: center;
font-family: Inter;
font-size: 0.875rem;
font-style: normal;
font-weight: 500;
line-height: 1rem; /* 114.286% */
}
.f-sub {
color: #97979b;
font-family: Inter;
font-size: 0.875rem;
font-style: normal;
font-weight: 400;
line-height: 1.125rem; /* 128.571% */
}
.f-idk {
color: var(--primary, #e4e4e7);
text-align: right;
font-family: Inter;
font-size: 0.875rem;
font-style: normal;
font-weight: 400;
line-height: 150%; /* 1.3125rem */
opacity: 40%;
}
.mbs-16 {
margin-block-start: 1rem;
}
.mbs-4 {
margin-block-start: 0.25rem;
}
.justify-between {
justify-content: space-between;
}
// Components
.gradient-box {
@include gradients.border-gradient;
--m-border-gradient-before: linear-gradient(
180deg,
rgba(255, 255, 255, 0.12) 0%,
rgba(255, 255, 255, 0) 125.11%
);
--m-border-radius: 1rem;
position: absolute;
background: var(--card, rgba(35, 35, 37, 0.9));
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(8px);
padding: 1.5rem;
z-index: 9999;
min-width: 20rem;
}
@keyframes fadeIn {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
.gradient-overlay {
position: absolute;
z-index: 100;
bottom: -7.5rem;
width: 100%;
height: 30rem;
display: flex;
flex-direction: column;
align-items: center;
gap: 1.25rem;
padding-block-start: 15rem;
opacity: 0;
animation: fadeIn 0.75s ease-in-out 1.5s forwards;
&::before {
content: '';
inset: 0;
position: absolute;
//background: #19191d; // old bg
//filter: blur(125px); // break Safari
background: #19191dcc;
filter: blur(67px);
}
h3 {
position: relative;
color: var(--primary, #e4e4e7);
text-align: center;
/* Desktop/Display */
font-family: Aeonik Pro;
font-size: 4rem;
font-style: normal;
font-weight: 400;
line-height: 4.25rem; /* 106.25% */
letter-spacing: -0.04rem;
}
p {
position: relative;
color: var(--secondary, #adadb0);
text-align: center;
/* Desktop/Description */
font-family: Inter;
font-size: 1.25rem;
font-style: normal;
font-weight: 500;
line-height: 1.75rem; /* 140% */
letter-spacing: -0.0175rem;
max-width: 40rem;
text-align: center;
}
}
// Specifics
.auth {
opacity: 0;
left: 4rem;
top: -11rem;
}
.storage {
opacity: 0;
left: -10rem;
top: -2rem;
}
.bandwidth {
opacity: 0;
left: -4rem;
top: 11rem;
}
.functions {
opacity: 0;
left: -6rem;
top: 35rem;
}
.databases {
opacity: 0;
top: -13rem;
right: 10rem;
}
.requests {
opacity: 0;
top: 17rem;
right: -18rem;
}
.realtime {
opacity: 0;
top: -1rem;
right: -7rem;
}
</style>

View File

@@ -1,238 +0,0 @@
import Phone from './phone.svelte';
import { safeAnimate, sleep } from '$lib/animations';
import { createResettable } from '$lib/utils/resettable';
import { getElSelector } from '../Products.svelte';
import { animate } from 'motion';
type Task = {
title: string;
tags: string[];
images?: string[];
};
type User = {
name: string;
color: string;
};
type State = {
tasks: {
todo: Task[];
doing: Task[];
done: Task[];
};
users: User[];
};
const state = createResettable<State>({
tasks: {
todo: [
{
title: 'Edit images for website',
tags: ['design', 'content'],
images: ['./images/animations/storage-2.png', './images/animations/storage-3.png']
}
],
doing: [
{
title: 'Handoff meet',
tags: ['design', 'dev']
}
],
done: []
},
users: []
});
export const connectionsProg = createResettable(0);
const addUser = (update: typeof state.update, user: User) => {
update((p) => ({
...p,
users: [...p.users, user]
}));
};
const addTask = (update: typeof state.update, group: keyof State['tasks'], task: Task) => {
update((p) => ({
...p,
tasks: {
...p.tasks,
[group]: [task, ...p.tasks[group]]
}
}));
};
const execute = async () => {
const phone = getElSelector('phone');
const code = getElSelector('code');
const box = getElSelector('box');
const walter = getElSelector('user-Walter');
const aditya = getElSelector('user-Aditya');
const sara = getElSelector('user-Sara');
const addTodo = getElSelector('add-todo');
const addDoing = getElSelector('add-doing');
const addDone = getElSelector('add-done');
const graphBox = getElSelector('graph-box');
const pd = getElSelector('pd');
const { update } = state.reset();
const { set: setConn } = connectionsProg.reset();
await Promise.all([
safeAnimate(box, { opacity: 0 }, { duration: 0.5 })?.finished,
safeAnimate(phone, { x: 0, y: 0, width: '660px' }, { duration: 0.5 })?.finished,
safeAnimate(code, { opacity: 0 }, { duration: 0.5 })?.finished,
safeAnimate(graphBox, { opacity: 0, x: 0, y: 0, visibility: 'visible' }, { duration: 0 })
?.finished,
safeAnimate(pd, { opacity: 1, y: 0 }, { duration: 0.5 })?.finished
]);
// Graphbox
sleep(1250).then(async () => {
await safeAnimate(graphBox, { opacity: 1 }, { duration: 0.5 })?.finished;
animate(
(y) => {
setConn(y);
},
{ duration: 2.5, easing: 'ease-in' }
);
});
// Walter
sleep(500).then(async () => {
addUser(update, { name: 'Walter', color: '#fd366e' });
await sleep(500);
await safeAnimate(walter, { x: -200, y: -100, scale: 1 }, { duration: 0.5 })?.finished;
await Promise.all([
safeAnimate(walter, { scale: [1, 0.9, 1] }, { duration: 0.25 })?.finished,
safeAnimate(addTodo, { scale: [1, 0.9, 1] }, { duration: 0.25 })?.finished
]);
addTask(update, 'todo', {
title: 'Handoff meet',
tags: ['design', 'dev']
});
await safeAnimate(walter, { scale: 1, x: -180, y: -160 }, { duration: 0.75, delay: 0.5 })
?.finished;
await sleep(500);
await safeAnimate(walter, { x: 210, y: -100, scale: 1 }, { duration: 0.5 })?.finished;
await Promise.all([
safeAnimate(walter, { scale: [1, 0.9, 1] }, { duration: 0.25 })?.finished,
safeAnimate(addDone, { scale: [1, 0.9, 1] }, { duration: 0.25 })?.finished
]);
addTask(update, 'done', {
title: 'Create migrations script',
tags: ['Dev']
});
safeAnimate(walter, { scale: 1, x: 230, y: -20 }, { duration: 0.75, delay: 0.5 });
await sleep(750);
await safeAnimate(walter, { x: -10, y: -100, scale: 1 }, { duration: 0.5 })?.finished;
await Promise.all([
safeAnimate(walter, { scale: [1, 0.9, 1] }, { duration: 0.25 })?.finished,
safeAnimate(addDoing, { scale: [1, 0.9, 1] }, { duration: 0.25 })?.finished
]);
addTask(update, 'doing', {
title: 'Configure blog SEO',
tags: ['dev', 'content']
});
await safeAnimate(walter, { scale: 1, x: -70, y: 80 }, { duration: 0.75, delay: 0.25 });
});
// Aditya
sleep(1500).then(async () => {
addUser(update, { name: 'Aditya', color: 'rgba(124, 103, 254, 1)' });
await sleep(500);
await safeAnimate(aditya, { x: 200, y: -100, scale: 1 }, { duration: 0.5 })?.finished;
await Promise.all([
safeAnimate(aditya, { scale: [1, 0.9, 1] }, { duration: 0.25 })?.finished,
safeAnimate(addDone, { scale: [1, 0.9, 1] }, { duration: 0.25 })?.finished
]);
addTask(update, 'done', {
title: 'Write up briefing',
tags: ['dev-rel']
});
await safeAnimate(aditya, { scale: 1, x: 180, y: 60 }, { duration: 0.75, delay: 0.5 })
?.finished;
await sleep(750);
await safeAnimate(aditya, { x: -210, y: -100, scale: 1 }, { duration: 0.5 })?.finished;
await Promise.all([
safeAnimate(aditya, { scale: [1, 0.9, 1] }, { duration: 0.25 })?.finished,
safeAnimate(addTodo, { scale: [1, 0.9, 1] }, { duration: 0.25 })?.finished
]);
addTask(update, 'todo', {
title: 'Review branding blog post',
tags: ['dev-rel']
});
await safeAnimate(aditya, { scale: 1, x: 70, y: -220 }, { duration: 0.75, delay: 0.5 })
?.finished;
});
// Sara
sleep(2500).then(async () => {
addUser(update, { name: 'Sara', color: 'rgba(103, 163, 254, 1)' });
await sleep(500);
await safeAnimate(sara, { x: 0, y: -100, scale: 1 }, { duration: 0.5 })?.finished;
await Promise.all([
safeAnimate(sara, { scale: [1, 0.9, 1] }, { duration: 0.25 })?.finished,
safeAnimate(addDoing, { scale: [1, 0.9, 1] }, { duration: 0.25 })?.finished
]);
addTask(update, 'doing', {
title: 'Prepare design system presentation',
tags: ['design']
});
await safeAnimate(sara, { scale: 1, y: 60, x: -50 }, { duration: 0.75, delay: 0.5 })
?.finished;
await sleep(250);
await safeAnimate(sara, { x: 200, y: -100, scale: 1 }, { duration: 0.5 })?.finished;
await Promise.all([
safeAnimate(sara, { scale: [1, 0.9, 1] }, { duration: 0.25 })?.finished,
safeAnimate(addDone, { scale: [1, 0.9, 1] }, { duration: 0.25 })?.finished
]);
addTask(update, 'done', {
title: 'QA branding animations',
tags: ['Dev']
});
await safeAnimate(sara, { scale: 1, x: 180, y: 60 }, { duration: 0.75, delay: 0.5 })
?.finished;
});
};
export const realtimeController = {
execute,
state
};
export const Realtime = {
Phone
};

View File

@@ -1,657 +0,0 @@
<script lang="ts">
import { objectKeys } from '$lib/utils/object';
import { scale } from 'svelte/transition';
import { connectionsProg, realtimeController } from '.';
import { elId } from '../Products.svelte';
import { flip } from '$lib/utils/flip';
import { toScale } from '$lib/utils/toScale';
const { state } = realtimeController;
const getInitial = (name: string) => name[0].toUpperCase();
$: connections = toScale($connectionsProg, [0, 1], [0, 10 ** 5]);
const lines = [9, 14, 44, 54, 50, 46, 52, 60, 66, 74, 86, 110];
$: progressedLines = (function getPL(): number[] {
const pl = lines.map(() => 0);
const total = lines.reduce((acc, curr) => acc + curr, 0);
const curr = total * $connectionsProg;
// Fill the lines until the current progress
let filled = 0;
for (let i = 0; i < pl.length; i++) {
const line = lines[i];
if (filled + line < curr) {
pl[i] = line;
filled += line;
} else {
pl[i] = curr - filled;
break;
}
}
return pl;
})();
const formatNumber = (num: number) => {
if (num < 1000) return Math.floor(num);
return `${Math.floor(num / 1000)}k`;
};
</script>
<div class="wrapper">
<div data-theme-ignore class="inner-phone light">
<div class="header">
<div class="row">
<p class="title">My Team's tasks</p>
<div class="flow gap-8">
<div class="tgl-avatars">
{#each $state.users as user}
<div class="tgl-avatar" style:--color={user.color} in:scale>
{getInitial(user.name)}
</div>
{/each}
</div>
<div class="vertical-sep"></div>
<span class="icon-menu"></span>
</div>
</div>
<div class="row">
<div class="search">
<span class="web-icon-search"></span>
<span class="text"> Search </span>
</div>
<div class="flow gap-8">
<button class="btn">Filter</button>
<button class="btn">Sort</button>
</div>
</div>
</div>
<hr />
<div class="content">
{#each objectKeys($state.tasks) as col, i}
{@const tasks = $state.tasks[col]}
{@const isLast = i === objectKeys($state.tasks).length - 1}
<div class="column">
<div class="title">
<span class="text capitalize">{col}</span>
<span class="tgl-inline-tag">{tasks.length}</span>
<span class="icon-dots-horizontal"></span>
</div>
<div class="flow-v mbs-8 gap-12">
<button class="dashed-btn" id="add-{col}-{$elId}">
<span class="icon-plus"></span>
<span class="text">New Task</span>
</button>
{#each tasks as task (task.title)}
<div
class="task"
animate:flip={{ duration: 250 }}
in:scale={{ delay: 150 }}
>
{#if task.images}
<ul class="flow gap-8">
{#each task.images as image}
<img class="sq-32" src={image} alt="" />
{/each}
</ul>
{/if}
<p class="text">{task.title}</p>
<ul class="flow wrap gap-8">
{#each task.tags as tag}
<li class="tgl-tag">{tag}</li>
{/each}
</ul>
</div>
{/each}
</div>
</div>
{#if !isLast}
<div class="vertical-sep"></div>
{/if}
{/each}
</div>
{#each $state.users as user}
<div class="user" style:--color={user.color} id="user-{user.name}-{$elId}" in:scale>
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 18 18">
<path
d="M2.58814 0.558469C1.60242 0.224627 1.10955 0.0577053 0.782928 0.173472C0.498743 0.274197 0.275173 0.497766 0.174449 0.781951C0.0586818 1.10858 0.225603 1.60144 0.559445 2.58716L4.67494 14.7388C5.13698 16.1031 5.368 16.7852 5.71194 16.9722C6.00951 17.134 6.36873 17.1341 6.66644 16.9726C7.01055 16.7859 7.24216 16.104 7.70539 14.7402L9.23555 10.235C9.32861 9.96103 9.37513 9.82404 9.45345 9.7101C9.52283 9.60918 9.61015 9.52185 9.71108 9.45248C9.82502 9.37416 9.96201 9.32763 10.236 9.23457L14.7411 7.70441C16.105 7.24118 16.7869 7.00957 16.9736 6.66547C17.1351 6.36776 17.1349 6.00853 16.9732 5.71096C16.7862 5.36702 16.1041 5.136 14.7398 4.67396L2.58814 0.558469Z"
/>
</svg>
<p class="text">{user.name}</p>
</div>
{/each}
</div>
<div class="graph-box" id="graph-box-{$elId}">
<p class="title">{formatNumber(connections)}</p>
<p class="subtitle">Realtime Connections</p>
<svg
width="324"
height="133"
viewBox="0 0 324 133"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<g clip-path="url(#clip0_3981_106717)">
<path
d="M15.8661 4.27273V13H14.8093V5.38068H14.7582L12.6275 6.79545V5.72159L14.8093 4.27273H15.8661ZM21.2227 13.1193C20.5806 13.1193 20.0337 12.9446 19.582 12.5952C19.1303 12.2429 18.7852 11.733 18.5465 11.0653C18.3079 10.3949 18.1886 9.58523 18.1886 8.63636C18.1886 7.69318 18.3079 6.88778 18.5465 6.22017C18.788 5.54972 19.1346 5.03835 19.5863 4.68608C20.0408 4.33097 20.5863 4.15341 21.2227 4.15341C21.859 4.15341 22.4031 4.33097 22.8548 4.68608C23.3093 5.03835 23.6559 5.54972 23.8945 6.22017C24.136 6.88778 24.2567 7.69318 24.2567 8.63636C24.2567 9.58523 24.1374 10.3949 23.8988 11.0653C23.6602 11.733 23.315 12.2429 22.8633 12.5952C22.4116 12.9446 21.8647 13.1193 21.2227 13.1193ZM21.2227 12.1818C21.859 12.1818 22.3533 11.875 22.7056 11.2614C23.0579 10.6477 23.234 9.77273 23.234 8.63636C23.234 7.88068 23.1531 7.23722 22.9911 6.70597C22.832 6.17472 22.6019 5.76989 22.3008 5.49148C22.0025 5.21307 21.6431 5.07386 21.2227 5.07386C20.592 5.07386 20.0991 5.38494 19.744 6.0071C19.3888 6.62642 19.2113 7.50284 19.2113 8.63636C19.2113 9.39205 19.2908 10.0341 19.4499 10.5625C19.609 11.0909 19.8377 11.4929 20.136 11.7685C20.4371 12.044 20.7994 12.1818 21.2227 12.1818ZM28.7227 13.1193C28.0806 13.1193 27.5337 12.9446 27.082 12.5952C26.6303 12.2429 26.2852 11.733 26.0465 11.0653C25.8079 10.3949 25.6886 9.58523 25.6886 8.63636C25.6886 7.69318 25.8079 6.88778 26.0465 6.22017C26.288 5.54972 26.6346 5.03835 27.0863 4.68608C27.5408 4.33097 28.0863 4.15341 28.7227 4.15341C29.359 4.15341 29.9031 4.33097 30.3548 4.68608C30.8093 5.03835 31.1559 5.54972 31.3945 6.22017C31.636 6.88778 31.7567 7.69318 31.7567 8.63636C31.7567 9.58523 31.6374 10.3949 31.3988 11.0653C31.1602 11.733 30.815 12.2429 30.3633 12.5952C29.9116 12.9446 29.3647 13.1193 28.7227 13.1193ZM28.7227 12.1818C29.359 12.1818 29.8533 11.875 30.2056 11.2614C30.5579 10.6477 30.734 9.77273 30.734 8.63636C30.734 7.88068 30.6531 7.23722 30.4911 6.70597C30.332 6.17472 30.1019 5.76989 29.8008 5.49148C29.5025 5.21307 29.1431 5.07386 28.7227 5.07386C28.092 5.07386 27.5991 5.38494 27.244 6.0071C26.8888 6.62642 26.7113 7.50284 26.7113 8.63636C26.7113 9.39205 26.7908 10.0341 26.9499 10.5625C27.109 11.0909 27.3377 11.4929 27.636 11.7685C27.9371 12.044 28.2994 12.1818 28.7227 12.1818ZM34.3306 10.6136L34.3136 9.36932H34.5181L37.3817 6.45455H38.6261L35.5749 9.53977H35.4897L34.3306 10.6136ZM33.3931 13V4.27273H34.3988V13H33.3931ZM37.5522 13L34.9954 9.76136L35.7113 9.0625L38.8306 13H37.5522Z"
fill="#6C6C71"
/>
<g filter="url(#filter0_b_3981_106717)">
<line
x1="55.1016"
y1="9.27344"
x2="324.001"
y2="9.27344"
stroke="white"
stroke-opacity="0.06"
/>
</g>
<path
d="M19.4847 52L23.3881 44.2784V44.2102H18.8881V43.2727H24.479V44.2614L20.5927 52H19.4847ZM28.7266 52.1193C28.0845 52.1193 27.5376 51.9446 27.0859 51.5952C26.6342 51.2429 26.2891 50.733 26.0504 50.0653C25.8118 49.3949 25.6925 48.5852 25.6925 47.6364C25.6925 46.6932 25.8118 45.8878 26.0504 45.2202C26.2919 44.5497 26.6385 44.0384 27.0902 43.6861C27.5447 43.331 28.0902 43.1534 28.7266 43.1534C29.3629 43.1534 29.907 43.331 30.3587 43.6861C30.8132 44.0384 31.1598 44.5497 31.3984 45.2202C31.6399 45.8878 31.7607 46.6932 31.7607 47.6364C31.7607 48.5852 31.6413 49.3949 31.4027 50.0653C31.1641 50.733 30.8189 51.2429 30.3672 51.5952C29.9155 51.9446 29.3686 52.1193 28.7266 52.1193ZM28.7266 51.1818C29.3629 51.1818 29.8572 50.875 30.2095 50.2614C30.5618 49.6477 30.7379 48.7727 30.7379 47.6364C30.7379 46.8807 30.657 46.2372 30.495 45.706C30.3359 45.1747 30.1058 44.7699 29.8047 44.4915C29.5064 44.2131 29.147 44.0739 28.7266 44.0739C28.0959 44.0739 27.603 44.3849 27.2479 45.0071C26.8928 45.6264 26.7152 46.5028 26.7152 47.6364C26.7152 48.392 26.7947 49.0341 26.9538 49.5625C27.1129 50.0909 27.3416 50.4929 27.6399 50.7685C27.9411 51.044 28.3033 51.1818 28.7266 51.1818ZM34.3345 49.6136L34.3175 48.3693H34.522L37.3857 45.4545H38.63L35.5788 48.5398H35.4936L34.3345 49.6136ZM33.397 52V43.2727H34.4027V52H33.397ZM37.5561 52L34.9993 48.7614L35.7152 48.0625L38.8345 52H37.5561Z"
fill="#6C6C71"
/>
<g filter="url(#filter1_b_3981_106717)">
<line
x1="55.1016"
y1="48.0391"
x2="324.001"
y2="48.0391"
stroke="white"
stroke-opacity="0.06"
/>
</g>
<path
d="M21.3026 90.1193C20.8026 90.1193 20.3523 90.0199 19.9517 89.821C19.5511 89.6222 19.2301 89.3494 18.9886 89.0028C18.7472 88.6562 18.6151 88.2614 18.5923 87.8182H19.6151C19.6548 88.2131 19.8338 88.5398 20.152 88.7983C20.473 89.054 20.8565 89.1818 21.3026 89.1818C21.6605 89.1818 21.9787 89.098 22.2571 88.9304C22.5384 88.7628 22.7585 88.5327 22.9176 88.2401C23.0795 87.9446 23.1605 87.6108 23.1605 87.2386C23.1605 86.858 23.0767 86.5185 22.9091 86.2202C22.7443 85.919 22.517 85.6818 22.2273 85.5085C21.9375 85.3352 21.6065 85.2472 21.2344 85.2443C20.9673 85.2415 20.6932 85.2827 20.4119 85.3679C20.1307 85.4503 19.8991 85.5568 19.7173 85.6875L18.7287 85.5682L19.2571 81.2727H23.7912V82.2102H20.1435L19.8366 84.7841H19.8878C20.0668 84.642 20.2912 84.5241 20.5611 84.4304C20.831 84.3366 21.1122 84.2898 21.4048 84.2898C21.9389 84.2898 22.4148 84.4176 22.8324 84.6733C23.2528 84.9261 23.5824 85.2727 23.821 85.7131C24.0625 86.1534 24.1832 86.6562 24.1832 87.2216C24.1832 87.7784 24.0582 88.2756 23.8082 88.7131C23.5611 89.1477 23.2202 89.4915 22.7855 89.7443C22.3509 89.9943 21.8565 90.1193 21.3026 90.1193ZM28.7227 90.1193C28.0806 90.1193 27.5337 89.9446 27.082 89.5952C26.6303 89.2429 26.2852 88.733 26.0465 88.0653C25.8079 87.3949 25.6886 86.5852 25.6886 85.6364C25.6886 84.6932 25.8079 83.8878 26.0465 83.2202C26.288 82.5497 26.6346 82.0384 27.0863 81.6861C27.5408 81.331 28.0863 81.1534 28.7227 81.1534C29.359 81.1534 29.9031 81.331 30.3548 81.6861C30.8093 82.0384 31.1559 82.5497 31.3945 83.2202C31.636 83.8878 31.7567 84.6932 31.7567 85.6364C31.7567 86.5852 31.6374 87.3949 31.3988 88.0653C31.1602 88.733 30.815 89.2429 30.3633 89.5952C29.9116 89.9446 29.3647 90.1193 28.7227 90.1193ZM28.7227 89.1818C29.359 89.1818 29.8533 88.875 30.2056 88.2614C30.5579 87.6477 30.734 86.7727 30.734 85.6364C30.734 84.8807 30.6531 84.2372 30.4911 83.706C30.332 83.1747 30.1019 82.7699 29.8008 82.4915C29.5025 82.2131 29.1431 82.0739 28.7227 82.0739C28.092 82.0739 27.5991 82.3849 27.244 83.0071C26.8888 83.6264 26.7113 84.5028 26.7113 85.6364C26.7113 86.392 26.7908 87.0341 26.9499 87.5625C27.109 88.0909 27.3377 88.4929 27.636 88.7685C27.9371 89.044 28.2994 89.1818 28.7227 89.1818ZM34.3306 87.6136L34.3136 86.3693H34.5181L37.3817 83.4545H38.6261L35.5749 86.5398H35.4897L34.3306 87.6136ZM33.3931 90V81.2727H34.3988V90H33.3931ZM37.5522 90L34.9954 86.7614L35.7113 86.0625L38.8306 90H37.5522Z"
fill="#6C6C71"
/>
<g filter="url(#filter2_b_3981_106717)">
<line
x1="55.1016"
y1="85.8125"
x2="324.001"
y2="85.8125"
stroke="white"
stroke-opacity="0.06"
/>
</g>
<path
d="M35.2539 128.119C34.6119 128.119 34.065 127.945 33.6133 127.595C33.1616 127.243 32.8164 126.733 32.5778 126.065C32.3391 125.395 32.2198 124.585 32.2198 123.636C32.2198 122.693 32.3391 121.888 32.5778 121.22C32.8192 120.55 33.1658 120.038 33.6175 119.686C34.0721 119.331 34.6175 119.153 35.2539 119.153C35.8903 119.153 36.4343 119.331 36.886 119.686C37.3406 120.038 37.6871 120.55 37.9258 121.22C38.1673 121.888 38.288 122.693 38.288 123.636C38.288 124.585 38.1687 125.395 37.93 126.065C37.6914 126.733 37.3462 127.243 36.8945 127.595C36.4428 127.945 35.896 128.119 35.2539 128.119ZM35.2539 127.182C35.8903 127.182 36.3846 126.875 36.7369 126.261C37.0891 125.648 37.2653 124.773 37.2653 123.636C37.2653 122.881 37.1843 122.237 37.0224 121.706C36.8633 121.175 36.6332 120.77 36.332 120.491C36.0337 120.213 35.6744 120.074 35.2539 120.074C34.6232 120.074 34.1303 120.385 33.7752 121.007C33.4201 121.626 33.2425 122.503 33.2425 123.636C33.2425 124.392 33.3221 125.034 33.4812 125.562C33.6403 126.091 33.869 126.493 34.1673 126.768C34.4684 127.044 34.8306 127.182 35.2539 127.182Z"
fill="#6C6C71"
/>
<g filter="url(#filter3_b_3981_106717)">
<line
x1="55.1016"
y1="123.586"
x2="324.001"
y2="123.586"
stroke="white"
stroke-opacity="0.06"
/>
</g>
{#each progressedLines as line, i}
{@const x = 57 + i * 24}
{@const y = 124 - line}
{#if line > 3}
<circle cx={x} cy={y} r="3" fill="#FD366E" />
<line x1={x} y1={y} x2={x} y2="124" stroke="#FD366E" stroke-width="6" />
{/if}
{/each}
</g>
<defs>
<filter
id="filter0_b_3981_106717"
x="-144.898"
y="-191.227"
width="668.899"
height="401"
filterUnits="userSpaceOnUse"
color-interpolation-filters="sRGB"
>
<feFlood flood-opacity="0" result="BackgroundImageFix" />
<feGaussianBlur in="BackgroundImageFix" stdDeviation="100" />
<feComposite
in2="SourceAlpha"
operator="in"
result="effect1_backgroundBlur_3981_106717"
/>
<feBlend
mode="normal"
in="SourceGraphic"
in2="effect1_backgroundBlur_3981_106717"
result="shape"
/>
</filter>
<filter
id="filter1_b_3981_106717"
x="-144.898"
y="-152.461"
width="668.899"
height="401"
filterUnits="userSpaceOnUse"
color-interpolation-filters="sRGB"
>
<feFlood flood-opacity="0" result="BackgroundImageFix" />
<feGaussianBlur in="BackgroundImageFix" stdDeviation="100" />
<feComposite
in2="SourceAlpha"
operator="in"
result="effect1_backgroundBlur_3981_106717"
/>
<feBlend
mode="normal"
in="SourceGraphic"
in2="effect1_backgroundBlur_3981_106717"
result="shape"
/>
</filter>
<filter
id="filter2_b_3981_106717"
x="-144.898"
y="-114.688"
width="668.899"
height="401"
filterUnits="userSpaceOnUse"
color-interpolation-filters="sRGB"
>
<feFlood flood-opacity="0" result="BackgroundImageFix" />
<feGaussianBlur in="BackgroundImageFix" stdDeviation="100" />
<feComposite
in2="SourceAlpha"
operator="in"
result="effect1_backgroundBlur_3981_106717"
/>
<feBlend
mode="normal"
in="SourceGraphic"
in2="effect1_backgroundBlur_3981_106717"
result="shape"
/>
</filter>
<filter
id="filter3_b_3981_106717"
x="-144.898"
y="-76.9141"
width="668.899"
height="401"
filterUnits="userSpaceOnUse"
color-interpolation-filters="sRGB"
>
<feFlood flood-opacity="0" result="BackgroundImageFix" />
<feGaussianBlur in="BackgroundImageFix" stdDeviation="100" />
<feComposite
in2="SourceAlpha"
operator="in"
result="effect1_backgroundBlur_3981_106717"
/>
<feBlend
mode="normal"
in="SourceGraphic"
in2="effect1_backgroundBlur_3981_106717"
result="shape"
/>
</filter>
<clipPath id="clip0_3981_106717">
<rect width="324" height="133" fill="white" />
</clipPath>
</defs>
</svg>
</div>
</div>
<style lang="scss">
@use '$scss/abstract/mixins/border-gradient' as gradients;
// Utilities
.flow {
display: flex;
align-items: center;
}
.flow-v {
display: flex;
flex-direction: column;
}
.gap-8 {
gap: 0.5rem;
}
.gap-12 {
gap: 0.75rem;
}
.sq-32 {
width: 2rem;
height: 2rem;
}
.wrap {
flex-wrap: wrap;
}
.capitalize {
text-transform: capitalize;
}
.mbs-8 {
margin-block-start: 0.5rem;
}
// Components
.tgl-avatars {
display: flex;
.tgl-avatar:not(:first-child) {
margin-inline-start: -0.5rem;
}
}
.tgl-avatar {
--size: 1.25rem;
width: var(--size);
height: var(--size);
display: grid;
place-items: center;
text-align: center;
border-radius: 100%;
color: white;
font-size: 0.65rem;
background-color: var(--color);
}
.tgl-tag {
padding: 0.25rem 0.4375rem;
border-radius: 0.21713rem;
border: 0.869px solid #ededf0;
box-shadow: 0px 1.73704px 3.47408px 0px rgba(0, 0, 0, 0.06);
color: #818186;
font-family: Inter;
font-size: 0.625rem;
font-style: normal;
font-weight: 500;
line-height: 0.75rem; /* 120% */
letter-spacing: -0.00875rem;
text-transform: capitalize;
}
.vertical-sep {
width: 1px;
height: 100%;
background-color: rgba(237, 237, 240, 1);
}
hr {
border-bottom: 1px solid hsl(var(--web-color-greyscale-50));
margin-block: 1rem;
}
.dashed-btn {
display: flex;
padding: 0.5rem 0.875rem;
justify-content: center;
align-items: center;
gap: 0.5rem;
align-self: stretch;
border-radius: 0.625rem;
border: 1px dashed hsl(var(--web-color-greyscale-50));
color: #56565c;
.text {
font-family: Inter;
font-size: 0.8rem;
font-style: normal;
font-weight: 400;
line-height: 1.08563rem; /* 142.857% */
}
[class*='icon'] {
font-size: 1rem;
}
}
.task {
display: flex;
padding: 0.625rem 0.875rem;
flex-direction: column;
justify-content: center;
align-items: flex-start;
gap: 0.5rem;
border-radius: 0.625rem;
border: 1px solid #ededf0;
background: var(--color-bw-white, #fff);
.text {
color: #56565c;
font-family: Inter;
font-size: 0.75rem;
font-style: normal;
font-weight: 500;
line-height: 1rem; /* 133.333% */
}
}
.user {
position: absolute;
left: 50%;
top: 50%;
fill: var(--color);
.text {
position: absolute;
left: 100%;
bottom: 0;
transform: translateX(-4px) translateY(50%);
border-radius: 0rem 0.375rem 0.375rem 0.375rem;
background: var(--color);
box-shadow: 0px 2px 4px 0px rgba(0, 0, 0, 0.06);
padding: 0.25rem 0.5rem;
color: var(--color-bw-white, #fff);
font-family: Inter;
font-size: 0.875rem;
font-style: normal;
font-weight: 500;
line-height: 1rem; /* 114.286% */
letter-spacing: -0.01225rem;
}
}
// Specifics
.wrapper {
height: 100%;
position: relative;
}
.inner-phone {
display: flex;
flex-direction: column;
height: 100%;
padding: 1.25rem;
overflow: hidden;
.header {
display: flex;
flex-direction: column;
gap: 0.5rem;
.row {
display: flex;
justify-content: space-between;
align-items: center;
.title {
color: var(--color-greyscale-800, #2d2d31);
font-family: Inter;
font-size: 0.875rem;
font-style: normal;
font-weight: 600;
line-height: 1.25rem; /* 142.857% */
letter-spacing: -0.01225rem;
}
.search {
display: flex;
align-items: center;
gap: 0.25rem;
padding: 0.25rem;
width: 12rem;
border-radius: 0.5rem;
border: 1.869px solid var(--greyscale-50, #ededf0);
[class*='icon'] {
font-size: 1rem;
}
.text {
color: var(--greyscale-300, #adadb0);
font-family: Inter;
font-size: 0.7rem;
font-style: normal;
font-weight: 500;
line-height: 0.875rem; /* 140% */
}
}
.btn {
display: inline-flex;
padding: 0.125rem 0.25rem;
justify-content: center;
align-items: center;
gap: 0.125rem;
border-radius: 0.25rem;
border: 0.869px solid var(--greyscale-50, #ededf0);
color: var(--greyscale-600, #6c6c71);
font-family: Inter;
font-size: 0.625rem;
font-style: normal;
font-weight: 500;
line-height: 0.875rem; /* 140% */
}
}
}
.content {
display: grid;
grid-template-columns: 1fr 1px 1fr 1px 1fr;
gap: 1rem;
height: 100%;
.column {
flex: 1 0 auto;
.title {
display: flex;
align-items: center;
gap: 0.5rem;
.text {
color: var(--greyscale-600, #6c6c71);
font-family: Inter;
font-size: 0.625rem;
font-style: normal;
font-weight: 500;
line-height: 0.875rem; /* 140% */
}
.tgl-inline-tag {
display: grid;
place-items: center;
padding: 0rem 0.21713rem 0.1rem;
border-radius: 0.21713rem;
background: var(--greyscale-50, #ededf0);
color: var(--greyscale-600, #6c6c71);
font-family: Inter;
font-size: 0.625rem;
font-style: normal;
font-weight: 500;
line-height: 0.875rem; /* 140% */
}
[class*='icon'] {
font-size: 1rem;
margin-inline-start: auto;
}
}
}
}
}
.graph-box {
@include gradients.border-gradient;
--m-border-gradient-before: linear-gradient(
180deg,
rgba(255, 255, 255, 0.12) 0%,
rgba(255, 255, 255, 0) 125.11%
);
--m-border-radius: 1rem;
position: absolute;
right: 2rem;
bottom: -10rem;
background: var(--card, rgba(35, 35, 37, 0.9));
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(8px);
padding-block: 1.5rem;
z-index: 9999;
.title,
.subtitle {
padding-inline: 1.88rem;
}
.title {
color: #e4e4e7;
font-family: Aeonik Pro;
font-size: 2rem;
font-style: normal;
font-weight: 400;
line-height: 2.25rem; /* 112.5% */
}
.subtitle {
color: #adadb0;
font-family: Inter;
font-size: 0.875rem;
font-style: normal;
font-weight: 400;
line-height: 1.125rem; /* 128.571% */
}
> svg {
margin-block-start: 1.25rem;
margin-inline: 0.5rem 1.5rem;
}
}
</style>

View File

@@ -1,39 +0,0 @@
<script lang="ts">
import { slide } from 'svelte/transition';
import { storageController } from '.';
import { flip } from '$lib/utils/flip';
const { state } = storageController;
</script>
<div class="pseudo-table">
<div class="header">
<span class="text-micro uppercase">Filename</span>
<span class="text-micro uppercase">Type</span>
<span class="text-micro uppercase">Size</span>
</div>
{#each $state.files as file (file.src)}
<div class="row" in:slide={{ duration: 150 }} animate:flip={{ duration: 150 }}>
<div class="img-wrapper">
<img src={file.src} alt="" />
<span>{file.filename}</span>
</div>
<span class="truncated">{file.type}</span>
<span class="truncated">{file.size}</span>
</div>
{/each}
</div>
<style lang="scss">
.header,
.row {
grid-template-columns: 7rem 1fr 1fr !important;
gap: 1.5rem 3rem;
}
.img-wrapper {
display: flex;
align-items: center;
gap: 0.5rem;
}
</style>

View File

@@ -1,12 +0,0 @@
<script lang="ts">
import Code from '$lib/animations/CodeWindow/Code.svelte';
let content = `
const result = storage.createFile(
'my-bucket',
ID.unique(),
document.getElementById("uploader").files[0]
);`.trim();
</script>
<Code {content} />

View File

@@ -1,137 +0,0 @@
import Box from './box.svelte';
import Code from './code.svelte';
import Phone from './phone.svelte';
import { safeAnimate, sleep } from '$lib/animations';
import { createResettable } from '$lib/utils/resettable';
import { getElSelector } from '../Products.svelte';
type File = {
src: string;
filename: string;
type: string;
size: string;
};
type State = {
files: File[];
};
const state = createResettable<State>({
files: []
});
const execute = async () => {
const phone = getElSelector('phone');
const box = getElSelector('box');
const code = getElSelector('code');
const overlay = getElSelector('overlay');
const drawer = getElSelector('drawer');
const upload = getElSelector('upload');
const uploadBtn = getElSelector('upload-btn');
const uploadImg = getElSelector('upload-img');
const uploadLoading = getElSelector('upload-loading');
const uploadText = getElSelector('upload-text');
const { update } = state.reset();
await Promise.all([
safeAnimate(phone, { x: 0, y: 0 }, { duration: 0.5 })?.finished,
safeAnimate(box, { opacity: 0 }, { duration: 0.5 })?.finished,
safeAnimate(code, { opacity: 0 }, { duration: 0.5 })?.finished,
safeAnimate(uploadLoading, { opacity: 0 }, { duration: 0 })?.finished
]);
await safeAnimate(code, { zIndex: 20 }, { duration: 0 })?.finished;
update((p) => ({
...p,
files: [
...p.files,
{
src: '/images/animations/storage-1.png',
filename: 'Profile.png',
type: 'image/png',
size: '362.6 KB'
}
]
}));
await sleep(250);
await Promise.all([
safeAnimate(overlay, { opacity: 1 }, { duration: 0.25 })?.finished,
safeAnimate(drawer, { y: [128, 0], opacity: 1 }, { duration: 0.5 })?.finished
]);
await sleep(250);
await safeAnimate(uploadBtn, { scale: [1, 0.9, 1] }, { duration: 0.25 })?.finished;
await safeAnimate(code, { x: 300, y: 32 }, { duration: 0 })?.finished;
await Promise.all([
safeAnimate(code, { y: [32 - 16, 32], opacity: 1 }, { duration: 0.5 })?.finished,
safeAnimate(upload, { y: [-16, 0], opacity: 1 }, { duration: 0.5 })?.finished
]);
await sleep(250);
await safeAnimate(box, { x: 300, y: 300 }, { duration: 0 })?.finished;
await Promise.all([
safeAnimate(uploadImg, { x: [64, 48], y: [80, 64], opacity: 1 }, { duration: 0.5 })
?.finished,
safeAnimate(box, { y: [300 - 16, 300], opacity: 1 }, { duration: 1 })?.finished
]);
await sleep(250);
await Promise.all([
safeAnimate(uploadText, { opacity: 0 }, { duration: 0.5 })?.finished,
safeAnimate(uploadLoading, { opacity: 1 }, { duration: 0.5 })?.finished,
safeAnimate(uploadImg, { opacity: 0, y: 64 + 8 }, { duration: 0.5 })?.finished
]);
await sleep(250);
await safeAnimate(upload, { opacity: 0, y: 48 }, { duration: 0.5 })?.finished;
update((p) => ({
...p,
files: [
...p.files,
{
src: '/images/animations/storage-2.png',
filename: 'Vector.svg',
type: 'vector/svg',
size: '1.5 KB'
}
]
}));
await sleep(250);
update((p) => ({
...p,
files: [
...p.files,
{
src: '/images/animations/storage-3.png',
filename: 'img2.webp',
type: 'image/webp',
size: '3.2 MB'
}
]
}));
};
export const storageController = {
execute,
state
};
export const Storage = {
Phone,
Box,
Code
};

View File

@@ -1,284 +0,0 @@
<script lang="ts">
import { fly } from 'svelte/transition';
import { storageController } from '.';
import { elId } from '../Products.svelte';
import TaskCheckbox from '../TaskCheckbox.svelte';
import { databasesController } from '../databases';
const { state: dbState } = databasesController;
const fixedTasks = $dbState.tasks;
const { state } = storageController;
</script>
<div data-theme-ignore class="inner-phone light">
<div class="header">
<p class="title">Your tasks</p>
<span class="icon-menu" aria-label="menu"></span>
</div>
<div class="date">Today</div>
<div class="tasks">
{#each fixedTasks as task (task.id)}
<div class="task" data-checked={task.checked ? '' : undefined} in:fly={{ x: -16 }}>
<TaskCheckbox bind:checked={task.checked} />
<span class="title">{task.title}</span>
</div>
{/each}
</div>
<div class="add-btn">
<span class="web-icon-plus"></span>
</div>
<div class="overlay" id="overlay-{$elId}">
<div class="drawer" id="drawer-{$elId}">
<p class="title">Edit images for website</p>
<p class="subtitle">Edit the attached images to use in the website</p>
<div class="upload" id="upload-btn-{$elId}">Upload media...</div>
<div class="images">
{#each $state.files.slice(1) as file}
<img src={file.src} alt="" transition:fly={{ x: 16 }} />
{/each}
</div>
</div>
</div>
</div>
<div class="upload-media" id="upload-{$elId}">
<p class="title">Upload media</p>
<div class="drop-zone">
<span id="upload-text-{$elId}"> Drop media here </span>
<div class="loading-overlay" id="upload-loading-{$elId}">
<div class="loader"></div>
</div>
</div>
<img id="upload-img-{$elId}" src="/images/animations/storage-2.png" alt="" />
</div>
<style lang="scss">
.inner-phone {
padding-block: 3rem;
padding-inline: 1rem;
color: rgba(67, 67, 71, 1);
text-align: left;
position: relative;
height: 100%;
overflow: visible;
.header {
display: flex;
justify-content: space-between;
align-items: center;
.title {
color: var(--color-greyscale-800, #2d2d31);
font-family: Inter;
font-size: 1rem;
font-style: normal;
font-weight: 600;
line-height: 1.375rem; /* 137.5% */
letter-spacing: -0.014rem;
}
[class*='icon-'] {
font-size: 1.25rem;
color: hsl(var(--web-color-greyscale-500));
}
}
.date {
margin-block-start: 3rem;
color: hsl(var(--web-color-greyscale-600));
font-family: Inter;
font-size: 0.75rem;
font-style: normal;
font-weight: 500;
line-height: 1.25rem; /* 166.667% */
}
.tasks {
margin-block-start: 0.5rem;
display: flex;
flex-direction: column;
gap: 0.5rem;
.task {
display: flex;
align-items: center;
gap: 0.75rem;
border-radius: 0.5rem;
border: 1px solid hsl(var(--web-color-greyscale-50));
background: hsl(var(--web-color-white));
color: var(--greyscale-700, var(--color-greyscale-700, #56565c));
padding-block: 0.55rem;
padding-inline: 0.88rem;
/* Responsive/SubBody-400 */
font-family: Inter;
font-size: 0.875rem;
font-style: normal;
font-weight: 400;
line-height: 1.375rem; /* 157.143% */
letter-spacing: -0.00394rem;
transition: opacity 200ms ease;
&[data-checked] {
opacity: 0.6;
}
}
}
.add-btn {
position: absolute;
right: 1rem;
bottom: 2.5rem;
width: 2.5rem;
height: 2.5rem;
flex-shrink: 0;
box-shadow: 0px 2px 6px 0px rgba(0, 0, 0, 0.12);
background-color: rgba(124, 103, 254, 1);
color: rgba(237, 237, 240, 1);
font-size: 1.5rem;
display: grid;
place-items: center;
border-radius: 100%;
}
}
.overlay {
opacity: 0;
position: absolute;
inset: 0;
overflow: hidden;
background: rgba(0, 0, 0, 0.32);
border-radius: 2rem;
.drawer {
position: absolute;
bottom: 0;
height: 60%;
opacity: 0;
background-color: white;
border-radius: 0.88463rem 0.88463rem 2rem 2rem;
padding: 1rem;
.title {
color: #434347;
font-family: Inter;
font-size: 0.88463rem;
font-style: normal;
font-weight: 600;
line-height: 1.21638rem; /* 137.5% */
letter-spacing: -0.01238rem;
}
.subtitle {
color: var(--greyscale-500, var(--color-greyscale-500, #818186));
font-family: Inter;
font-size: 0.77406rem;
font-style: normal;
font-weight: 400;
line-height: 1.10575rem; /* 142.857% */
letter-spacing: -0.01081rem;
margin-block-start: 0.2rem;
}
.upload {
display: flex;
padding: 0.44231rem 0.66344rem;
justify-content: center;
align-items: center;
align-self: stretch;
border-radius: 0.66344rem;
border: 1px dashed #d9d9d9;
color: var(--greyscale-500, var(--color-greyscale-500, #818186));
font-family: Inter;
font-size: 0.77406rem;
font-style: normal;
font-weight: 400;
line-height: 1.10575rem; /* 142.857% */
letter-spacing: -0.01081rem;
margin-block-start: 2rem;
}
}
}
.upload-media {
position: absolute;
right: calc(0% - 24px);
bottom: 8rem;
background-color: white;
box-shadow: 0px 4px 24px 0px rgba(0, 0, 0, 0.05);
border-radius: 1rem;
padding: 0.75rem;
opacity: 0;
.title {
color: var(--color-greyscale-800, #2d2d31);
font-family: Inter;
font-size: 0.85rem;
font-style: normal;
font-weight: 600;
line-height: 1.375rem; /* 137.5% */
letter-spacing: -0.014rem;
}
.drop-zone {
display: grid;
place-items: center;
border: 0.885px dashed #d9d9d9;
border-radius: 0.5rem;
color: var(--greyscale-500, var(--color-greyscale-500, #818186));
padding: 2rem 1.25rem;
margin-block-start: 0.5rem;
font-size: 0.65rem;
font-family: Inter;
position: relative;
overflow: hidden;
.loading-overlay {
position: absolute;
inset: 0;
opacity: 0;
z-index: 100;
display: grid;
place-items: center;
}
}
img {
position: absolute;
left: 0;
top: 0;
opacity: 0;
}
}
.images {
display: flex;
margin-block-start: 0.5rem;
gap: 0.5rem;
}
</style>

View File

@@ -1,245 +1,34 @@
import type { Action } from 'svelte/action';
import {
animate as motionAnimate,
type ElementOrSelector,
type MotionKeyframesDefinition,
type AnimationOptionsWithOverrides,
animate
} from 'motion';
export function animation(
elementOrSelector: ElementOrSelector,
keyframes: MotionKeyframesDefinition,
options?: AnimationOptionsWithOverrides
) {
const play = () => {
const played = motionAnimate(elementOrSelector, keyframes, options);
return played;
};
const reverse = () => {
const reversedKeyframes = Object.fromEntries(
Object.entries(keyframes).map(([key, keyframe]) => {
return [key, Array.isArray(keyframe) ? [...keyframe].reverse() : keyframe];
})
) as typeof keyframes;
const reversed = motionAnimate(elementOrSelector, reversedKeyframes, options);
return reversed;
};
return {
play,
reverse
};
}
export type Animation = ReturnType<typeof animation>;
export const safeAnimate = (
elementOrSelector: ElementOrSelector,
keyframes: MotionKeyframesDefinition,
options?: AnimationOptionsWithOverrides
) => {
try {
return animate(elementOrSelector, keyframes, options);
} catch {
// do nothing lol
}
};
type Unsubscriber = () => void;
type PreviousScroll = 'before' | 'after' | undefined;
type ScrollCallbackState = {
previous?: PreviousScroll;
unsubscribe?: Unsubscriber;
executedCount: number;
};
export type ScrollCallback = {
percentage: number;
whenAfter?: (args: Omit<ScrollCallbackState, 'unsubscribe'>) => Unsubscriber | void;
};
export function createScrollHandler(callbacks: ScrollCallback[]) {
const states: ScrollCallbackState[] = callbacks.map(() => ({
executedCount: 0
}));
const handler = function (scrollPercentage: number) {
callbacks.forEach((callback, i) => {
const { percentage, whenAfter } = callback;
const { previous, unsubscribe, executedCount } = states[i];
if (scrollPercentage >= percentage && previous !== 'after') {
// Execute whenAfter
states[i].unsubscribe = whenAfter?.({ previous, executedCount }) ?? undefined;
states[i].previous = 'after';
if (whenAfter) {
states[i].executedCount++;
}
} else if (scrollPercentage < percentage && previous === 'after') {
unsubscribe?.();
states[i].unsubscribe = undefined;
states[i].previous = 'before';
}
});
};
handler.reset = () => {
states.forEach((state) => {
// state.unsubscribe?.();
state.unsubscribe = undefined;
state.previous = undefined;
state.executedCount = 0;
});
};
return handler;
}
export type ScrollInfo = {
percentage: number;
traversed: number;
remaning: number;
};
export const scroll: Action<
HTMLElement,
undefined,
{
'on:web-scroll': (e: CustomEvent<ScrollInfo>) => void;
'on:web-resize': (e: CustomEvent<ScrollInfo>) => void;
}
> = (node) => {
function getScrollInfo(): ScrollInfo {
const { top, height } = node.getBoundingClientRect();
const { innerHeight } = window;
const scrollHeight = height - innerHeight;
const scrollPercentage = (-1 * top) / scrollHeight;
const traversed = scrollPercentage * scrollHeight;
const remaning = scrollHeight - traversed;
return {
percentage: scrollPercentage,
traversed,
remaning
};
}
const createHandler = (eventName: 'web-scroll' | 'web-resize') => {
return () => {
node.dispatchEvent(
new CustomEvent<ScrollInfo>(eventName, {
detail: getScrollInfo()
})
);
};
};
const handleScroll = createHandler('web-scroll');
const handleResize = createHandler('web-resize');
handleScroll();
handleResize();
window.addEventListener('scroll', handleScroll);
window.addEventListener('resize', handleResize);
return {
destroy() {
window.removeEventListener('scroll', handleScroll);
window.removeEventListener('resize', handleResize);
}
};
};
type TimelineEvent = {
at: number;
callback: () => void;
};
export function createTimeline(events: TimelineEvent[]) {
let timeoutIds: NodeJS.Timeout[] = [];
const play = () => {
events.forEach((event) => {
const timeoutId = setTimeout(event.callback, event.at);
timeoutIds.push(timeoutId);
});
};
const cancel = () => {
timeoutIds.forEach(clearTimeout);
timeoutIds = [];
};
return { play, cancel };
}
type ProgressEvent = {
percentage: number;
callback: () => void;
};
/**
* Given a list of events, create a sequence of events that will be executed
* when a given percentage is greater than the event percentage, and before
* the next event percentage.
* e.g. const handler = createProgressSequence(events) // where there's an event for each 0.1 percentage
* handler(0.45) // will execute the event with percentage 0.4.
*/
export function createProgressSequence(events: ProgressEvent[]) {
// Sort from highest to lowest percentage
const sortedEvents = [...events].sort((a, b) => b.percentage - a.percentage);
let lastEventIdx = -1;
const handler = (percentage: number) => {
const idx = sortedEvents.findIndex((event) => event.percentage <= percentage);
if (idx === lastEventIdx) {
return;
}
const event = sortedEvents[idx];
event?.callback();
lastEventIdx = idx;
};
handler.resetLastEventIdx = () => {
lastEventIdx = -1;
};
return handler;
}
export type ProgressSequence = ReturnType<typeof createProgressSequence>;
export function write(text: string, cb: (v: string) => void, duration = 500) {
if (text.length === 0) {
cb('');
return Promise.resolve();
}
const step = duration / text.length;
let i = 0;
return new Promise((resolve) => {
return new Promise<void>((resolve) => {
const interval = setInterval(() => {
cb(text.slice(0, ++i));
if (i === text.length) {
clearInterval(interval);
resolve(undefined);
resolve();
}
}, step);
});
}
export function unwrite(text: string, cb: (v: string) => void, duration = 500) {
if (text.length === 0) {
cb('');
return Promise.resolve();
}
const step = duration / text.length;
let i = text.length;
return new Promise((resolve) => {
return new Promise<void>((resolve) => {
const interval = setInterval(() => {
cb(text.slice(0, --i));
if (i === 0) {
clearInterval(interval);
resolve(undefined);
resolve();
}
}, step);
});
@@ -250,11 +39,3 @@ export function sleep(duration: number) {
setTimeout(resolve, duration);
});
}
export function getInitials(name: string) {
return name
.split(' ')
.map((word) => word?.[0]?.toUpperCase() ?? '')
.join('')
.slice(0, 2);
}

View File

@@ -1,45 +0,0 @@
<script lang="ts">
import { rect } from '$lib/actions';
import { clamp } from '$lib/utils/clamp';
import { onMount } from 'svelte';
import { writable } from 'svelte/store';
export let percentage = 0;
let easedPercentage = 0;
const elRect = writable<DOMRect | null>(null);
$: y = $elRect ? clamp(0, easedPercentage, 1) * $elRect.height : 0;
onMount(() => {
let frame: number | null = null;
const ease = () => {
easedPercentage += percentage - easedPercentage;
frame = window.requestAnimationFrame(ease);
};
ease();
return () => {
frame && window.cancelAnimationFrame(frame);
};
});
</script>
<div
class="scroll-indicator relative h-full w-px shrink-0 rounded-full"
use:rect={elRect}
style:--y={`${y}px`}
style:--percentage={`${easedPercentage * 100}%`}
>
<div class="absolute -top-[8px] left-1/2"></div>
</div>
<style lang="scss">
.scroll-indicator {
background: linear-gradient(
to bottom,
hsl(var(--web-color-accent)) 0%,
hsl(var(--web-color-greyscale-700)) var(--percentage),
hsl(var(--web-color-greyscale-700)) 100%
);
}
</style>

View File

@@ -1,14 +0,0 @@
import { APPWRITE_API_KEY_INIT } from '$env/static/private';
import { PUBLIC_APPWRITE_ENDPOINT, PUBLIC_APPWRITE_PROJECT_INIT_ID } from '$env/static/public';
import { Account, Client, Databases } from '@appwrite.io/console';
const clientServer = new Client();
clientServer
.setEndpoint(PUBLIC_APPWRITE_ENDPOINT)
.setProject(PUBLIC_APPWRITE_PROJECT_INIT_ID)
.setKey(APPWRITE_API_KEY_INIT);
export const appwriteInitServer = {
account: new Account(clientServer),
databases: new Databases(clientServer)
};

View File

@@ -1,10 +0,0 @@
import { PUBLIC_APPWRITE_ENDPOINT, PUBLIC_APPWRITE_PROJECT_INIT_ID } from '$env/static/public';
import { Client, Account } from '@appwrite.io/console';
const client = new Client();
client.setEndpoint(PUBLIC_APPWRITE_ENDPOINT).setProject(PUBLIC_APPWRITE_PROJECT_INIT_ID);
export const appwriteInit = {
client,
account: new Account(client)
};

View File

@@ -1,8 +1,7 @@
<script lang="ts">
import { trackEvent } from '$lib/actions/analytics';
import { createDialog, melt } from '@melt-ui/svelte';
import { fade, scale } from 'svelte/transition';
import { Button, Icon } from '$lib/components/ui';
import { createDialog, melt } from '@melt-ui/svelte';
const {
elements: { portalled, trigger, content, overlay },
@@ -13,14 +12,10 @@
</script>
<Button
class="cursor-pointer shadow-[0_2px_40px_rgba(0,0,0,0.5)] transition-opacity hover:opacity-90 active:scale-95"
action={trigger}
onclick={() => {
trackEvent({
plausible: { name: 'Appwrite in 100 seconds' },
posthog: { name: 'intro-video-btn_hero_click' }
});
}}
event="intro-video-btn_hero-click"
variant="secondary"
class="w-full! cursor-pointer shadow-[0_2px_40px_rgba(0,0,0,0.5)] transition-opacity hover:opacity-90 active:scale-95 lg:w-fit!"
>
Appwrite in 100 seconds

View File

@@ -7,11 +7,13 @@
</script>
<div
class="bg relative mt-12 -mb-16 flex min-h-[12rem] items-center justify-center overflow-hidden border-t border-[hsl(var(--web-color-subtle))] py-12"
class="bg relative mt-12 -mb-6 flex min-h-[12rem] items-center justify-center overflow-hidden border-t border-[hsl(var(--web-color-subtle))] py-12"
>
<div class="flex max-w-3xs flex-col items-center justify-center gap-5 text-center">
<h2 class="text-label text-primary font-aeonik-pro">{heading}</h2>
<Button href={getAppwriteDashboardUrl()}>{label}</Button>
<Button href={getAppwriteDashboardUrl()} event="blog-cta-get_started_btn-click"
>{label}</Button
>
</div>
</div>

View File

@@ -1,6 +1,7 @@
<script lang="ts">
import { createAccordion, melt } from '@melt-ui/svelte';
import { slide } from 'svelte/transition';
import { trackEvent } from '$lib/actions/analytics';
import { createAccordion, melt } from '@melt-ui/svelte';
export let noBorder = false;
@@ -38,7 +39,8 @@
{ label: 'Messaging', href: '/products/messaging' },
{ label: 'Storage', href: '/products/storage' },
{ label: 'Realtime', href: '/docs/apis/realtime' },
{ label: 'Network', href: '/docs/products/network' }
{ label: 'Network', href: '/docs/products/network' },
{ label: 'Hosting', href: '/docs/products/sites' }
],
Learn: [
{ label: 'Blog', href: '/blog' },
@@ -96,7 +98,7 @@
<nav
aria-label="Footer"
class="web-footer-nav relative mt-24"
class="web-footer-nav relative container mt-24"
class:web-u-sep-block-start={!noBorder}
>
<img class="web-logo" src="/images/logos/appwrite.svg" alt="appwrite" height="24" width="130" />
@@ -111,7 +113,16 @@
<ul class="web-footer-nav-secondary-list text-sub-body">
{#each items as { href, label, target, rel }}
<li>
<a class="web-link" {href} {target} {rel}>{label}</a>
<a
class="web-link"
{href}
{target}
{rel}
onclick={() =>
trackEvent(
`footer-${label.toLowerCase().replace(' ', '_')}-click`
)}>{label}</a
>
</li>
{/each}
</ul>
@@ -141,7 +152,16 @@
>
{#each items as { href, label, target, rel }}
<li>
<a class="web-link" {href} {target} {rel}>{label}</a>
<a
class="web-link"
{href}
{target}
{rel}
onclick={() =>
trackEvent(
`footer-${label.toLowerCase().replace(' ', '_')}-click`
)}>{label}</a
>
</li>
{/each}
</ul>

View File

@@ -1,31 +1,21 @@
<script lang="ts">
import { classNames } from '$lib/utils/classnames';
import { trackEvent } from '$lib/actions/analytics';
import { browser } from '$app/environment';
import { getAppwriteDashboardUrl } from '$lib/utils/dashboard';
import { Button } from '$lib/components/ui';
import { classNames } from '$lib/utils/classnames';
import { getAppwriteDashboardUrl } from '$lib/utils/dashboard';
interface Props {
class?: string;
}
const { class: className }: Props = $props();
const isLoggedIn = browser && 'loggedIn' in document.body.dataset;
function getTrackingEventName() {
return browser ? (isLoggedIn ? 'Go to console' : 'Start building') : 'Start building';
}
</script>
<Button
class={classNames('web-u-inline-width-100-percent-mobile', className)}
href={getAppwriteDashboardUrl()}
onclick={() =>
trackEvent({
plausible: { name: `${getTrackingEventName()} in header` },
...(isLoggedIn ? {} : { posthog: { name: 'get-started-btn_nav_click' } })
})}
event="main-get_started_btn_nav-click"
class={classNames('web-u-inline-width-100-percent-mobile', className)}
>
<span class="hidden group-[&[data-logged-in]]/body:block" aria-hidden={!isLoggedIn}
>Go to Console</span

View File

@@ -1,5 +1,15 @@
<script lang="ts">
export let title = "Trusted by developers from the world's leading organizations";
import { classNames } from '$lib/utils/classnames';
type Props = {
title?: string;
class?: string;
};
const {
title = "Trusted by developers from the world's leading organizations",
class: className
}: Props = $props();
const logos = [
{
@@ -77,14 +87,12 @@
];
</script>
<div class="my-32">
<div class={classNames('py-32', className)}>
<div class="container">
<h2
class="font-aeonik-pro text-greyscale-100 mx-auto max-w-xl text-center text-4xl leading-10"
>
<h2 class="font-aeonik-pro text-greyscale-100 text-label mx-auto max-w-md text-center">
{title}
</h2>
<ul class="grid grid-cols-3 gap-10 pt-20 text-center md:grid-cols-6">
<ul class="grid grid-cols-3 gap-10 pt-10 text-center md:grid-cols-6">
{#each logos as { src, alt, width, height }}
<li class="grid place-content-center">
<img {src} {alt} {width} {height} />

View File

@@ -1,6 +1,7 @@
<script lang="ts">
import { socials } from '$lib/constants';
import ThemeSelect from './ThemeSelect.svelte';
import { trackEvent } from '$lib/actions/analytics';
export let variant: 'homepage' | 'docs' = 'homepage';
@@ -8,7 +9,7 @@
</script>
{#if variant === 'homepage'}
<footer class="web-main-footer relative mt-12">
<footer class="web-main-footer relative mt-12 flex flex-col justify-between gap-10 lg:flex-row">
<ul class="flex gap-2">
{#each socials as social}
<li>
@@ -24,11 +25,11 @@
</li>
{/each}
</ul>
<div class="e-main-footer">
<div class="mt-1 grid grid-cols-2 gap-y-4 md:grid-cols-3">
<div>Copyright © {year} Appwrite</div>
<iframe
class="status w-full md:w-fit md:max-w-[230px]"
class="w-full md:w-fit md:max-w-[230px]"
title="Appwrite Status"
src="https://status.appwrite.online/badge?theme=dark"
height="35"
@@ -38,10 +39,28 @@
style:margin-top="-4px"
></iframe>
<ul class="flex gap-4">
<li><a class="web-link" href="/terms">Terms</a></li>
<li><a class="web-link" href="/privacy">Privacy</a></li>
<li><a class="web-link" href="/cookies">Cookies</a></li>
<ul class="flex gap-4 text-right md:justify-end">
<li>
<a
class="web-link"
href="/terms"
onclick={() => trackEvent(`footer-terms-click`)}>Terms</a
>
</li>
<li>
<a
class="web-link"
href="/privacy"
onclick={() => trackEvent(`footer-privacy-click`)}>Privacy</a
>
</li>
<li>
<a
class="web-link"
href="/cookies"
onclick={() => trackEvent(`footer-cookies-click`)}>Cookies</a
>
</li>
</ul>
</div>
</footer>
@@ -66,13 +85,21 @@
<div class="web-main-footer-grid-1-column-2">
<ThemeSelect />
</div>
<ul class="web-main-footer-grid-1-column-3 web-main-footer-links items-start">
<ul class="web-main-footer-grid-1-column-3 web-main-footer-links items-end text-right">
<li>
<a href="/discord" target="_blank" rel="noopener noreferrer">Support</a>
<a
href="/discord"
target="_blank"
rel="noopener noreferrer"
onclick={() => trackEvent(`footer-support-click`)}>Support</a
>
</li>
<li>
<a href="https://appwrite.online" target="_blank" rel="noopener noreferrer"
>Status</a
<a
href="https://appwrite.online"
target="_blank"
rel="noopener noreferrer"
onclick={() => trackEvent(`footer-appwrite_status-click`)}>Status</a
>
</li>
<!-- <li>
@@ -98,21 +125,4 @@
margin-bottom: 6px; /* balancing due to style:margin-top="-4px" & the `iframe` has some spacings too I think */
}
}
.e-main-footer {
display: flex;
@media #{devices.$break1} {
flex-direction: column;
> * {
padding-block: 1rem;
&:not(:first-child) {
border-block-start: solid 0.0625rem hsl(var(--web-color-border));
}
}
}
@media #{devices.$break2open} {
display: flex;
gap: 2rem;
}
}
</style>

View File

@@ -41,12 +41,7 @@
href={link.href}
data-initialized={initialized ? '' : undefined}
data-badge={link.showBadge ? '' : undefined}
on:click={() => {
trackEvent({
plausible: { name: `${link.label} in header` },
posthog: { name: `${link.label.toLowerCase()}_nav_click` }
});
}}
onclick={() => trackEvent(`main-nav-${link.label.toLowerCase()}-nav-click`)}
>{link.label}
</a>
{/if}

View File

@@ -6,6 +6,7 @@
import { getAppwriteDashboardUrl } from '$lib/utils/dashboard';
import { Button, InlineTag, Icon } from '$lib/components/ui';
import { GithubStats } from '$lib/components/shared';
import { trackEvent } from '$lib/actions/analytics';
export let open = false;
export let links: NavLink[];
@@ -20,7 +21,12 @@
<nav class="web-side-nav web-is-not-desktop" class:hidden={!open}>
<div class="web-side-nav-wrapper ps-4 pe-4">
<div class="flex items-center gap-2 px-4">
<Button href={getAppwriteDashboardUrl('/register')} variant="secondary" class="flex-1">
<Button
href={getAppwriteDashboardUrl('/register')}
variant="secondary"
class="flex-1"
event="mobile_nav-sign_up-click"
>
Sign up
</Button>
<IsLoggedIn class="flex-1" />
@@ -33,7 +39,14 @@
{#if mobileSubmenu}
<svelte:component this={mobileSubmenu} {label} />
{:else}
<a class="web-side-nav-button" {href}>
<a
class="web-side-nav-button"
{href}
onclick={() =>
trackEvent(
`mobile-nav-${label.toLowerCase().replace(' ', '_')}-click`
)}
>
<span class="text-caption">{label}</span>
</a>
{/if}

View File

@@ -15,12 +15,15 @@
let { selected = $bindable('js'), data = [], width = null, height = null }: Props = $props();
let snippets = $derived(writable(new Set(data.map((d) => d.language))));
const getSnippets = () => {
return snippets;
};
let content = $derived(data.find((d) => d.language === selected)?.content ?? '');
let platform = $derived(data.find((d) => d.language === selected)?.platform ?? '');
snippets?.subscribe((n) => {
getSnippets().subscribe((n) => {
if (selected === null && n.size > 0) {
selected = Array.from(n)[0] as Language;
}
@@ -33,7 +36,7 @@
type CopyStatusType = keyof typeof CopyStatus;
type CopyStatusValue = (typeof CopyStatus)[CopyStatusType];
let copyText: CopyStatusValue = CopyStatus.Copy;
let copyText = $state<CopyStatusValue>(CopyStatus.Copy);
async function handleCopy() {
await copy(content);
@@ -60,7 +63,7 @@
</script>
<section
class="dark web-code-snippet mx-auto lg:!max-w-[90vw]"
class="dark web-code-snippet mx-auto w-full lg:!max-w-[90vw]"
aria-label="code-snippet panel"
style={`width: ${width ? width / 16 + 'rem' : 'inherit'}; height: ${
height ? height / 16 + 'rem' : 'inherit'

View File

@@ -1,6 +1,7 @@
<script context="module" lang="ts">
import { PUBLIC_GROWTH_ENDPOINT } from '$env/static/public';
import { Button } from '$lib/components/ui';
import { trackEvent } from '$lib/actions/analytics';
export async function newsletter(name: string, email: string) {
const response = await fetch(`${PUBLIC_GROWTH_ENDPOINT}/newsletter/subscribe`, {
@@ -167,7 +168,11 @@
bind:value={email}
/>
</div>
<Button type="submit" disabled={submitting}>Sign up</Button>
<Button
type="submit"
disabled={submitting}
event="newsletter-subscribe-submit">Sign up</Button
>
{#if error}
<span class="text">
Something went wrong. Please try again later.

View File

@@ -1,7 +1,6 @@
<script lang="ts">
import { trackEvent } from '$lib/actions/analytics';
import { getAppwriteDashboardUrl } from '$lib/utils/dashboard';
import { Button, type Variant } from '$lib/components/ui';
import { getAppwriteDashboardUrl } from '$lib/utils/dashboard';
const plans: Array<{
name: string;
@@ -21,7 +20,7 @@
buttonText: 'Get started',
buttonLink: getAppwriteDashboardUrl('/register'),
buttonVariant: 'secondary',
eventName: 'Get started Free plan'
eventName: 'footer-plans-free-click'
},
{
name: 'Pro',
@@ -33,7 +32,7 @@
buttonText: 'Start building',
buttonLink: getAppwriteDashboardUrl('/console?type=create&plan=tier-1'),
buttonVariant: 'primary',
eventName: 'Get started Pro plan'
eventName: 'footer-plans-pro-click'
},
{
name: 'Scale',
@@ -44,7 +43,7 @@
buttonText: 'Start building',
buttonLink: getAppwriteDashboardUrl('/console?type=create&plan=tier-2'),
buttonVariant: 'secondary',
eventName: 'Get started Scale plan'
eventName: 'footer-plans-scale-click'
},
{
name: 'Enterprise',
@@ -53,7 +52,7 @@
buttonText: 'Contact us',
buttonLink: '/contact-us/enterprise',
buttonVariant: 'secondary',
eventName: 'Get started Enterprise plan'
eventName: 'footer-plans-enterprise-click'
}
];
</script>
@@ -73,9 +72,9 @@
</h2>
<Button
variant="transparent"
href={getAppwriteDashboardUrl()}
class="self-center"
onclick={() => trackEvent({ plausible: { name: 'Get started in pre footer' } })}
href={getAppwriteDashboardUrl()}
event="footer-plans-get_started-click"
>
<span class="text">Get started</span>
</Button>
@@ -117,15 +116,9 @@
{plan.description}
</p>
<Button
event={plan.eventName}
variant={plan.buttonVariant}
href={plan.buttonLink}
class="w-full! flex-3 self-end md:w-fit"
onclick={() =>
trackEvent({
plausible: {
name: plan.eventName
}
})}
>
<span class="text" style:padding-inline="0.5rem">{plan.buttonText}</span
>

View File

@@ -1,9 +1,9 @@
<script lang="ts">
import { classNames } from '$lib/utils/classnames';
import { melt, createCollapsible } from '@melt-ui/svelte';
import { slide } from 'svelte/transition';
import { products, sublinks } from './ProductsSubmenu.svelte';
import { classNames } from '$lib/utils/classnames';
import { trackEvent } from '$lib/actions/analytics';
import { melt, createCollapsible } from '@melt-ui/svelte';
import { products, sublinks } from './ProductsSubmenu.svelte';
export let label: string;
@@ -34,13 +34,11 @@
{#each products as product}
<a
href={product.href}
onclick={() =>
trackEvent(
`products-mobile_submenu-${product.name.toLowerCase()}-click`
)}
class="group flex gap-3 rounded-xl p-2 text-white transition-colors outline-none focus:bg-white/8"
on:click={() =>
trackEvent({
plausible: {
name: `${product.name} in products submenu`
}
})}
>
<div
class="flex size-12 shrink-0 items-center justify-center rounded-lg border border-white/12 bg-white/6"

View File

@@ -43,6 +43,12 @@
description: 'Set up a full-functioning messaging service.',
icon: '/images/icons/illustrated/dark/messaging.png'
},
{
name: 'Sites',
href: '/products/sites',
description: 'The open-source Vercel alternative.',
icon: '/images/icons/illustrated/dark/sites.png'
},
{
name: 'Realtime',
href: '/docs/apis/realtime',
@@ -59,15 +65,19 @@
{
label: 'Appwrite vs. Firebase',
href: '/blog/post/open-source-firebase-alternative'
},
{
label: 'Appwrite vs. Vercel',
href: '/blog/post/open-source-vercel-alternative'
}
];
</script>
<script lang="ts">
import { dev } from '$app/environment';
import { trackEvent } from '$lib/actions/analytics';
import { classNames } from '$lib/utils/classnames';
import { createDropdownMenu, melt } from '@melt-ui/svelte';
import Icon from './ui/icon';
const {
elements: { trigger, menu, item, overlay },
@@ -117,12 +127,8 @@
<a
href={product.href}
use:melt={$item}
on:click={() =>
trackEvent({
plausible: {
name: `${product.name} in products submenu`
}
})}
onclick={() =>
trackEvent(`products-submenu-${product.name.toLowerCase()}-click`)}
class="group flex gap-3 rounded-xl p-1 text-white transition-colors outline-none focus:bg-white/8"
>
<div
@@ -160,15 +166,16 @@
>
<header class="flex items-center justify-between">
<span
class="font-aeonik-fono tracking-loose text-secondary block text-xs uppercase"
class="font-aeonik-fono tracking-loose text-primary block text-xs uppercase"
>Customer Stories<span class="text-accent">_</span></span
>
<a
href="/blog/category/customer-stories"
class="text-primary text-caption flex items-center gap-2"
>See more <span
class="web-icon-chevron-right transition-transform group-hover:translate-x-0.5"
></span></a
class="text-secondary text-caption flex items-center"
>Read more customer stories <Icon
name="chevron-right"
class="transition-transform group-hover:translate-x-0.5"
></Icon></a
>
</header>
@@ -182,8 +189,8 @@
class="aspect-[3/1] max-w-[7.5rem] shrink-0 rounded-xl object-cover"
/>
<p class="text-pretty">
Empowering Shopify merchants with real-time store monitoring using
StoreAlert
Appwrited helped reduce development time by 60%, and lower server costs
by 40%.
</p>
</a>
</div>

View File

@@ -165,7 +165,7 @@
<div id="searchbox"></div>
<input
class="web-input-button bg-greyscale-800/75! relative z-1 !rounded-b-none !pl-10"
class="web-input-button bg-white-800/75! relative z-1 !rounded-b-none !pl-10"
type="text"
id="search"
bind:value

View File

@@ -75,7 +75,10 @@
<a
href={platform.href}
class="web-icon-button web-box-icon has-border-gradient"
on:click={() => trackEvent({ plausible: { name: `${platform.name} clicked` } })}
onclick={() =>
trackEvent(
`technologies-${platform.name.replace(' ', '-').toLowerCase()}-click`
)}
>
<img
src={platform.image}

View File

@@ -1,16 +0,0 @@
<script lang="ts">
export let text: string;
const words = text.split(' ');
</script>
<span class="sr-only">{text}</span>
<span class="relative">
{#each words as word, i}
<span
class="animate-enter mr-2 inline-block"
style:animation-delay="{i * 75}ms
">{word}</span
>
{/each}
</span>

View File

@@ -5,9 +5,7 @@ export const pins = {
lng: -77.49,
city: 'Ashburn',
code: 'ASH',
available: true,
offsetX: 10,
offsetY: -10
available: true
},
{
lat: 33.75,
@@ -562,9 +560,7 @@ export const pins = {
lng: -74.01,
city: 'New York',
code: 'NYC',
available: true,
offsetX: 10,
offsetY: -10
available: true
},
{
lat: 50.11,
@@ -579,6 +575,48 @@ export const pins = {
city: 'Sydney',
code: 'AUS',
available: true
},
{
lat: 1.35,
lng: 103.82,
city: 'Singapore',
code: 'SIN',
date: 'Q4 2025'
},
{
lat: 37.77,
lng: -122.42,
city: 'San Francisco',
code: 'SFO',
date: 'Q4 2025'
},
{
lat: 12.97,
lng: 77.59,
city: 'Bangalore',
code: 'BLR',
date: 'Planned'
},
{
lat: 52.37,
lng: 4.9,
city: 'Amsterdam',
code: 'AMS',
date: 'Planned'
},
{
lat: 51.51,
lng: -0.13,
city: 'London',
code: 'LON',
date: 'Planned'
},
{
lat: 43.65,
lng: -79.38,
city: 'Toronto',
code: 'TOR',
date: 'Planned'
}
],
regions: [
@@ -587,9 +625,7 @@ export const pins = {
lng: -74.01,
city: 'New York',
code: 'NYC',
available: true,
offsetX: 10,
offsetY: -10
available: true
},
{
lat: 50.11,
@@ -598,13 +634,54 @@ export const pins = {
code: 'FRA',
available: true
},
{
lat: -33.87,
lng: 151.21,
city: 'Sydney',
code: 'AUS',
available: true
},
{
lat: 1.35,
lng: 103.82,
city: 'Singapore',
code: 'SIN',
date: 'Q4 2025'
},
{
lat: 37.77,
lng: -122.42,
city: 'San Francisco',
code: 'SFO',
date: 'Q4 2025'
},
{
lat: 12.97,
lng: 77.59,
city: 'Bangalore',
code: 'BLR',
date: 'Planned'
},
{
lat: 52.37,
lng: 4.9,
city: 'Amsterdam',
code: 'AMS',
date: 'Planned'
},
{
lat: 51.51,
lng: -0.13,
city: 'London',
code: 'LON',
date: 'Planned'
},
{
lat: 43.65,
lng: -79.38,
city: 'Toronto',
code: 'TOR',
date: 'Planned'
}
]
};

View File

@@ -1,64 +0,0 @@
<script lang="ts">
import { classNames } from '$lib/utils/classnames';
import { slugify } from '$lib/utils/slugify';
import { latLongToSvgPosition } from './utils/projections';
import { tooltipData } from './map-tooltip.svelte';
interface Props {
city: string;
code: string;
index: number;
lat: number;
lng: number;
bounds: {
north: number;
south: number;
west: number;
east: number;
};
available: boolean;
class?: string;
animate?: boolean;
}
const { city, code, index = 0, lat, lng, available, animate = false }: Props = $props();
const position = $derived(latLongToSvgPosition({ latitude: lat, longitude: lng }));
const handleSetActiveMarker = () => {
tooltipData.set({
city,
code,
available
});
};
const handleResetActiveMarker = () => {
tooltipData.set({
city: null,
code: null,
available: null
});
};
</script>
<button
class={classNames(
'group absolute z-10 flex size-2 cursor-pointer items-center justify-center opacity-0 [animation-delay:var(--delay)]',
{ 'animate-fade-in': animate }
)}
style="left: {position.x}%; top: {position.y}%;--delay: {index * 10}ms;"
data-region={slugify(city)}
onmouseenter={handleSetActiveMarker}
onfocus={handleSetActiveMarker}
onmouseleave={handleResetActiveMarker}
onblur={handleResetActiveMarker}
aria-label={city}
>
<span
class="from-accent/20 to-accent/10 border-gradient ease-spring pointer-events-none absolute inline-flex h-5 w-5 rounded-full bg-gradient-to-b opacity-0 transition-opacity group-hover:animate-ping group-hover:opacity-75 before:rounded-full"
style:animation-duration="1.5s"
></span>
<span class="bg-accent absolute inline-flex h-full w-full rounded-full"></span>
<span class="absolute size-1/2 rounded-full bg-white/80 transition-all"></span>
</button>

View File

@@ -1,55 +1,104 @@
<script lang="ts">
import { classNames } from '$lib/utils/classnames';
import { Tabs } from 'bits-ui';
import type { IconType } from '../ui';
import Icon from '../ui/icon';
import Icon from '../ui/icon/icon.svelte';
import { classNames } from '$lib/utils/classnames';
import { trackEvent } from '$lib/actions/analytics';
type Props = {
const {
onValueChange,
theme = 'dark'
}: {
onValueChange: (value: string) => void;
};
theme?: 'light' | 'dark';
} = $props();
const { onValueChange }: Props = $props();
const navItems: Array<{ label: string; value: string; icon: IconType }> = [
const navItems = [
{
label: 'PoP Locations',
value: 'pop-locations',
icon: 'pop-locations'
icon: 'pop-locations',
href: '/docs/products/network/cdn',
description: 'Points of presence ensure <50ms ping around the globe.'
},
{
label: 'Edges',
value: 'edges',
icon: 'edge'
icon: 'edge',
href: '/docs/products/network/edges#list',
description: 'Edges bring compute closer to users for faster response times.'
},
{
label: 'Regions',
value: 'regions',
icon: 'regions'
icon: 'regions',
href: '/docs/products/network/regions#list',
description: 'Regions offer data residency and redundancy across continents.'
}
];
] satisfies Array<{
label: string;
value: string;
icon: IconType;
href: string;
description: string;
}>;
let activeIndex = $state(0);
</script>
<Tabs.Root
value={navItems[0].value}
{onValueChange}
class="flex flex-col items-center justify-center gap-12 md:-mt-8"
>
<Tabs.List
class="border-smooth animate-fade-in bg-card relative grid w-full max-w-xl grid-cols-1 place-content-center gap-3 p-1 px-8 drop-shadow-md md:grid-cols-3 md:rounded-full md:border md:px-1"
<div class="flex flex-col gap-4 text-center">
<Tabs.Root
onValueChange={(value) => {
activeIndex = navItems.findIndex((item) => item.value === value);
onValueChange(value);
}}
value={navItems[0]?.value}
class="flex flex-col items-center justify-center gap-12 md:mt-9"
>
{#each navItems as { label, icon, value }, index}
<Tabs.Trigger
{value}
class={classNames(
'text-caption animate-enter text-primary bg-smooth border-smooth flex h-8 cursor-pointer items-center justify-center gap-2 rounded-full border font-medium outline-0 transition-colors hover:border-white/12',
'group data-[state="active"]:bg-accent/4 data-[state="active"]:border-accent/36 data-[state="active"]:text-white'
)}
style="animation-delay:{index * 75}ms;"
>
<Icon name={icon} class="group-data-[state='active']:text-accent -ml-2" />
{label}</Tabs.Trigger
>
{/each}
</Tabs.List>
</Tabs.Root>
<Tabs.List
class={classNames(
'border-smooth animate-fade-in relative grid w-full max-w-xl grid-cols-1 place-content-center gap-3 overflow-hidden p-1 px-8 shadow-[0px_4px_8p_rgba(0,0,0,0.04)] md:grid-cols-3 md:rounded-full md:border md:px-1',
theme === 'light' ? 'md:bg-white' : 'md:bg-card'
)}
>
{#each navItems as { label, icon, value }, index}
<Tabs.Trigger
{value}
class={classNames(
'text-caption animate-enter text-primary bg-smooth border-smooth flex h-8 cursor-pointer items-center justify-center gap-2 rounded-full border font-medium outline-0 transition-colors hover:border-white/12',
'group data-[state="active"]:bg-accent/4 data-[state="active"]:border-accent/36 data-[state="active"]:text-accent'
)}
style="animation-delay:{index * 75}ms;"
onclick={() => {
trackEvent(
`network-map-nav-${value.toLowerCase().replace(' ', '-')}-click`
);
}}
>
<Icon
name={icon}
class={classNames(
'-ml-2',
"group-data-[state='active']:text-accent",
theme === 'light' ? 'text-[#19191C]' : 'text-inherit'
)}
/>
{label}
</Tabs.Trigger>
{/each}
</Tabs.List>
</Tabs.Root>
<p class="text-caption text-secondary px-4">
{navItems[activeIndex].description}
<a
class="text-primary group mt-2 flex items-center justify-center gap-0.25 md:hidden"
href={navItems[activeIndex].href}
>Learn more about {navItems[activeIndex].label}
<Icon
name="arrow-right"
class="-rotate-45 transition-all group-hover:translate-x-0.25 group-hover:-translate-y-0.25 group-hover:opacity-100 group-focus:translate-x-0.25 group-focus:-translate-y-0.25 group-focus:opacity-100 xl:opacity-0"
/>
</a>
</p>
</div>

View File

@@ -1,58 +1,124 @@
<script lang="ts" module>
import { classNames } from '$lib/utils/classnames';
import { writable } from 'svelte/store';
import { animate } from 'motion';
export const tooltipData = writable<{
let tooltipData = $state<{
city: string | null;
code: string | null;
available: boolean | null;
available?: boolean | null;
date?: string | null;
}>({
city: null,
code: null,
available: null
available: null,
date: null
});
</script>
<script lang="ts">
type Props = {
coords: {
x: number;
y: number;
export const handleSetActiveTooltip = (
city: string,
code: string,
available?: boolean,
date?: string
) => {
if (timeoutId) {
clearTimeout(timeoutId);
timeoutId = null;
}
tooltipData = {
city,
code,
available,
date
};
};
const { coords }: Props = $props();
let timeoutId: ReturnType<typeof setTimeout> | null = null;
export const handleResetActiveTooltip = (delay?: number) => {
if (timeoutId) {
clearTimeout(timeoutId);
timeoutId = null;
}
if (delay) {
timeoutId = setTimeout(() => {
tooltipData = {
city: null,
code: null,
available: null,
date: null
};
timeoutId = null;
}, delay);
} else {
tooltipData = {
city: null,
code: null,
available: null,
date: null
};
}
};
</script>
{#if $tooltipData.city}
<div
class="pointer-events-none absolute"
style:left="{coords.x - 50}px"
style:top="{coords.y - 50}px"
>
<script lang="ts">
import { classNames } from '$lib/utils/classnames';
interface TooltipProps {
x: number;
y: number;
theme?: 'light' | 'dark';
}
const { x, y, theme = 'light' }: TooltipProps = $props();
let city = $state<HTMLElement | null>(null);
$effect(() => {
if (!city) return;
animate(city, { y: [-5, 0], filter: ['blur(4px)', '0px'] }, { duration: 0.2 });
});
</script>
<div class={classNames('pointer-events-none absolute z-100 hidden md:block', theme)}>
{#if tooltipData.city}
<div
class={classNames(
'bg-card/90 border-gradient relative z-100 flex w-[190px] flex-col gap-2 rounded-[10px] p-2 backdrop-blur-lg before:rounded-[10px] after:rounded-[10px]',
'data-[state="closed"]:animate-menu-out data-[state="instant-open"]:animate-menu-in data-[state="delayed-open"]:animate-menu-in'
'border-gradient relative z-100 flex w-[190px] flex-col gap-2 rounded-[10px] p-2 backdrop-blur-lg before:rounded-[10px] after:rounded-[10px]',
{ 'bg-transparent': theme === 'dark', 'bg-white': theme === 'light' }
)}
style:transform={`translateX(${x + 20}px) translateY(${y - 425}px)`}
>
<span class="text-primary text-caption w-fit">
{$tooltipData.city}
({$tooltipData.code})
</span>
{#if $tooltipData.available}
{#key tooltipData.city}
<span class="text-primary text-caption w-fit" bind:this={city}>
{tooltipData.city}
({tooltipData.code})
</span>
{/key}
{#if tooltipData.available}
<div
class="text-caption flex h-5 items-center justify-center place-self-start rounded-md bg-[#10B981]/24 p-1 text-center text-[#B4F8E2]"
class={classNames(
'text-caption flex h-5 items-center justify-center place-self-start rounded-md p-1 text-center',
{
'bg-[#10B981]/16 text-[#0A714F]': theme === 'light',
'bg-[#10B981]/24 text-[#B4F8E2]': theme === 'dark'
}
)}
>
<span class="text-micro -tracking-tight">Available now</span>
</div>
{:else}
<div
class="text-caption flex h-5 items-center justify-center place-self-start rounded-md bg-white/6 p-1 text-center text-white/60"
class={classNames(
'text-caption text-primary flex h-5 items-center justify-center place-self-start rounded-md bg-black/6 p-1 text-center',
{
'text-primary bg-black/6': theme === 'light',
'text-primary bg-white/6': theme === 'dark'
}
)}
>
<span class="text-micro -tracking-tight">Planned</span>
<span class="text-micro -tracking-tight">{tooltipData.date}</span>
</div>
{/if}
</div>
</div>
{/if}
{/if}
</div>

View File

@@ -1,36 +1,25 @@
<script lang="ts" module>
export const MAP_BOUNDS = $state({
west: -138,
east: 167,
north: 74,
south: -62
});
</script>
<script lang="ts">
import MapMarker from './map-marker.svelte';
import { slugify } from '$lib/utils/slugify';
import { classNames } from '$lib/utils/classnames';
import MapNav from './map-nav.svelte';
import { useMousePosition } from '$lib/actions/mouse-position';
import { useMousePosition } from '$lib/actions/mouse-position.svelte';
import { useAnimateInView } from '$lib/actions/animate-in-view';
import { pins, type PinSegment } from './data/pins';
import MapTooltip from './map-tooltip.svelte';
import { dev } from '$app/environment';
import MapTooltip, {
handleSetActiveTooltip,
handleResetActiveTooltip
} from './map-tooltip.svelte';
import { createMap } from 'svg-dotted-map';
import { browser } from '$app/environment';
let dimensions = $state({
width: 0,
height: 0
});
let activeRegion = $state<string | null>(null);
let activeMarker: HTMLElement | null = null;
let activeSegment = $state<string>('pop-locations');
let activeMarkers = $derived(pins[activeSegment as PinSegment]);
let activeRegion = $state<string | null>(null);
let activeMarker: SVGGElement | null = null;
const { action: mousePosition, position } = useMousePosition();
const { action: inView, animate } = useAnimateInView({});
const { action: inView } = useAnimateInView();
const scrollMarkerIntoView = (marker: HTMLElement) => {
const scrollMarkerIntoView = (marker: SVGGElement) => {
return new Promise<void>((resolve) => {
marker.scrollIntoView({
behavior: 'smooth',
@@ -68,58 +57,82 @@
activeRegion = citySlug;
}
};
const radius = 0.4;
const height = 75;
const { points, addMarkers } = createMap({
width: height * 2,
height,
mapSamples: 5000,
radius
});
const markers = $derived(
addMarkers<{ city: string; code: string; available?: boolean; date?: string }>(
activeMarkers
)
);
type Props = {
theme: 'light' | 'dark';
};
const { theme = 'dark' }: Props = $props();
</script>
<div class="-mt-8 w-full overflow-x-scroll [scrollbar-width:none] md:overflow-x-hidden">
<div
class="sticky left-0 mx-auto block max-w-[calc(100vw_-_calc(var(--spacing)_*-2))] md:hidden"
>
<select
class="web-input-text mx-auto appearance-none"
onchange={(e) => handleSetActiveMarker(e.currentTarget.value)}
>
{#each pins[activeSegment as PinSegment] as pin}
<option value={pin.city}>{pin.city}-({pin.code})</option>
{/each}
</select>
</div>
<div
class="relative mx-auto h-full w-[250vw] [scrollbar-width:none] md:w-fit"
use:inView
use:mousePosition
>
<div class="relative w-full overflow-x-scroll [scrollbar-width:none]">
<div class="relative mx-auto h-full [scrollbar-width:none] md:w-full" use:inView>
<div
class="relative w-full origin-bottom transform-[perspective(25px)_rotateX(1deg)_scale3d(1.4,_1.4,_1)] transition-all [scrollbar-width:none]"
bind:clientWidth={dimensions.width}
bind:clientHeight={dimensions.height}
class="relative mx-auto my-10 h-fit w-full max-w-5xl origin-bottom transform-[perspective(25px)_rotateX(1deg)_scale3d(1.2,_1.2,_1)] transition-all [scrollbar-width:none] md:my-0 md:-translate-x-20"
use:mousePosition
>
<div
class="absolute inset-0 mask-[image:url('/images/appwrite-network/map.svg')] mask-contain mask-no-repeat"
>
<div
class={classNames(
'relative block aspect-square size-40 rounded-full blur-3xl transition-opacity',
'from-accent bg-radial-[circle_at_center] via-white/70 to-white/70',
'transform-[translate3d(calc(var(--mouse-x,_-100%)_*_1_-_16rem),_calc(var(--mouse-y,_-100%)_*_1_-_28rem),0)]'
)}
style:--mouse-x="{$position.x}px"
style:--mouse-y="{$position.y}px"
></div>
</div>
<img
src="/images/appwrite-network/map.svg"
class="pointer-events-none relative -z-10 w-full opacity-10 md:max-h-[525px]"
draggable="false"
alt="Map of the world"
/>
{#each pins[activeSegment as PinSegment] as pin, index}
<MapMarker {...pin} animate={$animate} {index} bounds={MAP_BOUNDS} />
{/each}
<svg viewBox={`0 0 ${height * 2} ${height}`}>
{#each points as point}
<ellipse
cx={point.x}
cy={point.y}
rx={radius}
ry={radius * 1.25}
fill={theme === 'dark' ? 'rgba(255,255,255,.1)' : '#dadadd'}
/>
{/each}
{#each markers as marker}
<g
role="tooltip"
class="animate-fade-in outline-none"
aria-label={`${marker.city} (${marker.code})`}
onmouseover={() =>
handleSetActiveTooltip(
marker.city,
marker.code,
marker.available,
marker.date
)}
onfocus={() =>
handleSetActiveTooltip(
marker.city,
marker.code,
marker.available,
marker.date
)}
onblur={() => handleResetActiveTooltip()}
onmouseout={() => handleResetActiveTooltip()}
data-region={slugify(marker.city)}
>
<circle cx={marker.x} cy={marker.y} r={radius * 1.25} class="fill-accent" />
<circle cx={marker.x} cy={marker.y} r={radius * 0.5} class="fill-white" />
<circle
cx={marker.x}
cy={marker.y}
r={radius * 4}
class="fill-transparent"
/>
</g>
{/each}
</svg>
</div>
</div>
</div>
<MapTooltip coords={$position} />
<MapNav onValueChange={(value) => (activeSegment = value)} />
<MapTooltip {theme} {...position()} />
<MapNav {theme} onValueChange={(value) => (activeSegment = value)} />

View File

@@ -1,24 +0,0 @@
import { MAP_BOUNDS } from '../map.svelte';
const MAP_WIDTH = 1048.25;
const MAP_HEIGHT = 525;
type Coordinates = {
latitude: number;
longitude: number;
};
export const latLongToSvgPosition = ({ latitude, longitude }: Coordinates) => {
const { west, east, north, south } = MAP_BOUNDS;
const lngRatio = (longitude - west) / (east - west);
const latRatio = (latitude - south) / (north - south);
const clampedLngRatio = Math.max(0, Math.min(1, lngRatio));
const clampedLatRatio = Math.max(0, Math.min(1, latRatio));
const x = clampedLngRatio * 100;
const y = (1 - clampedLatRatio) * 100;
return { x, y }; // percentages, e.g., { x: 42.3, y: 71.8 }
};

View File

@@ -97,7 +97,11 @@
name="email"
bind:value={email}
/>
<Button type="submit" disabled={submitting}>Sign up</Button>
<Button
type="submit"
disabled={submitting}
event="newsletter-subscribe-submit">Sign up</Button
>
</div>
{#if error}

View File

@@ -0,0 +1,25 @@
<script lang="ts">
import { classNames } from '$lib/utils/classnames';
interface Props {
text: string;
class?: string;
}
let { text, class: className = '' }: Props = $props();
const words = text.split(' ');
</script>
<span class="sr-only">{text}</span>
<span class={classNames('relative overflow-hidden', className)}>
{#each words as word, i}
<span class="relative overflow-hidden">
<span
class="animate-enter mr-2 inline-block"
style:animation-delay="{i * 75}ms
">{word}</span
>
</span>
{/each}
</span>

Some files were not shown because too many files have changed in this diff Show More