Merge branch 'main' into partners-catalog
@@ -2,7 +2,7 @@ PUBLIC_APPWRITE_COL_MESSAGES_ID=
|
||||
PUBLIC_APPWRITE_COL_THREADS_ID=
|
||||
PUBLIC_APPWRITE_DB_MAIN_ID=
|
||||
PUBLIC_APPWRITE_FN_TLDR_ID=
|
||||
PUBLIC_APPWRITE_ENDPOINT=
|
||||
PUBLIC_APPWRITE_ENDPOINT=https://cloud.appwrite.io/v1
|
||||
PUBLIC_APPWRITE_PROJECT_ID=
|
||||
PUBLIC_APPWRITE_DASHBOARD=https://cloud.appwrite.io
|
||||
PUBLIC_APPWRITE_PROJECT_INIT_ID=
|
||||
|
||||
@@ -15,7 +15,7 @@ The Appwrite Website has been built with the following frameworks:
|
||||
|
||||
## Development
|
||||
|
||||
_If this is your first time setting up the repository, please run `pnpm install` inside the repo's directory._
|
||||
_If this is your first time setting up the repository, please run `pnpm install` inside the repo's directory and create a `.env` file based on `.env.example`._
|
||||
|
||||
To get the repo up and running in your local environment, use the following command:
|
||||
|
||||
|
||||
@@ -1,33 +1,51 @@
|
||||
import prettier from 'eslint-config-prettier';
|
||||
import { includeIgnoreFile } from '@eslint/compat';
|
||||
import js from '@eslint/js';
|
||||
import prettier from 'eslint-config-prettier';
|
||||
import svelte from 'eslint-plugin-svelte';
|
||||
import globals from 'globals';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import ts from 'typescript-eslint';
|
||||
import svelteConfig from './svelte.config.js';
|
||||
|
||||
const gitignorePath = fileURLToPath(new URL('./.gitignore', import.meta.url));
|
||||
|
||||
export default ts.config(
|
||||
includeIgnoreFile(gitignorePath),
|
||||
js.configs.recommended,
|
||||
...ts.configs.recommended,
|
||||
...svelte.configs['flat/recommended'],
|
||||
...svelte.configs.recommended,
|
||||
prettier,
|
||||
...svelte.configs['flat/prettier'],
|
||||
...svelte.configs.prettier,
|
||||
{
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.browser,
|
||||
...globals.node
|
||||
}
|
||||
globals: { ...globals.browser, ...globals.node }
|
||||
},
|
||||
rules: {
|
||||
// TODO: remove them one by one
|
||||
'@typescript-eslint/no-unused-vars': 'off',
|
||||
'@typescript-eslint/no-duplicate-enum-values': 'off',
|
||||
'@typescript-eslint/no-empty-object-type': 'off',
|
||||
'@typescript-eslint/no-unused-expressions': 'off',
|
||||
'svelte/infinite-reactive-loop': 'off',
|
||||
'svelte/require-each-key': 'off',
|
||||
'svelte/no-immutable-reactive-statements': 'off',
|
||||
'svelte/no-at-html-tags': 'off',
|
||||
'svelte/no-useless-mustaches': 'off',
|
||||
'svelte/no-reactive-reassign': 'off',
|
||||
'svelte/no-reactive-literals': 'off'
|
||||
}
|
||||
},
|
||||
{
|
||||
files: ['**/*.svelte'],
|
||||
|
||||
files: ['**/*.svelte', '**/*.svelte.ts', '**/*.svelte.js'],
|
||||
ignores: ['eslint.config.js', 'svelte.config.js'],
|
||||
languageOptions: {
|
||||
parserOptions: {
|
||||
parser: ts.parser
|
||||
// Only uncomment this if you want it to take 3 minutes https://github.com/sveltejs/eslint-plugin-svelte/issues/1084
|
||||
// projectService: true,
|
||||
extraFileExtensions: ['.svelte'],
|
||||
parser: ts.parser,
|
||||
svelteConfig
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
ignores: ['build/', '.svelte-kit/', 'dist/']
|
||||
}
|
||||
);
|
||||
|
||||
37
package.json
@@ -12,6 +12,7 @@
|
||||
"download-contributors": "node ./scripts/download-contributor-data.js",
|
||||
"format": "prettier --write .",
|
||||
"format:check": "prettier --check .",
|
||||
"generate:icons": "node ./src/icons/optimize.js",
|
||||
"icons:build": "node ./src/icons/build.js",
|
||||
"icons:generate": "node ./src/icons/optimize.js && node ./src/icons/build.js",
|
||||
"icons:optimize": "node ./src/icons/optimize.js",
|
||||
@@ -23,10 +24,9 @@
|
||||
"optimize": "node ./scripts/optimize-assets.js",
|
||||
"optimize:all": "node ./scripts/optimize-all.js"
|
||||
},
|
||||
"packageManager": "pnpm@10.6.2",
|
||||
"packageManager": "pnpm@10.8.1",
|
||||
"dependencies": {
|
||||
"@number-flow/svelte": "^0.3.3",
|
||||
"@sentry/sveltekit": "^8.51.0",
|
||||
"h3": "^1.14.0",
|
||||
"posthog-js": "^1.210.2",
|
||||
"sharp": "^0.33.5"
|
||||
@@ -36,22 +36,24 @@
|
||||
"@appwrite.io/pink": "~0.26.0",
|
||||
"@appwrite.io/pink-icons": "~0.26.0",
|
||||
"@appwrite.io/repo": "github:appwrite/appwrite#1.6.x",
|
||||
"@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.2",
|
||||
"@melt-ui/svelte": "^0.86.5",
|
||||
"@playwright/test": "^1.50.0",
|
||||
"@sveltejs/adapter-node": "^4.0.1",
|
||||
"@sveltejs/enhanced-img": "^0.1.9",
|
||||
"@sveltejs/kit": "^2.16.1",
|
||||
"@sveltejs/vite-plugin-svelte": "^3.1.2",
|
||||
"@tailwindcss/postcss": "4.0.0-alpha.17",
|
||||
"@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",
|
||||
"@types/compression": "^1.7.5",
|
||||
"@types/glob": "^8.1.0",
|
||||
"@types/markdown-it": "^13.0.9",
|
||||
"@types/morgan": "^1.9.9",
|
||||
"analytics": "^0.8.16",
|
||||
"bits-ui": "^1.3.19",
|
||||
"clsx": "^2.1.1",
|
||||
"cva": "npm:class-variance-authority@^0.7.1",
|
||||
"date-fns": "^3.6.0",
|
||||
@@ -68,6 +70,7 @@
|
||||
"highlight.js": "^11.11.1",
|
||||
"markdown-it": "^14.1.0",
|
||||
"meilisearch": "^0.37.0",
|
||||
"melt": "^0.29.2",
|
||||
"motion": "^10.18.0",
|
||||
"node-html-parser": "^6.1.13",
|
||||
"openapi-types": "^12.1.3",
|
||||
@@ -80,27 +83,27 @@
|
||||
"prettier-plugin-svelte": "^3.3.3",
|
||||
"prettier-plugin-tailwindcss": "^0.6.11",
|
||||
"remeda": "^2.20.0",
|
||||
"reodotdev": "^1.0.0",
|
||||
"sass": "^1.83.4",
|
||||
"svelte": "^4.2.19",
|
||||
"svelte-check": "^3.8.6",
|
||||
"svelte-markdoc-preprocess": "^2.1.0",
|
||||
"svelte": "^5.25.6",
|
||||
"svelte-check": "^4.0.0",
|
||||
"svelte-markdoc-preprocess": "3.0.0",
|
||||
"svelte-markdown": "^0.4.1",
|
||||
"svgtofont": "^4.2.3",
|
||||
"tailwind-merge": "^2.6.0",
|
||||
"tailwindcss": "4.0.0-alpha.17",
|
||||
"tailwind-merge": "^3.0.2",
|
||||
"tailwindcss": "^4.1.2",
|
||||
"tslib": "^2.8.1",
|
||||
"typescript": "^5.7.3",
|
||||
"typescript": "^5.8.2",
|
||||
"typescript-eslint": "^8.21.0",
|
||||
"vite": "^5.4.14",
|
||||
"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": "^1.6.0"
|
||||
"vitest": "^3.1.1"
|
||||
},
|
||||
"pnpm": {
|
||||
"onlyBuiltDependencies": [
|
||||
"@parcel/watcher",
|
||||
"@sentry/cli",
|
||||
"core-js",
|
||||
"esbuild",
|
||||
"sharp",
|
||||
|
||||
3036
pnpm-lock.yaml
generated
@@ -1,12 +1,13 @@
|
||||
import { createApp, fromNodeMiddleware, toNodeListener } from 'h3';
|
||||
import { sitemaps } from './sitemap.js';
|
||||
import { createServer } from 'node:http';
|
||||
import { handler } from '../build/handler.js';
|
||||
import { sitemap } from './sitemap.js';
|
||||
import { createApp, fromNodeMiddleware, toNodeListener } from 'h3';
|
||||
|
||||
async function main() {
|
||||
const port = process.env.PORT || 3000;
|
||||
const app = createApp();
|
||||
app.use('/sitemap.xml', await sitemap());
|
||||
app.use(['/sitemap.xml', '/sitemaps'], await sitemaps());
|
||||
|
||||
app.use(fromNodeMiddleware(handler));
|
||||
const server = createServer(toNodeListener(app)).listen(port);
|
||||
server.addListener('listening', () => {
|
||||
|
||||
@@ -1,28 +1,125 @@
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { createRequire } from 'node:module';
|
||||
import { defineEventHandler, setResponseHeader } from 'h3';
|
||||
import { dirname, join } from 'node:path';
|
||||
import { readFile, stat } from 'node:fs/promises';
|
||||
import { mkdirSync, writeFileSync } from 'node:fs';
|
||||
import {
|
||||
defineEventHandler,
|
||||
getRequestURL,
|
||||
sendRedirect,
|
||||
serveStatic,
|
||||
setResponseHeader
|
||||
} from 'h3';
|
||||
|
||||
/**
|
||||
* @returns {Promise<import('h3').EventHandler>}
|
||||
*/
|
||||
export async function sitemap() {
|
||||
const MAX_THREADS_PER_FILE = 1000;
|
||||
const BASE_URL = 'https://appwrite.io';
|
||||
const BASE_DIR = dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
const SITEMAP_DIR = join(BASE_DIR, './sitemaps');
|
||||
const THREADS_DIR = join(SITEMAP_DIR, 'threads');
|
||||
const NAMED_GROUPS = {
|
||||
blog: '/blog',
|
||||
docs: '/docs',
|
||||
integrations: '/integrations'
|
||||
};
|
||||
|
||||
export async function sitemaps() {
|
||||
console.info('Preparing Sitemap...');
|
||||
const { manifest } = await import('../build/server/manifest.js');
|
||||
const sveltekit_routes = manifest._.routes
|
||||
.filter((route) => route.params.length === 0)
|
||||
.map((route) => route.id);
|
||||
const threads = collectThreads();
|
||||
const all_routes = [...sveltekit_routes, ...threads];
|
||||
const document_routes = all_routes.filter(
|
||||
(route) => !['.json', '.xml'].some((ext) => route.endsWith(ext))
|
||||
const threads = collectThreads().map((id) => `/threads/${id}`);
|
||||
const otherRoutes = manifest._.routes
|
||||
.filter((r) => r.params.length === 0)
|
||||
.map((r) => r.id)
|
||||
.filter(
|
||||
(id) => !id.startsWith('/threads/') && !id.endsWith('.json') && !id.endsWith('.xml')
|
||||
);
|
||||
const routes = new Set(document_routes);
|
||||
console.info(`Sitemap loaded with ${routes.length} routes!`);
|
||||
console.group();
|
||||
console.info(`sveltekit: ${sveltekit_routes.length}`);
|
||||
console.info(`threads: ${threads.length}`);
|
||||
console.groupEnd();
|
||||
|
||||
const sitemap = `
|
||||
mkdirSync(SITEMAP_DIR, { recursive: true });
|
||||
mkdirSync(THREADS_DIR, { recursive: true });
|
||||
|
||||
let totalCount = 0;
|
||||
const sitemapIndexOrder = [];
|
||||
|
||||
const grouped = {},
|
||||
fallback = [];
|
||||
|
||||
for (const route of otherRoutes) {
|
||||
const match = Object.entries(NAMED_GROUPS).find(([, prefix]) => route.startsWith(prefix));
|
||||
if (match) {
|
||||
const [group] = match;
|
||||
grouped[group] ??= [];
|
||||
grouped[group].push(route);
|
||||
} else fallback.push(route);
|
||||
}
|
||||
|
||||
totalCount += writeSitemap('pages.xml', fallback, SITEMAP_DIR);
|
||||
sitemapIndexOrder.push('pages.xml');
|
||||
|
||||
for (const group of ['docs', 'blog', 'integrations']) {
|
||||
if (grouped[group]?.length) {
|
||||
const filename = `${group}.xml`;
|
||||
totalCount += writeSitemap(filename, grouped[group], SITEMAP_DIR);
|
||||
sitemapIndexOrder.push(filename);
|
||||
}
|
||||
}
|
||||
|
||||
const threadChunks = chunkArray(threads, MAX_THREADS_PER_FILE);
|
||||
threadChunks.forEach((chunk, i) => {
|
||||
const filename = `${i + 1}.xml`;
|
||||
totalCount += writeSitemap(filename, chunk, THREADS_DIR);
|
||||
sitemapIndexOrder.push(`threads/${filename}`);
|
||||
});
|
||||
|
||||
const sitemapIndex = `
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<sitemapindex xmlns="https://www.sitemaps.org/schemas/sitemap/0.9">
|
||||
${sitemapIndexOrder
|
||||
.map(
|
||||
(name) => `
|
||||
<sitemap>
|
||||
<loc>${BASE_URL}/sitemaps/${name}</loc>
|
||||
</sitemap>`
|
||||
)
|
||||
.join('\n')}
|
||||
</sitemapindex>`.trim();
|
||||
|
||||
console.info(`✅ Sitemap generation complete — ${totalCount} URLs in total.\n`);
|
||||
|
||||
return defineEventHandler(async (event) => {
|
||||
const url = getRequestURL(event);
|
||||
|
||||
if (url.pathname === '/sitemap.xml') {
|
||||
setResponseHeader(event, 'Content-Type', 'application/xml');
|
||||
return sitemapIndex;
|
||||
}
|
||||
|
||||
if (url.pathname === '/sitemaps') {
|
||||
return sendRedirect(event, '/sitemap.xml', 307);
|
||||
}
|
||||
|
||||
if (url.pathname === '/sitemaps/threads') {
|
||||
return sendRedirect(event, '/sitemaps/threads/1.xml', 307);
|
||||
}
|
||||
|
||||
const dir = import.meta.resolve('./sitemaps');
|
||||
return serveStatic(event, {
|
||||
fallthrough: true,
|
||||
indexNames: undefined,
|
||||
getContents: (id) => readFile(new URL(dir + id)),
|
||||
getMeta: async (id) => {
|
||||
const stats = await stat(new URL(dir + id)).catch(() => null);
|
||||
if (!stats?.isFile()) return;
|
||||
return {
|
||||
size: stats.size,
|
||||
mtime: stats.mtimeMs
|
||||
};
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function writeSitemap(filename, routes, dir) {
|
||||
const body = `
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<urlset
|
||||
xmlns="https://www.sitemaps.org/schemas/sitemap/0.9"
|
||||
@@ -32,28 +129,26 @@ export async function sitemap() {
|
||||
xmlns:image="https://www.google.com/schemas/sitemap-image/1.1"
|
||||
xmlns:video="https://www.google.com/schemas/sitemap-video/1.1"
|
||||
>
|
||||
${[...routes]
|
||||
.map(
|
||||
(route) => `<url>
|
||||
<loc>https://appwrite.io${route}</loc>
|
||||
</url>
|
||||
`
|
||||
)
|
||||
.join('')}
|
||||
${routes.map((route) => ` <url>\n <loc>${BASE_URL}${route}</loc>\n </url>`).join('\n')}
|
||||
</urlset>`.trim();
|
||||
|
||||
return defineEventHandler((event) => {
|
||||
setResponseHeader(event, 'Content-Type', 'application/xml');
|
||||
const filepath = join(dir, filename);
|
||||
writeFileSync(filepath, body);
|
||||
|
||||
return sitemap;
|
||||
});
|
||||
const label = filepath.replace(BASE_DIR + '/sitemaps', '');
|
||||
console.info(` └── Generated ${label} with ${routes.length} URLs`);
|
||||
|
||||
return routes.length;
|
||||
}
|
||||
|
||||
function chunkArray(arr, size) {
|
||||
const chunks = [];
|
||||
for (let i = 0; i < arr.length; i += size) {
|
||||
chunks.push(arr.slice(i, i + size));
|
||||
}
|
||||
return chunks;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {string[]}
|
||||
*/
|
||||
function collectThreads() {
|
||||
const threads = createRequire(import.meta.url)('../build/prerendered/threads/data.json');
|
||||
|
||||
return threads.map((id) => `/threads/${id}`);
|
||||
return createRequire(import.meta.url)('../build/prerendered/threads/data.json');
|
||||
}
|
||||
|
||||
322
src/app.css
@@ -1,4 +1,5 @@
|
||||
@import 'tailwindcss';
|
||||
@import './styles/typography.css';
|
||||
@variant dark (&:is(.dark *));
|
||||
|
||||
@theme {
|
||||
@@ -6,8 +7,13 @@
|
||||
--color-*: initial;
|
||||
|
||||
/* base */
|
||||
--color-primary: hsl(var(--color-primary));
|
||||
--color-secondary: hsl(var(--color-secondary));
|
||||
--color-black: #000;
|
||||
--color-white: #fff;
|
||||
--color-transparent: transparent;
|
||||
|
||||
/* theme */
|
||||
--color-primary: var(--color-primary);
|
||||
--color-secondary: var(--color-secondary);
|
||||
--color-accent: var(--color-secondary);
|
||||
--color-smooth: var(--color-smooth);
|
||||
|
||||
@@ -18,9 +24,9 @@
|
||||
--color-pink-700: hsl(var(--color-pink-hue) 65% 36%);
|
||||
|
||||
/* red */
|
||||
--color-red-200: calc(hsl(var(--color-red-hue) - 2) 100% 92%);
|
||||
--color-red-200: hsl(calc(var(--color-red-hue) - 2) 100% 92%);
|
||||
--color-red-500: hsl(var(--color-red-hue) 100% 61%);
|
||||
--color-red-700: calc(hsl(var(--color-red-hue) - 3) 82% 39%);
|
||||
--color-red-700: hsl(calc(var(--color-red-hue) - 3) 82% 39%);
|
||||
|
||||
/* orange */
|
||||
--color-orange-200: hsl(var(--color-orange-hue) 100% 88%);
|
||||
@@ -29,23 +35,23 @@
|
||||
|
||||
/* mint */
|
||||
--color-mint-200: hsl(var(--color-mint-hue) 56% 88%);
|
||||
--color-mint-500: calc(hsl(var(--color-mint-hue) + 1) 54% 69%);
|
||||
--color-mint-700: calc(hsl(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: calc(hsl(var(--color-purple-hue) - 1) 99% 70%);
|
||||
--color-purple-700: calc(hsl(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: calc(hsl(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: calc(hsl(var(--color-blue-hue) - 1) 99% 70%);
|
||||
--color-blue-700: calc(hsl(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;
|
||||
@@ -55,9 +61,6 @@
|
||||
--color-accent-200: hsl(var(--color-secondary-hue), 78%, 60%, 0.32);
|
||||
|
||||
/* greyscale */
|
||||
--color-white: hsl(0 0% 100%);
|
||||
--color-black: hsl(0 0% 0%);
|
||||
--color-transparent: rgba(0, 0, 0, 0);
|
||||
--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%);
|
||||
@@ -74,46 +77,21 @@
|
||||
--color-greyscale-850: hsl(var(--color-greyscale-hue) 3% 14%);
|
||||
--color-greyscale-900: hsl(var(--color-greyscale-hue) 5.7% 10.4%);
|
||||
|
||||
/* utility colors */
|
||||
--color-badge-bg-light: #f2c8d6;
|
||||
--color-badge-border-light: #f69db7;
|
||||
--color-badge-bg-dark: #2c2c2f;
|
||||
--color-badge-border-dark: #39393c;
|
||||
|
||||
/* Easings */
|
||||
--transition-timing-function-bounce: linear(
|
||||
0,
|
||||
0.063,
|
||||
0.25 18.2%,
|
||||
1 36.4%,
|
||||
0.813,
|
||||
0.75,
|
||||
0.813,
|
||||
1,
|
||||
0.938,
|
||||
1,
|
||||
1
|
||||
);
|
||||
--transition-timing-function-spring: linear(
|
||||
0,
|
||||
0.938 16.7%,
|
||||
1.149 24.3%,
|
||||
1.154 29.9%,
|
||||
0.977 51%,
|
||||
1
|
||||
);
|
||||
--easing-bounce: linear(0, 0.063, 0.25 18.2%, 1 36.4%, 0.813, 0.75, 0.813, 1, 0.938, 1, 1);
|
||||
--easing-spring: linear(0, 0.938 16.7%, 1.149 24.3%, 1.154 29.9%, 0.977 51%, 1);
|
||||
|
||||
/* Animations */
|
||||
--animate-scale-in: scale-in 200ms ease-out forwards;
|
||||
--animate-caret-blink: caret-blink 1s ease-in-out infinite;
|
||||
--animate-text:
|
||||
fade 0.75s ease-in-out both, blur 0.75s ease-in-out both, up 0.75s ease-in-out both;
|
||||
--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-fade-in: fade-in 0.5s ease-in-out both;
|
||||
--animate-marquee: marquee var(--speed, 30s) linear infinite var(--direction, forwards);
|
||||
|
||||
/* Pink polyfills */
|
||||
--transition: 0.2s;
|
||||
--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;
|
||||
|
||||
/* Keyframes */
|
||||
@keyframes scale-in {
|
||||
@@ -179,63 +157,157 @@
|
||||
}
|
||||
}
|
||||
|
||||
/* Fonts */
|
||||
--font-family-sans: 'Inter', arial, sans-serif;
|
||||
--font-family-mono: 'Fira Code', monospace;
|
||||
--font-family-aeonik-fono: 'Aenoik Fono', monospace;
|
||||
--font-family-aeonik-pro: 'Aeonik Pro', var(--font-family-sans);
|
||||
--font-family-archia: 'Archia', arial, sans-serif;
|
||||
|
||||
/* Font sizes */
|
||||
--font-size-x-micro: 0.625rem;
|
||||
--font-size-x-micro--line-height: 0.875rem;
|
||||
--font-size-x-micro--letter-spacing: var(--letter-spacing-tighter);
|
||||
--font-size-micro: 0.75rem;
|
||||
--font-size-micro--line-height: 1rem;
|
||||
--font-size-micro--letter-spacing: var(--letter-spacing-tighter);
|
||||
--font-size-caption: 0.875rem;
|
||||
--font-size-caption--line-height: 1.375rem;
|
||||
--font-size-caption--letter-spacing: var(--letter-spacing-tight);
|
||||
--font-size-sub-body: clamp(0.875rem, 2vw, 1rem);
|
||||
--font-size-sub-body--line-height: 1.375rem;
|
||||
--font-size-sub-body--letter-spacing: var(--letter-spacing-tight);
|
||||
--font-size-body: clamp(1rem, 2.5vw, 1.125rem);
|
||||
--font-size-body--line-height: clamp(1.375rem, 3vw, 1.625rem);
|
||||
--font-size-body--letter-spacing: var(--letter-spacing-tight);
|
||||
--font-size-paragraph-md: 1rem;
|
||||
--font-size-paragraph-md--line-height: 1.625rem;
|
||||
--font-size-paragraph-md--letter-spacing: var(--letter-spacing-tight);
|
||||
--font-size-paragraph-lg: 1.125rem;
|
||||
--font-size-paragraph-lg--line-height: 1.75rem;
|
||||
--font-size-paragraph-lg--letter-spacing: var(--letter-spacing-tight);
|
||||
--font-size-description: clamp(1.125rem, 3vw, 1.25rem);
|
||||
--font-size-description--line-height: clamp(1.625rem, 3.5vw, 1.75rem);
|
||||
--font-size-description--letter-spacing: var(--letter-spacing-tighter);
|
||||
--font-size-label: 1.5rem;
|
||||
--font-size-label--line-height: 1.75rem;
|
||||
--font-size-title: clamp(2rem, 5vw, 2.5rem);
|
||||
--font-size-title--line-height: clamp(2.125rem, 5.5vw, 2.75rem);
|
||||
--font-size-title--letter-spacing: var(--letter-spacing-squeezed);
|
||||
--font-size-display: clamp(3rem, 7vw, 4rem);
|
||||
--font-size-display--line-height: clamp(3.125rem, 7.5vw, 4.25rem);
|
||||
--font-size-display--letter-spacing: var(--letter-spacing-compressed);
|
||||
--font-size-headline: clamp(3.5rem, 8vw, 5.5rem);
|
||||
--font-size-headline--line-height: clamp(3.5rem, 8.5vw, 5.75rem);
|
||||
--font-size-headline--letter-spacing: var(--letter-spacing-compressed);
|
||||
|
||||
/* Letter spacing */
|
||||
--letter-spacing-*: initial;
|
||||
--letter-spacing-compressed: -0.022em;
|
||||
--letter-spacing-squeezed: -0.01em;
|
||||
--letter-spacing-tighter: -0.018em;
|
||||
--letter-spacing-tight: -0.0045em;
|
||||
--letter-spacing-none: 0em;
|
||||
--letter-spacing-loose: 0.08em;
|
||||
@keyframes lighting {
|
||||
0% {
|
||||
opacity: 0;
|
||||
clip-path: inset(5%);
|
||||
transform: scale(111.11%);
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
clip-path: inset(0);
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes menu-in {
|
||||
0% {
|
||||
opacity: 0;
|
||||
filter: blur(2px);
|
||||
transform: translateY(8px);
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
filter: blur(0px);
|
||||
transform: translateY(0px);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes menu-out {
|
||||
0% {
|
||||
opacity: 1;
|
||||
filter: blur(0px);
|
||||
transform: translateY(0px);
|
||||
}
|
||||
100% {
|
||||
opacity: 0;
|
||||
filter: blur(2px);
|
||||
transform: translateY(8px);
|
||||
}
|
||||
}
|
||||
|
||||
/* Fonts */
|
||||
--font-sans: 'Inter', arial, sans-serif;
|
||||
--font-mono: 'Fira Code', monospace;
|
||||
--font-aeonik-fono: 'Aenoik Fono', monospace;
|
||||
--font-aeonik-pro: 'Aeonik Pro', var(--font-sans);
|
||||
--font-archia: 'Archia', arial, sans-serif;
|
||||
|
||||
/* Font sizes */
|
||||
--text-x-micro: 0.625rem;
|
||||
--text-x-micro--line-height: 0.875rem;
|
||||
--text-x-micro--letter-spacing: var(--tracking-tighter);
|
||||
--text-micro: 0.75rem;
|
||||
--text-micro--line-height: 1rem;
|
||||
--text-micro--letter-spacing: var(--tracking-tighter);
|
||||
--text-caption: 0.875rem;
|
||||
--text-caption--line-height: 1.375rem;
|
||||
--text-caption--letter-spacing: var(--tracking-tight);
|
||||
--text-sub-body: clamp(0.875rem, 2vw, 1rem);
|
||||
--text-sub-body--line-height: 1.375rem;
|
||||
--text-sub-body--letter-spacing: var(--tracking-tight);
|
||||
--text-body: clamp(1rem, 2.5vw, 1.125rem);
|
||||
--text-body--line-height: clamp(1.375rem, 3vw, 1.625rem);
|
||||
--text-body--letter-spacing: var(--tracking-tight);
|
||||
--text-paragraph-md: 1rem;
|
||||
--text-paragraph-md--line-height: 1.625rem;
|
||||
--text-paragraph-md--letter-spacing: var(--tracking-tight);
|
||||
--text-paragraph-lg: 1.125rem;
|
||||
--text-paragraph-lg--line-height: 1.75rem;
|
||||
--text-paragraph-lg--letter-spacing: var(--tracking-tight);
|
||||
--text-description: clamp(1.125rem, 3vw, 1.25rem);
|
||||
--text-description--line-height: clamp(1.625rem, 3.5vw, 1.75rem);
|
||||
--text-description--letter-spacing: var(--tracking-tighter);
|
||||
--text-label: 1.5rem;
|
||||
--text-label--line-height: 1.75rem;
|
||||
--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-display: clamp(3rem, 7vw, 4rem);
|
||||
--text-display--line-height: clamp(3.125rem, 7.5vw, 4.25rem);
|
||||
--text-display--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);
|
||||
|
||||
/* Letter spacing */
|
||||
--tracking-*: initial;
|
||||
--tracking-compressed: -0.022em;
|
||||
--tracking-squeezed: -0.01em;
|
||||
--tracking-tighter: -0.018em;
|
||||
--tracking-tight: -0.0045em;
|
||||
--tracking-none: 0em;
|
||||
--tracking-loose: 0.08em;
|
||||
}
|
||||
|
||||
@utility container {
|
||||
margin-inline: auto;
|
||||
padding-inline: 1.25rem;
|
||||
max-width: 75rem;
|
||||
}
|
||||
|
||||
@utility border-gradient {
|
||||
--border-gradient-before: linear-gradient(
|
||||
180deg,
|
||||
rgba(255, 255, 255, 0.16) 0%,
|
||||
rgba(255, 255, 255, 0) 100%
|
||||
);
|
||||
--border-gradient-after: linear-gradient(
|
||||
180deg,
|
||||
rgba(255, 255, 255, 0.12) 0%,
|
||||
rgba(255, 255, 255, 0) 125.11%
|
||||
);
|
||||
--border-radius: 0.5rem;
|
||||
|
||||
&::before,
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
border-radius: var(--border-radius);
|
||||
border: 1px solid transparent;
|
||||
mask:
|
||||
linear-gradient(#fff 0 0) padding-box,
|
||||
linear-gradient(#fff 0 0);
|
||||
mask-composite: exclude;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
&::before {
|
||||
background: var(--border-gradient-before) border-box;
|
||||
}
|
||||
|
||||
&::after {
|
||||
background: var(--border-gradient-after) border-box;
|
||||
}
|
||||
}
|
||||
|
||||
@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
|
||||
);
|
||||
}
|
||||
|
||||
/* Themes */
|
||||
:root,
|
||||
.light {
|
||||
/* pink polyfills */
|
||||
--transition: 0.2s;
|
||||
|
||||
/* color hues */
|
||||
--color-pink-hue: 343;
|
||||
--color-secondary-hue: 351;
|
||||
--color-red-hue: 3;
|
||||
@@ -255,7 +327,6 @@
|
||||
--color-smooth: hsl(var(--color-greyscale-hue) 6%, 10%, 0.04);
|
||||
}
|
||||
|
||||
/* dark theme */
|
||||
.dark {
|
||||
--color-primary: var(--color-greyscale-100);
|
||||
--color-secondary: var(--color-greyscale-300);
|
||||
@@ -263,54 +334,3 @@
|
||||
--color-badge-border: var(--color-badge-border-dark);
|
||||
--color-smooth: hsl(0 0%, 100%, 0.06);
|
||||
}
|
||||
|
||||
/* Container */
|
||||
@layer components {
|
||||
.container {
|
||||
@apply mx-auto box-content max-w-[75rem] px-5;
|
||||
}
|
||||
|
||||
.mask {
|
||||
mask-image: linear-gradient(
|
||||
to var(--mask-direction, top),
|
||||
transparent,
|
||||
black var(--mask-height, 32px),
|
||||
black calc(100% - var(--mask-height, 32px)),
|
||||
black
|
||||
);
|
||||
}
|
||||
|
||||
.grid-bg {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
overflow: hidden;
|
||||
background-image:
|
||||
repeating-linear-gradient(
|
||||
0deg,
|
||||
var(--line-color, rgba(255, 255, 255, 0.02)),
|
||||
var(--line-color, rgba(255, 255, 255, 0.02)) 1px,
|
||||
transparent 1px,
|
||||
transparent var(--size, calc(100vw / 16))
|
||||
),
|
||||
repeating-linear-gradient(
|
||||
90deg,
|
||||
var(--line-color, rgba(255, 255, 255, 0.02)),
|
||||
var(--line-color, rgba(255, 255, 255, 0.02)) 1px,
|
||||
transparent 1px,
|
||||
transparent var(--size, calc(100vw / 16))
|
||||
);
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-image: radial-gradient(
|
||||
circle at bottom right,
|
||||
var(--overlay, rgba(25, 25, 28, 0.5) 19%),
|
||||
transparent 100%
|
||||
);
|
||||
z-index: -2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
1
src/global.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
declare module 'reodotdev';
|
||||
@@ -1,22 +0,0 @@
|
||||
import { dev } from '$app/environment';
|
||||
import { SENTRY_DSN } from '$lib/constants';
|
||||
import { handleErrorWithSentry } from '@sentry/sveltekit';
|
||||
import * as Sentry from '@sentry/sveltekit';
|
||||
|
||||
Sentry.init({
|
||||
enabled: !dev,
|
||||
dsn: SENTRY_DSN,
|
||||
allowUrls: [/appwrite\.io/],
|
||||
tracesSampleRate: 1.0,
|
||||
|
||||
// This sets the sample rate to be 10%. You may want this to be 100% while
|
||||
// in development and sample at a lower rate in production
|
||||
replaysSessionSampleRate: 0,
|
||||
|
||||
// If the entire session is not sampled, use the below sample rate to sample
|
||||
// sessions when an error occurs.
|
||||
replaysOnErrorSampleRate: 0
|
||||
});
|
||||
|
||||
// If you have a custom error handler, pass it to `handleErrorWithSentry`
|
||||
export const handleError = handleErrorWithSentry();
|
||||
@@ -1,17 +1,9 @@
|
||||
import * as Sentry from '@sentry/sveltekit';
|
||||
import type { Handle } from '@sveltejs/kit';
|
||||
import redirects from './redirects.json';
|
||||
import { sequence } from '@sveltejs/kit/hooks';
|
||||
import { BANNER_KEY, SENTRY_DSN } from '$lib/constants';
|
||||
import { BANNER_KEY } from '$lib/constants';
|
||||
import { dev } from '$app/environment';
|
||||
|
||||
Sentry.init({
|
||||
enabled: !dev,
|
||||
dsn: SENTRY_DSN,
|
||||
tracesSampleRate: 1,
|
||||
allowUrls: [/appwrite\.io/]
|
||||
});
|
||||
|
||||
const redirectMap = new Map(redirects.map(({ link, redirect }) => [link, redirect]));
|
||||
|
||||
const redirecter: Handle = async ({ event, resolve }) => {
|
||||
@@ -131,5 +123,4 @@ const bannerRewriter: Handle = async ({ event, resolve }) => {
|
||||
return response;
|
||||
};
|
||||
|
||||
export const handle = sequence(Sentry.sentryHandle(), redirecter, bannerRewriter, securityheaders);
|
||||
export const handleError = Sentry.handleErrorWithSentry();
|
||||
export const handle = sequence(redirecter, bannerRewriter, securityheaders);
|
||||
|
||||
1
src/icons/optimized/edge.svg
Normal file
@@ -0,0 +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>
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
1
src/icons/optimized/pop-locations.svg
Normal 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="M9.167 4.438 C 7.692 4.591,6.489 5.403,5.845 6.679 C 5.665 7.034,5.490 7.548,5.417 7.933 C 5.388 8.089,5.357 8.248,5.349 8.287 C 5.336 8.348,5.303 8.360,5.098 8.379 C 4.540 8.431,3.888 8.622,3.350 8.890 C 2.026 9.551,1.337 10.768,1.411 12.317 C 1.497 14.133,2.739 15.301,4.866 15.565 C 5.255 15.613,14.745 15.613,15.134 15.565 C 16.701 15.370,17.807 14.676,18.307 13.574 C 18.646 12.826,18.689 11.790,18.416 10.950 C 17.980 9.610,16.649 8.644,14.885 8.388 L 14.621 8.349 14.529 8.031 C 14.275 7.147,13.888 6.448,13.316 5.840 C 12.290 4.750,10.850 4.264,9.167 4.438 M10.305 5.635 C 11.168 5.723,11.851 6.047,12.419 6.638 C 13.064 7.309,13.425 8.176,13.517 9.275 L 13.539 9.533 13.995 9.533 C 15.366 9.534,16.567 10.061,17.063 10.881 C 17.464 11.544,17.505 12.528,17.158 13.178 C 16.817 13.817,16.071 14.239,15.057 14.366 C 14.685 14.412,5.315 14.412,4.943 14.366 C 3.718 14.212,2.914 13.644,2.677 12.763 C 2.593 12.452,2.602 11.738,2.694 11.395 C 2.917 10.563,3.522 10.006,4.519 9.717 C 4.960 9.589,5.406 9.533,5.983 9.533 L 6.500 9.533 6.501 9.275 C 6.501 8.850,6.561 8.267,6.631 7.994 C 7.068 6.287,8.387 5.440,10.305 5.635 " stroke="none" fill-rule="evenodd" fill="black"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
1
src/icons/optimized/regions.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg width="21" height="20" viewBox="0 0 21 20" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M9.468 2.484 C 8.358 2.664,7.422 3.046,6.510 3.690 C 6.111 3.973,5.323 4.766,5.030 5.180 C 4.233 6.306,3.806 7.818,3.917 9.119 C 4.093 11.169,5.147 13.050,7.302 15.155 C 8.143 15.977,9.253 16.888,10.023 17.390 C 10.274 17.554,10.317 17.570,10.500 17.570 C 10.683 17.570,10.726 17.554,10.977 17.390 C 12.355 16.491,14.197 14.796,15.194 13.510 C 17.213 10.907,17.631 8.361,16.431 5.968 C 16.114 5.334,15.817 4.930,15.261 4.374 C 14.350 3.464,13.397 2.924,12.145 2.609 C 11.339 2.406,10.262 2.356,9.468 2.484 M11.242 3.657 C 13.450 3.949,15.235 5.489,15.746 7.543 C 15.985 8.501,15.920 9.509,15.557 10.483 C 14.974 12.050,13.662 13.713,11.765 15.290 C 11.209 15.752,10.560 16.240,10.500 16.240 C 10.438 16.240,9.772 15.737,9.196 15.256 C 6.995 13.419,5.618 11.502,5.214 9.713 C 5.070 9.073,5.086 8.182,5.255 7.528 C 5.915 4.961,8.494 3.294,11.242 3.657 M9.929 6.459 C 9.106 6.623,8.309 7.338,8.034 8.158 C 7.955 8.395,7.933 8.535,7.920 8.873 C 7.899 9.410,7.958 9.706,8.177 10.150 C 8.317 10.433,8.391 10.533,8.677 10.819 C 8.968 11.110,9.059 11.177,9.346 11.316 C 10.097 11.678,10.903 11.678,11.654 11.316 C 11.941 11.177,12.032 11.110,12.323 10.819 C 12.609 10.532,12.683 10.433,12.823 10.150 C 13.018 9.755,13.089 9.445,13.089 8.995 C 13.089 8.131,12.734 7.431,12.023 6.892 C 11.475 6.477,10.672 6.311,9.929 6.459 M11.085 7.733 C 11.957 8.142,12.162 9.297,11.482 9.976 C 11.296 10.163,11.095 10.277,10.833 10.347 C 10.371 10.471,9.871 10.329,9.518 9.976 C 8.977 9.435,8.975 8.556,9.514 8.018 C 9.701 7.830,9.970 7.678,10.197 7.629 C 10.456 7.574,10.841 7.619,11.085 7.733 " stroke="none" fill-rule="evenodd" fill="black"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.7 KiB |
@@ -1,18 +1,85 @@
|
||||
// @ts-expect-error missing types
|
||||
import SVGFixer from 'oslllo-svg-fixer';
|
||||
import svgtofont from 'svgtofont';
|
||||
import { resolve } from 'path';
|
||||
import { basename, extname, resolve } from 'path';
|
||||
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from 'fs';
|
||||
|
||||
const src = resolve(process.cwd(), 'src/icons/svg');
|
||||
const optimized = resolve(process.cwd(), 'src/icons/optimized');
|
||||
const dist = resolve(process.cwd(), 'src/icons/output');
|
||||
const outputPath = resolve(process.cwd(), 'src/lib/components/ui/icon');
|
||||
|
||||
const generateIconSprite = () => {
|
||||
const files = readdirSync(optimized);
|
||||
const outputDir = resolve(`${outputPath}/sprite`);
|
||||
const spriteOutputPath = resolve(outputDir, 'sprite.svelte');
|
||||
|
||||
if (!existsSync(outputDir)) {
|
||||
mkdirSync(outputDir, { recursive: true });
|
||||
}
|
||||
|
||||
let spriteContent = `<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style="display: none;">\n`;
|
||||
|
||||
files.forEach((file) => {
|
||||
if (!file.endsWith('.svg')) return;
|
||||
|
||||
const filePath = resolve(optimized, file);
|
||||
const fileName = basename(file, '.svg');
|
||||
const svgContent = readFileSync(filePath, 'utf8');
|
||||
|
||||
// Extract the SVG content (everything between <svg> and </svg>)
|
||||
const svgMatch = svgContent.match(/<svg[^>]*>([\s\S]*?)<\/svg>/i);
|
||||
|
||||
if (svgMatch && svgMatch[1]) {
|
||||
const innerContent = svgMatch[1]
|
||||
.trim()
|
||||
.replace(/fill=['"]([^'"]*)['"]/g, 'fill="currentColor"');
|
||||
const viewBoxMatch = svgContent.match(/viewBox=['"]([^'"]*)['"]/i);
|
||||
const viewBox = viewBoxMatch ? viewBoxMatch[1] : '0 0 24 24';
|
||||
|
||||
// Add symbol with the extracted content
|
||||
spriteContent += ` <symbol id="${fileName}" stroke="currentColor" viewBox="${viewBox}">\n ${innerContent}\n </symbol>\n`;
|
||||
}
|
||||
});
|
||||
|
||||
// Close the sprite
|
||||
spriteContent += '</svg>';
|
||||
|
||||
// Write the sprite file
|
||||
writeFileSync(spriteOutputPath, spriteContent);
|
||||
console.log(`Created SVG sprite at ${spriteOutputPath}`);
|
||||
|
||||
return spriteOutputPath;
|
||||
};
|
||||
|
||||
const generateIconType = () => {
|
||||
try {
|
||||
const files = readdirSync(optimized);
|
||||
|
||||
const fileNames = files
|
||||
.filter((file) => extname(file) !== '')
|
||||
.map((file) => basename(file, extname(file)));
|
||||
|
||||
const typeDefinition = `export type IconType = ${fileNames.map((name) => `"${name}"`).join(' | ')};`;
|
||||
|
||||
writeFileSync(`${outputPath}/types.ts`, typeDefinition);
|
||||
|
||||
console.log(`Type generated successfully at ${outputPath}`);
|
||||
console.log(`Generated type: ${typeDefinition}`);
|
||||
} catch (error) {
|
||||
console.error('Error generating filename type:', error);
|
||||
}
|
||||
};
|
||||
|
||||
export const optimizeSVG = async () => {
|
||||
const fixer = new SVGFixer(src, optimized, {
|
||||
showProgressBar: true
|
||||
});
|
||||
|
||||
await fixer.fix();
|
||||
await fixer
|
||||
.fix()
|
||||
.then(() => generateIconSprite())
|
||||
.then(() => generateIconType());
|
||||
};
|
||||
|
||||
export const generateIcons = async () => {
|
||||
|
||||
14
src/icons/svg/edge.svg
Normal file
@@ -0,0 +1,14 @@
|
||||
<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"
|
||||
/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 509 B |
13
src/icons/svg/pop-locations.svg
Normal file
@@ -0,0 +1,13 @@
|
||||
<svg
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M9.82222 5C6.69333 5 5.91111 7.49542 5.91111 8.92137C5.55556 8.92137 2 8.92137 2 12.1298C2 14.6965 4.60741 15.1005 5.91111 14.9817H14.0889C15.3926 15.1005 18 14.6965 18 12.1298C18 9.56305 15.3926 8.92137 14.0889 8.92137C13.9704 7.61425 12.9511 5 9.82222 5Z"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.2"
|
||||
/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 466 B |
8
src/icons/svg/regions.svg
Normal file
@@ -0,0 +1,8 @@
|
||||
<svg width="21" height="20" viewBox="0 0 21 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M16.5 8.72727C16.5 13.1818 10.5 17 10.5 17C10.5 17 4.5 13.1818 4.5 8.72727C4.5 7.20831 5.13214 5.75155 6.25736 4.67748C7.38258 3.60341 8.9087 3 10.5 3C12.0913 3 13.6174 3.60341 14.7426 4.67748C15.8679 5.75155 16.5 7.20831 16.5 8.72727Z"
|
||||
stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round" />
|
||||
<path
|
||||
d="M10.5 11C11.6046 11 12.5 10.1046 12.5 9C12.5 7.89543 11.6046 7 10.5 7C9.39543 7 8.5 7.89543 8.5 9C8.5 10.1046 9.39543 11 10.5 11Z"
|
||||
stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 710 B |
@@ -55,10 +55,12 @@ const analytics = Analytics({
|
||||
plugins: [plausible('appwrite.io')]
|
||||
});
|
||||
|
||||
export const trackEvent = async (platforms: {
|
||||
export type TrackEventArgs = {
|
||||
plausible?: { name: string; data?: object };
|
||||
posthog?: { name: string };
|
||||
}) => {
|
||||
};
|
||||
|
||||
export const trackEvent = async (platforms: TrackEventArgs) => {
|
||||
if (!isTrackingAllowed()) {
|
||||
return;
|
||||
}
|
||||
|
||||
21
src/lib/actions/animate-in-view.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { inView, type InViewOptions } from 'motion';
|
||||
import { writable } from 'svelte/store';
|
||||
|
||||
export const useAnimateInView = ({ options }: { options?: InViewOptions }) => {
|
||||
let animate = writable<boolean>(false);
|
||||
|
||||
const action = (node: HTMLElement) => {
|
||||
inView(
|
||||
node,
|
||||
() => {
|
||||
animate.set(true);
|
||||
},
|
||||
{ ...options }
|
||||
);
|
||||
};
|
||||
|
||||
return {
|
||||
animate,
|
||||
action
|
||||
};
|
||||
};
|
||||
34
src/lib/actions/mouse-position.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
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 };
|
||||
};
|
||||
@@ -10,7 +10,7 @@
|
||||
class="true-body"
|
||||
style:width={`${$bodyRect?.width ?? 0}px`}
|
||||
style:height={`${$bodyRect?.height ?? 0}px`}
|
||||
/>
|
||||
></div>
|
||||
<div class="body" use:rect={bodyRect}>
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
@@ -5,16 +5,16 @@
|
||||
|
||||
<div class="code-console">
|
||||
<div class="header">
|
||||
<div class="ellipse" />
|
||||
<div class="ellipse-2" />
|
||||
<div class="ellipse-3" />
|
||||
<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 id="code-bottom"></div>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { toScale, type Scale } from '$lib/utils/toScale';
|
||||
import { spring, type AnimationListOptions, type SpringOptions } from 'motion';
|
||||
import { animation, createScrollHandler, scroll, type Animation } from '.';
|
||||
import { GITHUB_REPO_LINK, GITHUB_STARS } from '$lib/constants';
|
||||
import { SOCIAL_STATS } from '$lib/constants';
|
||||
|
||||
const springOptions: SpringOptions = {
|
||||
stiffness: 58.78,
|
||||
@@ -180,7 +180,7 @@
|
||||
|
||||
<div class="cards-wrapper">
|
||||
<a
|
||||
href="/discord"
|
||||
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"
|
||||
@@ -191,30 +191,32 @@
|
||||
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>
|
||||
<div class="text-title font-aeonik-pro mt-auto">17k+ 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={GITHUB_REPO_LINK}
|
||||
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">
|
||||
{GITHUB_STARS}+ GitHub Stars
|
||||
{SOCIAL_STATS.GITHUB.STAT} GitHub Stars
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<a
|
||||
href="https://twitter.com/appwrite"
|
||||
href={SOCIAL_STATS.TWITTER.LINK}
|
||||
class="web-card is-white web-u-min-block-size-320 oss-card flex flex-col"
|
||||
id="oss-twitter"
|
||||
>
|
||||
@@ -223,13 +225,15 @@
|
||||
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>
|
||||
<div class="text-title font-aeonik-pro mt-auto">128k+ Twitter Followers</div>
|
||||
</a>
|
||||
|
||||
<a
|
||||
href="https://www.youtube.com/@Appwrite"
|
||||
href={SOCIAL_STATS.YOUTUBE.LINK}
|
||||
class="web-card is-white web-u-min-block-size-320 oss-card flex flex-col"
|
||||
id="oss-youtube"
|
||||
>
|
||||
@@ -238,24 +242,28 @@
|
||||
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>
|
||||
<div class="text-title font-aeonik-pro mt-auto">7k+ 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={GITHUB_REPO_LINK}
|
||||
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>
|
||||
<div class="text-title font-aeonik-pro mt-auto">21k+ Code Commits</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
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);
|
||||
|
||||
@@ -33,7 +34,7 @@
|
||||
subtitle: string;
|
||||
description: string;
|
||||
features: string[];
|
||||
shot?: string;
|
||||
shot?: EnhancedImgAttributes['src'];
|
||||
};
|
||||
export const infos: { [K in Product]?: ProductInfo } = {
|
||||
auth: {
|
||||
@@ -237,7 +238,7 @@
|
||||
>
|
||||
{#if scrollInfo.percentage > -0.1}
|
||||
<span
|
||||
class="web-badges text-micro uppercase !text-white"
|
||||
class="web-badges text-micro !text-white uppercase"
|
||||
transition:slide={{ axis: 'x' }}>Products_</span
|
||||
>
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
<div class="outside">
|
||||
<div class="wrapper">
|
||||
<span class="web-badges text-micro uppercase !text-white">Products_</span>
|
||||
<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
|
||||
@@ -60,7 +60,7 @@
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="img-overlay" />
|
||||
<div class="img-overlay"></div>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
<div class="wrapper">
|
||||
<button use:melt={$root} class="anim-checkbox">
|
||||
{#if $isChecked}
|
||||
<span class="web-icon-check" />
|
||||
<span class="web-icon-check"></span>
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -14,12 +14,12 @@
|
||||
{#each objectKeys($state.controls) as provider, i}
|
||||
{@const isLast = i === objectKeys($state.controls).length - 1}
|
||||
<div>
|
||||
<span class={getIcon(provider)} />
|
||||
<span class={getIcon(provider)}></span>
|
||||
<span>{provider}</span>
|
||||
<Switch bind:checked={$state.controls[provider]} />
|
||||
</div>
|
||||
{#if !isLast}
|
||||
<div class="sep" />
|
||||
<div class="sep"></div>
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
@@ -47,7 +47,7 @@
|
||||
animate:flip={{ duration: 250 }}
|
||||
>
|
||||
<div class="inner">
|
||||
<span class="web-icon-{provider.toLowerCase()}" />
|
||||
<span class="web-icon-{provider.toLowerCase()}"></span>
|
||||
<span>{provider}</span>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
{#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 class="web-icon-copy"></span>
|
||||
<span>{task.id}</span>
|
||||
</div>
|
||||
<span class="truncated">{task.title}</span>
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
<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 class="icon-menu" aria-label="menu"></span>
|
||||
</div>
|
||||
|
||||
<div class="date">Today</div>
|
||||
@@ -22,9 +22,9 @@
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<button class="add-btn">
|
||||
<span class="web-icon-plus" />
|
||||
</button>
|
||||
<div class="add-btn">
|
||||
<span class="web-icon-plus"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
|
||||
@@ -25,14 +25,14 @@ return res.json({ success: true });`.trim();
|
||||
|
||||
<div use:portal={{ target: '#code-bottom' }} class="bottom">
|
||||
{#if $state.submit !== 'idle'}
|
||||
<span class="web-icon-github" in:fade />
|
||||
<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 class="loader is-small" in:fade></div>
|
||||
{:else if $state.submit === 'success'}
|
||||
<span>Deployed to Appwrite Cloud</span>
|
||||
<span class="web-icon-check" />
|
||||
<span class="web-icon-check"></span>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
<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 class="icon-menu" aria-label="menu"></span>
|
||||
</div>
|
||||
|
||||
<div class="plan">
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
{#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 class="web-icon-copy"></span>
|
||||
<span>{task.id}</span>
|
||||
</div>
|
||||
<div class="icon-button">
|
||||
@@ -27,9 +27,9 @@
|
||||
|
||||
<div class="status-indicator">
|
||||
{#if task.status === 'sending'}
|
||||
<div class="loader is-small" in:fade />
|
||||
<div class="loader is-small" in:fade></div>
|
||||
{:else}
|
||||
<span class="web-icon-check" />
|
||||
<span class="web-icon-check"></span>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
{#if $state.submit === 'success'}
|
||||
<div class="push-notification" in:fly={{ y: -20 }}>
|
||||
<div class="icon" />
|
||||
<div class="icon"></div>
|
||||
<div class="content">
|
||||
<div class="header">
|
||||
<h3 class="title">New task assigned to you</h3>
|
||||
@@ -22,7 +22,7 @@
|
||||
<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 class="icon-menu" aria-label="menu"></span>
|
||||
</div>
|
||||
|
||||
<div class="date">Today</div>
|
||||
@@ -34,9 +34,9 @@
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
<button class="add-btn">
|
||||
<span class="web-icon-plus" />
|
||||
</button>
|
||||
<div class="add-btn">
|
||||
<span class="web-icon-plus"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
|
||||
<div class="gradient-box auth" id="post-auth-{$elId}">
|
||||
<div class="flex items-center gap-2">
|
||||
<p class="icon-user-group" />
|
||||
<p class="icon-user-group"></p>
|
||||
<p class="f-eyebrow">Authentication</p>
|
||||
</div>
|
||||
<p class="f-display mbs-16">
|
||||
@@ -31,7 +31,7 @@
|
||||
|
||||
<div class="gradient-box storage" id="post-storage-{$elId}">
|
||||
<div class="flex items-center gap-2">
|
||||
<p class="icon-folder" />
|
||||
<p class="icon-folder"></p>
|
||||
<p class="f-eyebrow">Storage</p>
|
||||
</div>
|
||||
<p class="f-display mbs-16">
|
||||
@@ -55,7 +55,7 @@
|
||||
|
||||
<div class="gradient-box functions" id="post-functions-{$elId}">
|
||||
<div class="flex items-center gap-2">
|
||||
<p class="icon-lightning-bolt" />
|
||||
<p class="icon-lightning-bolt"></p>
|
||||
<p class="f-eyebrow">Functions</p>
|
||||
</div>
|
||||
<p class="f-display mbs-16">
|
||||
@@ -68,7 +68,7 @@
|
||||
|
||||
<div class="gradient-box databases" id="post-databases-{$elId}">
|
||||
<div class="flex items-center gap-2">
|
||||
<p class="icon-database" />
|
||||
<p class="icon-database"></p>
|
||||
<p class="f-eyebrow">Databases</p>
|
||||
</div>
|
||||
<p class="f-display mbs-16">
|
||||
|
||||
@@ -55,13 +55,13 @@
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
<div class="vertical-sep" />
|
||||
<span class="icon-menu" />
|
||||
<div class="vertical-sep"></div>
|
||||
<span class="icon-menu"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="search">
|
||||
<span class="web-icon-search" />
|
||||
<span class="web-icon-search"></span>
|
||||
<span class="text"> Search </span>
|
||||
</div>
|
||||
<div class="flow gap-8">
|
||||
@@ -81,11 +81,11 @@
|
||||
<div class="title">
|
||||
<span class="text capitalize">{col}</span>
|
||||
<span class="tgl-inline-tag">{tasks.length}</span>
|
||||
<span class="icon-dots-horizontal" />
|
||||
<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 class="icon-plus"></span>
|
||||
<span class="text">New Task</span>
|
||||
</button>
|
||||
{#each tasks as task (task.title)}
|
||||
@@ -112,7 +112,7 @@
|
||||
</div>
|
||||
</div>
|
||||
{#if !isLast}
|
||||
<div class="vertical-sep" />
|
||||
<div class="vertical-sep"></div>
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
<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 class="icon-menu" aria-label="menu"></span>
|
||||
</div>
|
||||
|
||||
<div class="date">Today</div>
|
||||
@@ -27,9 +27,9 @@
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<button class="add-btn">
|
||||
<span class="web-icon-plus" />
|
||||
</button>
|
||||
<div class="add-btn">
|
||||
<span class="web-icon-plus"></span>
|
||||
</div>
|
||||
|
||||
<div class="overlay" id="overlay-{$elId}">
|
||||
<div class="drawer" id="drawer-{$elId}">
|
||||
@@ -50,7 +50,7 @@
|
||||
<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 class="loader"></div>
|
||||
</div>
|
||||
</div>
|
||||
<img id="upload-img-{$elId}" src="/images/animations/storage-2.png" alt="" />
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
style:--y={`${y}px`}
|
||||
style:--percentage={`${easedPercentage * 100}%`}
|
||||
>
|
||||
<div class="absolute -top-[8px] left-1/2" />
|
||||
<div class="absolute -top-[8px] left-1/2"></div>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
>
|
||||
<span class="text-primary text-sub-body font-medium">{title}</span>
|
||||
<div class="icon text-primary transition-transform group-[&[open]]:rotate-180">
|
||||
<span class="icon-cheveron-down" aria-hidden="true" />
|
||||
<span class="icon-cheveron-down" aria-hidden="true"></span>
|
||||
</div>
|
||||
</summary>
|
||||
<div class="collapsible-content text-secondary text-sub-body flex flex-col">
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
aria-label="close discord message"
|
||||
on:click={hideTopBanner}
|
||||
>
|
||||
<span class="web-icon-close" aria-hidden="true" />
|
||||
<span class="web-icon-close" aria-hidden="true"></span>
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
<script lang="ts">
|
||||
import { fade, scale } from 'svelte/transition';
|
||||
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';
|
||||
|
||||
const {
|
||||
elements: { portalled, trigger, content, overlay },
|
||||
@@ -11,24 +12,23 @@
|
||||
});
|
||||
</script>
|
||||
|
||||
<button
|
||||
use:melt={$trigger}
|
||||
on:click={() => {
|
||||
<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' }
|
||||
});
|
||||
}}
|
||||
class="web-button cursor-pointer transition-opacity hover:opacity-90 active:scale-95"
|
||||
style:box-shadow="0 2px 40px rgba(0, 0, 0, 0.5)"
|
||||
>
|
||||
<span class="web-icon-play" />
|
||||
<span>Appwrite in 100 seconds</span>
|
||||
</button>
|
||||
Appwrite in 100 seconds
|
||||
|
||||
<Icon name="play" />
|
||||
</Button>
|
||||
{#if $open}
|
||||
<div use:melt={$portalled}>
|
||||
<div use:melt={$overlay} class="overlay" transition:fade={{ duration: 150 }} />
|
||||
<div use:melt={$overlay} class="overlay" transition:fade={{ duration: 150 }}></div>
|
||||
|
||||
<div
|
||||
class="web-media content"
|
||||
@@ -41,7 +41,7 @@
|
||||
frameborder="0"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
|
||||
allowfullscreen
|
||||
/>
|
||||
></iframe>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { getAppwriteDashboardUrl } from '$lib/utils/dashboard';
|
||||
import { Button } from '$lib/components/ui';
|
||||
|
||||
export let heading: string = 'Start building with Appwrite today';
|
||||
export let label: string = 'Get started';
|
||||
@@ -10,7 +11,7 @@
|
||||
>
|
||||
<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>
|
||||
<a href={getAppwriteDashboardUrl()} class="web-button">{label}</a>
|
||||
<Button href={getAppwriteDashboardUrl()}>{label}</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,8 +1,16 @@
|
||||
<script lang="ts">
|
||||
import type { Snippet } from 'svelte';
|
||||
|
||||
let carousel: HTMLElement;
|
||||
|
||||
export let size: 'default' | 'medium' | 'big' = 'default';
|
||||
export let gap = 32;
|
||||
interface Props {
|
||||
size?: 'default' | 'medium' | 'big';
|
||||
gap?: number;
|
||||
header?: Snippet;
|
||||
children: Snippet;
|
||||
}
|
||||
|
||||
const { size = 'default', gap = 32, header, children }: Props = $props();
|
||||
let scroll = 0;
|
||||
|
||||
function calculateScrollAmount(prev = false) {
|
||||
@@ -32,8 +40,8 @@
|
||||
});
|
||||
}
|
||||
|
||||
let isEnd = false;
|
||||
let isStart = true;
|
||||
let isEnd = $state(false);
|
||||
let isStart = $state(true);
|
||||
|
||||
function handleScroll() {
|
||||
isStart = carousel.scrollLeft <= 0;
|
||||
@@ -43,23 +51,25 @@
|
||||
|
||||
<div>
|
||||
<div class="mt-2 flex flex-wrap items-center">
|
||||
<slot name="header" />
|
||||
{#if header}
|
||||
{@render header()}
|
||||
{/if}
|
||||
<div class="nav ml-auto flex items-end gap-3">
|
||||
<button
|
||||
class="web-icon-button"
|
||||
class="web-icon-button cursor-pointer"
|
||||
aria-label="Move carousel backward"
|
||||
disabled={isStart}
|
||||
on:click={prev}
|
||||
onclick={prev}
|
||||
>
|
||||
<span class="web-icon-arrow-left" aria-hidden="true" />
|
||||
<span class="web-icon-arrow-left" aria-hidden="true"></span>
|
||||
</button>
|
||||
<button
|
||||
class="web-icon-button"
|
||||
class="web-icon-button cursor-pointer"
|
||||
aria-label="Move carousel forward"
|
||||
disabled={isEnd}
|
||||
on:click={next}
|
||||
onclick={next}
|
||||
>
|
||||
<span class="web-icon-arrow-right" aria-hidden="true" />
|
||||
<span class="web-icon-arrow-right" aria-hidden="true"></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -71,9 +81,9 @@
|
||||
class:is-big={size === 'big'}
|
||||
style:gap="{gap}px"
|
||||
bind:this={carousel}
|
||||
on:scroll={handleScroll}
|
||||
onscroll={handleScroll}
|
||||
>
|
||||
<slot />
|
||||
{@render children()}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { page } from '$app/stores';
|
||||
import { page } from '$app/state';
|
||||
import { fade } from 'svelte/transition';
|
||||
import { loggedIn, user } from '$lib/utils/console';
|
||||
import { PUBLIC_GROWTH_ENDPOINT } from '$env/static/public';
|
||||
import { Button } from '$lib/components/ui';
|
||||
|
||||
export let date: string | undefined = undefined;
|
||||
let showFeedback = false;
|
||||
@@ -28,7 +29,7 @@
|
||||
body: JSON.stringify({
|
||||
email,
|
||||
type: feedbackType,
|
||||
route: $page.route.id,
|
||||
route: page.route.id,
|
||||
comment,
|
||||
metaFields: {
|
||||
userId
|
||||
@@ -75,23 +76,23 @@
|
||||
<button
|
||||
class="web-radio-button"
|
||||
aria-label="helpful"
|
||||
on:click={() => {
|
||||
onclick={() => {
|
||||
showFeedback = feedbackType !== 'positive';
|
||||
feedbackType = 'positive';
|
||||
}}
|
||||
>
|
||||
<span class="icon-thumb-up" />
|
||||
<span class="icon-thumb-up"></span>
|
||||
</button>
|
||||
<button
|
||||
class="web-radio-button"
|
||||
aria-label="unhelpful"
|
||||
on:click={() => {
|
||||
onclick={() => {
|
||||
showFeedback = feedbackType !== 'negative';
|
||||
feedbackType = 'negative';
|
||||
}}
|
||||
>
|
||||
<!-- TODO: fix the icon name on pink -->
|
||||
<span class="icon-thumb-dowm" />
|
||||
<span class="icon-thumb-dowm"></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -102,12 +103,12 @@
|
||||
{/if}
|
||||
<li>
|
||||
<a
|
||||
href={`https://github.com/appwrite/website/tree/main/src/routes${$page.route.id}`}
|
||||
href={`https://github.com/appwrite/website/tree/main/src/routes${page.route.id}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="web-link flex items-baseline gap-1"
|
||||
>
|
||||
<span class="icon-pencil-alt contents" aria-hidden="true" />
|
||||
<span class="icon-pencil-alt contents" aria-hidden="true"></span>
|
||||
<span>Update on GitHub</span>
|
||||
</a>
|
||||
</li>
|
||||
@@ -117,7 +118,10 @@
|
||||
</header>
|
||||
{#if showFeedback}
|
||||
<form
|
||||
on:submit|preventDefault={handleSubmit}
|
||||
onsubmit={(e) => {
|
||||
e.preventDefault();
|
||||
handleSubmit();
|
||||
}}
|
||||
class="web-card is-normal"
|
||||
style="--card-padding:1rem"
|
||||
out:fade={{ duration: 450 }}
|
||||
@@ -125,15 +129,16 @@
|
||||
<div class="flex flex-col gap-2">
|
||||
<label for="message">
|
||||
<span class="text-primary">
|
||||
What did you {feedbackType === 'negative' ? 'dislike' : 'like'}? (optional)
|
||||
What did you {feedbackType === 'negative' ? 'dislike' : 'like'}?
|
||||
</span>
|
||||
</label>
|
||||
<textarea
|
||||
class="web-input-text"
|
||||
id="message"
|
||||
placeholder="Write your message"
|
||||
required
|
||||
bind:value={comment}
|
||||
/>
|
||||
></textarea>
|
||||
<label for="message" class="mt-2">
|
||||
<span class="text-primary">Email</span>
|
||||
</label>
|
||||
@@ -158,12 +163,8 @@
|
||||
{/if}
|
||||
|
||||
<div class="mt-4 flex justify-end gap-2">
|
||||
<button class="web-button is-text" on:click={() => (showFeedback = false)}>
|
||||
<span>Cancel</span>
|
||||
</button>
|
||||
<button type="submit" class="web-button" disabled={submitting || !email}>
|
||||
<span>Submit</span>
|
||||
</button>
|
||||
<Button variant="text" onclick={() => (showFeedback = false)}>Cancel</Button>
|
||||
<Button type="submit" disabled={submitting || !email}>Submit</Button>
|
||||
</div>
|
||||
</form>
|
||||
{/if}
|
||||
|
||||
@@ -37,15 +37,16 @@
|
||||
{ label: 'Functions', href: '/products/functions' },
|
||||
{ label: 'Messaging', href: '/products/messaging' },
|
||||
{ label: 'Storage', href: '/products/storage' },
|
||||
{ label: 'Realtime', href: '/docs/apis/realtime' }
|
||||
{ label: 'Realtime', href: '/docs/apis/realtime' },
|
||||
{ label: 'Network', href: '/docs/products/network' }
|
||||
],
|
||||
Learn: [
|
||||
{ label: 'Blog', href: '/blog' },
|
||||
{ label: 'Docs', href: '/docs' },
|
||||
{ label: 'Integrations', href: '/integrations' },
|
||||
{ label: 'Community', href: '/community' },
|
||||
{ label: 'Init', href: '/init' },
|
||||
{ label: 'Threads', href: '/threads' },
|
||||
{ label: 'Blog', href: '/blog' },
|
||||
{ label: 'Changelog', href: '/changelog' },
|
||||
{
|
||||
label: 'Roadmap',
|
||||
@@ -129,7 +130,7 @@
|
||||
class="web-icon-chevron-down web-u-transition"
|
||||
class:web-u-rotate-180={$isSelected(title)}
|
||||
style:font-size="1rem"
|
||||
/>
|
||||
></span>
|
||||
</button>
|
||||
</h5>
|
||||
{#if $isSelected(title)}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/stores';
|
||||
import { page } from '$app/state';
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
let mounted = false;
|
||||
@@ -28,7 +28,7 @@
|
||||
const randomDelay = () => Math.floor(Math.random() * 750);
|
||||
</script>
|
||||
|
||||
<div class="banner" class:hidden={$page.url.pathname.includes('init')}>
|
||||
<div class="banner" class:hidden={page.url.pathname.includes('init')}>
|
||||
<div class="content text-primary">
|
||||
<div class="headings">
|
||||
<span style:font-weight="500"
|
||||
@@ -39,16 +39,16 @@
|
||||
> has started
|
||||
</span>
|
||||
<span class="web-u-color-text-secondary">The start of something new</span>
|
||||
<div class="shadow" />
|
||||
<div class="shadow"></div>
|
||||
</div>
|
||||
<a href="/init" rel="noopener noreferrer" class="action">
|
||||
<span class="text-caption font-medium">Join now</span>
|
||||
<span class="web-icon-arrow-right" aria-hidden="true" />
|
||||
<div class="shadow" />
|
||||
<span class="web-icon-arrow-right" aria-hidden="true"></span>
|
||||
<div class="shadow"></div>
|
||||
</a>
|
||||
</div>
|
||||
<div class="shine" />
|
||||
<div class="border" />
|
||||
<div class="shine"></div>
|
||||
<div class="border"></div>
|
||||
<div class="lines">
|
||||
{#if mounted}
|
||||
{#each Array.from({ length: groups.length }) as _, i}
|
||||
@@ -57,7 +57,7 @@
|
||||
<div
|
||||
class="line"
|
||||
style={`--width:${getRandomWidth(index)}px;--initial-delay:${randomDelay()}ms;left:${getRandomXValue()}px;`}
|
||||
/>
|
||||
></div>
|
||||
{/each}
|
||||
</div>
|
||||
{/each}
|
||||
|
||||
@@ -2,10 +2,14 @@
|
||||
import { classNames } from '$lib/utils/classnames';
|
||||
import { trackEvent } from '$lib/actions/analytics';
|
||||
import { browser } from '$app/environment';
|
||||
import { page } from '$app/stores';
|
||||
import { getAppwriteDashboardUrl } from '$lib/utils/dashboard';
|
||||
import { Button } from '$lib/components/ui';
|
||||
|
||||
export let classes = '';
|
||||
interface Props {
|
||||
class?: string;
|
||||
}
|
||||
|
||||
const { class: className }: Props = $props();
|
||||
|
||||
const isLoggedIn = browser && 'loggedIn' in document.body.dataset;
|
||||
|
||||
@@ -14,15 +18,19 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<a
|
||||
class={classNames('web-button web-u-inline-width-100-percent-mobile', classes)}
|
||||
<Button
|
||||
class={classNames('web-u-inline-width-100-percent-mobile', className)}
|
||||
href={getAppwriteDashboardUrl()}
|
||||
on:click={() =>
|
||||
onclick={() =>
|
||||
trackEvent({
|
||||
plausible: { name: `${getTrackingEventName()} in header` },
|
||||
...(isLoggedIn ? {} : { posthog: { name: 'get-started-btn_nav_click' } })
|
||||
})}
|
||||
>
|
||||
<span class="hidden group-[&[data-logged-in]]/body:block">Go to Console</span>
|
||||
<span class="block group-[&[data-logged-in]]/body:hidden">Start building</span>
|
||||
</a>
|
||||
<span class="hidden group-[&[data-logged-in]]/body:block" aria-hidden={!isLoggedIn}
|
||||
>Go to Console</span
|
||||
>
|
||||
<span class="block group-[&[data-logged-in]]/body:hidden" aria-hidden={isLoggedIn}
|
||||
>Start building</span
|
||||
>
|
||||
</Button>
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<span class={social.icon} aria-hidden="true" />
|
||||
<span class={social.icon} aria-hidden="true"></span>
|
||||
</a>
|
||||
</li>
|
||||
{/each}
|
||||
@@ -36,7 +36,7 @@
|
||||
scrolling="no"
|
||||
style:color-scheme="none"
|
||||
style:margin-top="-4px"
|
||||
/>
|
||||
></iframe>
|
||||
|
||||
<ul class="flex gap-4">
|
||||
<li><a class="web-link" href="/terms">Terms</a></li>
|
||||
@@ -58,7 +58,7 @@
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<span class={social.icon} aria-hidden="true" />
|
||||
<span class={social.icon} aria-hidden="true"></span>
|
||||
</a>
|
||||
</li>
|
||||
{/each}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
<script lang="ts" context="module">
|
||||
import type { ComponentType } from 'svelte';
|
||||
import type { Component } from 'svelte';
|
||||
|
||||
export type NavLink = {
|
||||
label: string;
|
||||
href?: string;
|
||||
showBadge?: boolean;
|
||||
submenu?: ComponentType;
|
||||
mobileSubmenu?: ComponentType;
|
||||
submenu?: Component<{ label: string }>;
|
||||
mobileSubmenu?: Component<{ label: string }>;
|
||||
};
|
||||
</script>
|
||||
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
<script lang="ts">
|
||||
import { afterNavigate } from '$app/navigation';
|
||||
import { IsLoggedIn } from '$lib/components';
|
||||
import { GITHUB_REPO_LINK, GITHUB_STARS } from '$lib/constants';
|
||||
import { SOCIAL_STATS } from '$lib/constants';
|
||||
import type { NavLink } from './MainNav.svelte';
|
||||
import { getAppwriteDashboardUrl } from '$lib/utils/dashboard';
|
||||
import { Button, InlineTag, Icon } from '$lib/components/ui';
|
||||
import { GithubStats } from '$lib/components/shared';
|
||||
|
||||
export let open = false;
|
||||
export let links: NavLink[];
|
||||
@@ -18,10 +20,10 @@
|
||||
<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">
|
||||
<a href={getAppwriteDashboardUrl('/register')} class="web-button is-secondary flex-1">
|
||||
<Button href={getAppwriteDashboardUrl('/register')} variant="secondary" class="flex-1">
|
||||
Sign up
|
||||
</a>
|
||||
<IsLoggedIn classes="flex-1" />
|
||||
</Button>
|
||||
<IsLoggedIn class="flex-1" />
|
||||
</div>
|
||||
<div class="web-side-nav-scroll">
|
||||
<section>
|
||||
@@ -41,16 +43,7 @@
|
||||
</section>
|
||||
</div>
|
||||
<div class="web-side-nav-mobile-footer-buttons">
|
||||
<a
|
||||
href={GITHUB_REPO_LINK}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="web-button is-text web-u-inline-width-100-percent-mobile"
|
||||
>
|
||||
<span class="web-icon-star" aria-hidden="true" />
|
||||
<span class="text">Star on GitHub</span>
|
||||
<span class="web-inline-tag text-sub-body">{GITHUB_STARS}</span>
|
||||
</a>
|
||||
<GithubStats class="w-full! md:w-fit" />
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
@@ -1,20 +1,24 @@
|
||||
<script lang="ts">
|
||||
import { platformMap } from '$lib/utils/references';
|
||||
import { writable } from 'svelte/store';
|
||||
import { Select, Tooltip } from '$lib/components';
|
||||
import { getCodeHtml, type Language } from '$lib/utils/code';
|
||||
import { copy } from '$lib/utils/copy';
|
||||
import { Select, Tooltip } from '$lib/components';
|
||||
import { platformMap } from '$lib/utils/references';
|
||||
import { writable } from 'svelte/store';
|
||||
|
||||
export let selected: Language = 'js';
|
||||
export let data: { language: string; content: string; platform?: string }[] = [];
|
||||
export let width: number | null = null;
|
||||
export let height: number | null = null;
|
||||
interface Props {
|
||||
selected?: Language;
|
||||
data?: { language: string; content: string; platform?: string }[];
|
||||
width?: number | null;
|
||||
height?: number | null;
|
||||
}
|
||||
|
||||
$: snippets = writable(new Set(data.map((d) => d.language)));
|
||||
let { selected = $bindable('js'), data = [], width = null, height = null }: Props = $props();
|
||||
|
||||
$: content = data.find((d) => d.language === selected)?.content ?? '';
|
||||
let snippets = $derived(writable(new Set(data.map((d) => d.language))));
|
||||
|
||||
$: platform = data.find((d) => d.language === selected)?.platform ?? '';
|
||||
let content = $derived(data.find((d) => d.language === selected)?.content ?? '');
|
||||
|
||||
let platform = $derived(data.find((d) => d.language === selected)?.platform ?? '');
|
||||
|
||||
snippets?.subscribe((n) => {
|
||||
if (selected === null && n.size > 0) {
|
||||
@@ -22,11 +26,15 @@
|
||||
}
|
||||
});
|
||||
|
||||
enum CopyStatus {
|
||||
Copy = 'Copy',
|
||||
Copied = 'Copied!'
|
||||
}
|
||||
let copyText = CopyStatus.Copy;
|
||||
const CopyStatus = {
|
||||
Copy: 'Copy',
|
||||
Copied: 'Copied!'
|
||||
} as const;
|
||||
type CopyStatusType = keyof typeof CopyStatus;
|
||||
type CopyStatusValue = (typeof CopyStatus)[CopyStatusType];
|
||||
|
||||
let copyText: CopyStatusValue = CopyStatus.Copy;
|
||||
|
||||
async function handleCopy() {
|
||||
await copy(content);
|
||||
|
||||
@@ -36,15 +44,19 @@
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
$: result = getCodeHtml({
|
||||
let result = $derived(
|
||||
getCodeHtml({
|
||||
content,
|
||||
language: selected ?? 'sh',
|
||||
withLineNumbers: true
|
||||
});
|
||||
$: options = Array.from($snippets).map((language) => ({
|
||||
})
|
||||
);
|
||||
let options = $derived(
|
||||
Array.from($snippets).map((language) => ({
|
||||
value: language,
|
||||
label: platformMap[language]
|
||||
}));
|
||||
}))
|
||||
);
|
||||
</script>
|
||||
|
||||
<section
|
||||
@@ -72,14 +84,16 @@
|
||||
<li class="buttons-list-item" style="padding-inline-start: 13px">
|
||||
<Tooltip>
|
||||
<button
|
||||
on:click={handleCopy}
|
||||
onclick={handleCopy}
|
||||
class="web-icon-button"
|
||||
aria-label="copy code from code-snippet"
|
||||
><span class="web-icon-copy" aria-hidden="true" /></button
|
||||
><span class="web-icon-copy" aria-hidden="true"></span></button
|
||||
>
|
||||
<svelte:fragment slot="tooltip">
|
||||
{#snippet tooltip()}
|
||||
<span>
|
||||
{copyText}
|
||||
</svelte:fragment>
|
||||
</span>
|
||||
{/snippet}
|
||||
</Tooltip>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script context="module" lang="ts">
|
||||
import { PUBLIC_GROWTH_ENDPOINT } from '$env/static/public';
|
||||
import { Button } from '$lib/components/ui';
|
||||
|
||||
export async function newsletter(name: string, email: string) {
|
||||
const response = await fetch(`${PUBLIC_GROWTH_ENDPOINT}/newsletter/subscribe`, {
|
||||
@@ -166,9 +167,7 @@
|
||||
bind:value={email}
|
||||
/>
|
||||
</div>
|
||||
<button type="submit" class="web-button" disabled={submitting}
|
||||
>Sign up</button
|
||||
>
|
||||
<Button type="submit" disabled={submitting}>Sign up</Button>
|
||||
{#if error}
|
||||
<span class="text">
|
||||
Something went wrong. Please try again later.
|
||||
|
||||
@@ -1,6 +1,61 @@
|
||||
<script lang="ts">
|
||||
import { trackEvent } from '$lib/actions/analytics';
|
||||
import { getAppwriteDashboardUrl } from '$lib/utils/dashboard';
|
||||
import { Button, type Variant } from '$lib/components/ui';
|
||||
|
||||
const plans: Array<{
|
||||
name: string;
|
||||
price: string;
|
||||
description: string;
|
||||
variable?: boolean;
|
||||
tag?: string;
|
||||
buttonText: string;
|
||||
buttonLink: string;
|
||||
buttonVariant: Variant;
|
||||
eventName: string;
|
||||
}> = [
|
||||
{
|
||||
name: 'Free',
|
||||
price: '$0',
|
||||
description: 'A great fit for passion projects and small applications.',
|
||||
buttonText: 'Get started',
|
||||
buttonLink: getAppwriteDashboardUrl('/register'),
|
||||
buttonVariant: 'secondary',
|
||||
eventName: 'Get started Free plan'
|
||||
},
|
||||
{
|
||||
name: 'Pro',
|
||||
price: '$15',
|
||||
variable: true,
|
||||
tag: 'Most Popular',
|
||||
description:
|
||||
'For production applications that need powerful functionality and resources to scale.',
|
||||
buttonText: 'Start building',
|
||||
buttonLink: getAppwriteDashboardUrl('/console?type=create&plan=tier-1'),
|
||||
buttonVariant: 'primary',
|
||||
eventName: 'Get started Pro plan'
|
||||
},
|
||||
{
|
||||
name: 'Scale',
|
||||
price: '$599',
|
||||
variable: true,
|
||||
description:
|
||||
'For teams that handle more complex and large projects and need more control and support.',
|
||||
buttonText: 'Start building',
|
||||
buttonLink: getAppwriteDashboardUrl('/console?type=create&plan=tier-2'),
|
||||
buttonVariant: 'secondary',
|
||||
eventName: 'Get started Scale plan'
|
||||
},
|
||||
{
|
||||
name: 'Enterprise',
|
||||
price: 'Custom',
|
||||
description: 'For enterprises that need more power and premium support.',
|
||||
buttonText: 'Contact us',
|
||||
buttonLink: '/contact-us/enterprise',
|
||||
buttonVariant: 'secondary',
|
||||
eventName: 'Get started Enterprise plan'
|
||||
}
|
||||
];
|
||||
</script>
|
||||
|
||||
<img
|
||||
@@ -16,13 +71,14 @@
|
||||
<h2 class="text-display font-aeonik-pro text-primary max-w-[500px] text-center">
|
||||
Start building with Appwrite today
|
||||
</h2>
|
||||
<a
|
||||
<Button
|
||||
variant="transparent"
|
||||
href={getAppwriteDashboardUrl()}
|
||||
class="web-button is-transparent web-self-center"
|
||||
on:click={() => trackEvent({ plausible: { name: 'Get started in pre footer' } })}
|
||||
class="self-center"
|
||||
onclick={() => trackEvent({ plausible: { name: 'Get started in pre footer' } })}
|
||||
>
|
||||
<span class="text">Get started</span>
|
||||
</a>
|
||||
</Button>
|
||||
</section>
|
||||
<section
|
||||
class="web-card is-transparent has-border-gradient web-u-max-inline-width-584-mobile web-mx-auto-mobile web-u-inline-width-100-percent-mobile p-8!"
|
||||
@@ -34,127 +90,49 @@
|
||||
</header>
|
||||
|
||||
<ul class="web-strip-plans -mt-8">
|
||||
{#each plans as plan}
|
||||
<li class="web-strip-plans-item web-strip-plans-container-query">
|
||||
<div class="web-strip-plans-item-wrapper">
|
||||
<div class="web-strip-plans-plan">
|
||||
<h4 class="title text-description">Free</h4>
|
||||
<div class="text-title font-aeonik-pro text-primary">$0</div>
|
||||
<div class="info text-caption font-medium" />
|
||||
</div>
|
||||
<p class="web-strip-plans-info text-caption font-medium">
|
||||
A great fit for passion projects and small applications.
|
||||
</p>
|
||||
<a
|
||||
href={getAppwriteDashboardUrl('/register')}
|
||||
class="web-button is-secondary is-full-width-mobile web-u-cross-child-end"
|
||||
on:click={() =>
|
||||
trackEvent({
|
||||
plausible: {
|
||||
name: 'Get started Free plan'
|
||||
}
|
||||
})}
|
||||
>
|
||||
<span class="text" style:padding-inline="0.5rem">Get started</span>
|
||||
</a>
|
||||
</div>
|
||||
</li>
|
||||
<li class="web-strip-plans-item web-strip-plans-container-query">
|
||||
<div class="web-strip-plans-item-wrapper">
|
||||
<div class="web-strip-plans-plan">
|
||||
<div class="place-item-end grid grid-cols-1 gap-6 md:grid-cols-3">
|
||||
<div class="flex flex-col">
|
||||
<div class="flex gap-3">
|
||||
<h4 class="title text-description">Pro</h4>
|
||||
<div class="web-inline-tag is-pink text-sub-body">Most popular</div>
|
||||
<h4 class="title text-description">{plan.name}</h4>
|
||||
{#if plan.tag}<div class="web-inline-tag is-pink text-sub-body">
|
||||
Most popular
|
||||
</div>{/if}
|
||||
</div>
|
||||
|
||||
<div class="mt-4 flex flex-col">
|
||||
<span>From</span>
|
||||
{#if plan.variable}<span>From</span>{/if}
|
||||
<div class="flex items-end gap-2">
|
||||
<div class="text-title font-aeonik-pro text-primary">$15</div>
|
||||
<div class="text-title font-aeonik-pro text-primary">
|
||||
{plan.price}
|
||||
</div>
|
||||
{#if plan.variable}
|
||||
<div class="info text-caption font-medium">/month</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p class="web-strip-plans-info text-caption font-medium">
|
||||
For production applications that need powerful functionality and resources
|
||||
to scale.
|
||||
<p class="web-strip-plans-info text-caption self-end font-medium">
|
||||
{plan.description}
|
||||
</p>
|
||||
<a
|
||||
href={getAppwriteDashboardUrl('/console?type=create&plan=tier-1')}
|
||||
class="web-button is-full-width-mobile web-u-cross-child-end"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
on:click={() =>
|
||||
<Button
|
||||
variant={plan.buttonVariant}
|
||||
href={plan.buttonLink}
|
||||
class="w-full! flex-3 self-end md:w-fit"
|
||||
onclick={() =>
|
||||
trackEvent({
|
||||
plausible: {
|
||||
name: 'Get started Pro plan'
|
||||
name: plan.eventName
|
||||
}
|
||||
})}
|
||||
>
|
||||
<!-- <span class="text">Start trial</span> -->
|
||||
<span class="text">Start building</span>
|
||||
</a>
|
||||
</div>
|
||||
</li>
|
||||
<li class="web-strip-plans-item web-strip-plans-container-query">
|
||||
<div class="web-strip-plans-item-wrapper">
|
||||
<div class="web-strip-plans-plan">
|
||||
<h4 class="text-description text-primary">Scale</h4>
|
||||
<div class="mt-4 flex flex-col">
|
||||
<span>From</span>
|
||||
<div class="flex items-end gap-2">
|
||||
<div class="text-title font-aeonik-pro text-primary">$599</div>
|
||||
<div class="info text-caption font-medium">/month</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p class="web-strip-plans-info text-caption font-medium">
|
||||
For teams that handle more complex and large projects and need more control
|
||||
and support.
|
||||
</p>
|
||||
<a
|
||||
href={getAppwriteDashboardUrl('/console?type=create&plan=tier-2')}
|
||||
class="web-button is-secondary is-full-width-mobile web-u-cross-child-end"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
on:click={() =>
|
||||
trackEvent({
|
||||
plausible: {
|
||||
name: 'Get started Scale plan'
|
||||
}
|
||||
})}
|
||||
<span class="text" style:padding-inline="0.5rem">{plan.buttonText}</span
|
||||
>
|
||||
<span class="text">Start building</span>
|
||||
</a>
|
||||
</div>
|
||||
</li>
|
||||
<li class="web-strip-plans-item web-strip-plans-container-query">
|
||||
<div class="web-strip-plans-item-wrapper">
|
||||
<div class="web-strip-plans-plan">
|
||||
<h4 class="text-description text-primary">Enterprise</h4>
|
||||
<div class="mt-4 flex flex-col">
|
||||
<div class="flex items-end gap-2">
|
||||
<div class="text-title font-aeonik-pro text-primary">Custom</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p class="web-strip-plans-info text-caption font-medium">
|
||||
For enterprises that need more power and premium support.
|
||||
</p>
|
||||
<a
|
||||
href="/contact-us/enterprise"
|
||||
class="web-button is-secondary is-full-width-mobile web-u-cross-child-end"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
on:click={() =>
|
||||
trackEvent({
|
||||
plausible: {
|
||||
name: 'Get started Scale plan'
|
||||
}
|
||||
})}
|
||||
>
|
||||
<span class="text" style:padding-inline="0.5rem">Contact us</span>
|
||||
</a>
|
||||
</Button>
|
||||
</div>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</section>
|
||||
</div>
|
||||
@@ -164,10 +142,6 @@
|
||||
flex-basis: 5rem !important;
|
||||
}
|
||||
|
||||
.web-strip-plans .web-button {
|
||||
flex: 3;
|
||||
}
|
||||
|
||||
.web-strip-plans-item-wrapper {
|
||||
gap: 2.65rem;
|
||||
}
|
||||
|
||||
@@ -23,18 +23,18 @@
|
||||
class={classNames('web-icon-chevron-down transition-transform', {
|
||||
'rotate-180': $open
|
||||
})}
|
||||
/></button
|
||||
></span></button
|
||||
>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
{#if $open}
|
||||
<div use:melt={$content} transition:slide class="py-3 px-2">
|
||||
<div use:melt={$content} transition:slide class="px-2 py-3">
|
||||
<div class="flex flex-col gap-2">
|
||||
{#each products as product}
|
||||
<a
|
||||
href={product.href}
|
||||
class="group flex gap-3 rounded-xl p-2 text-white outline-none transition-colors focus:bg-white/8"
|
||||
class="group flex gap-3 rounded-xl p-2 text-white transition-colors outline-none focus:bg-white/8"
|
||||
on:click={() =>
|
||||
trackEvent({
|
||||
plausible: {
|
||||
@@ -57,7 +57,7 @@
|
||||
|
||||
{#if product.beta}
|
||||
<span
|
||||
class="text-caption bg-accent/24 ml-1 rounded py-1 px-2 font-medium text-white"
|
||||
class="text-caption bg-accent/24 ml-1 rounded px-2 py-1 font-medium text-white"
|
||||
>Coming soon</span
|
||||
>
|
||||
{/if}
|
||||
@@ -81,7 +81,7 @@
|
||||
href={sublink.href}
|
||||
class="text-caption text-primary flex items-center gap-2"
|
||||
>
|
||||
{sublink.label} <span class="web-icon-chevron-right" />
|
||||
{sublink.label} <span class="web-icon-chevron-right"></span>
|
||||
</a>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
@@ -65,9 +65,9 @@
|
||||
|
||||
<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 { trackEvent } from '$lib/actions/analytics';
|
||||
|
||||
const {
|
||||
elements: { trigger, menu, item, overlay },
|
||||
@@ -94,13 +94,13 @@
|
||||
class={classNames('web-icon-chevron-down block transition-transform', {
|
||||
'rotate-180': $open
|
||||
})}
|
||||
/>
|
||||
></span>
|
||||
</button>
|
||||
|
||||
<div
|
||||
use:melt={$menu}
|
||||
class={classNames(
|
||||
'data-[state=closed]:animate-fade-out data-[state=open]:animate-fade-in relative !left-1/2 z-10 mt-6 mx-auto hidden w-full -translate-x-1/2 flex-col items-center p-0 outline-none [max-inline-size:86.875rem] md:flex'
|
||||
'data-[state=closed]:animate-fade-out data-[state=open]:animate-fade-in relative !left-1/2 z-10 mx-auto mt-6 hidden w-full -translate-x-1/2 flex-col items-center p-0 outline-none [max-inline-size:86.875rem] md:flex'
|
||||
)}
|
||||
>
|
||||
<div class="is-special-padding w-full rounded-2xl border border-white/8 bg-[#232325] p-6">
|
||||
@@ -123,7 +123,7 @@
|
||||
name: `${product.name} in products submenu`
|
||||
}
|
||||
})}
|
||||
class="group flex gap-3 rounded-xl p-1 text-white outline-none transition-colors focus:bg-white/8"
|
||||
class="group flex gap-3 rounded-xl p-1 text-white transition-colors outline-none focus:bg-white/8"
|
||||
>
|
||||
<div
|
||||
class="flex size-12 shrink-0 items-center justify-center rounded-lg border border-white/12 bg-white/6"
|
||||
@@ -140,7 +140,7 @@
|
||||
|
||||
{#if product.beta}
|
||||
<span
|
||||
class="text-caption bg-accent/24 ml-1 rounded py-1 px-2 font-medium text-white"
|
||||
class="text-caption bg-accent/24 ml-1 rounded px-2 py-1 font-medium text-white"
|
||||
>Coming soon</span
|
||||
>
|
||||
{/if}
|
||||
@@ -154,8 +154,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-span-4 -ml-12 border-l border-white/6 pl-12">
|
||||
<a
|
||||
href="/blog/post/customer-story-storealert"
|
||||
<div
|
||||
use:melt={$item}
|
||||
class="group block rounded-2xl border border-white/12 bg-white/6 p-4 outline-none focus-within:bg-white/12"
|
||||
>
|
||||
@@ -169,11 +168,14 @@
|
||||
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"
|
||||
/></a
|
||||
></span></a
|
||||
>
|
||||
</header>
|
||||
|
||||
<div class="my-4 flex flex-1 gap-3 outline-none">
|
||||
<a
|
||||
href="/blog/post/customer-story-storealert"
|
||||
class="my-4 flex flex-1 gap-3 outline-none"
|
||||
>
|
||||
<img
|
||||
src="/images/blog/customer-story-storealert/cover.png"
|
||||
alt="Case study cover"
|
||||
@@ -183,8 +185,8 @@
|
||||
Empowering Shopify merchants with real-time store monitoring using
|
||||
StoreAlert
|
||||
</p>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="mt-8">
|
||||
<span
|
||||
@@ -200,7 +202,7 @@
|
||||
{sublink.label}
|
||||
<span
|
||||
class="web-icon-chevron-right transition-transform group-hover:translate-x-0.5"
|
||||
/>
|
||||
></span>
|
||||
</a>
|
||||
{/each}
|
||||
</div>
|
||||
@@ -211,5 +213,5 @@
|
||||
<div
|
||||
use:melt={$overlay}
|
||||
class="data-[state=closed]:animate-fade-out fixed inset-0 bg-black/60"
|
||||
/>
|
||||
></div>
|
||||
</div>
|
||||
|
||||
@@ -160,8 +160,9 @@
|
||||
on:click={handleExit}
|
||||
>
|
||||
<div class="web-input-text-search-wrapper web-u-margin-inline-20 w-full max-w-[680px]">
|
||||
<span class="web-icon-search z-[5]" aria-hidden="true" style="inset-block-start:0.9rem" />
|
||||
<div id="searchbox" />
|
||||
<span class="web-icon-search z-[5]" aria-hidden="true" style="inset-block-start:0.9rem"
|
||||
></span>
|
||||
<div id="searchbox"></div>
|
||||
|
||||
<input
|
||||
class="web-input-button bg-greyscale-800/75! relative z-1 !rounded-b-none !pl-10"
|
||||
@@ -222,7 +223,7 @@
|
||||
</div>
|
||||
{#if hit.p}
|
||||
<div
|
||||
class="web-u-color-text-secondary w-full overflow-hidden text-ellipsis whitespace-nowrap text-left"
|
||||
class="web-u-color-text-secondary w-full overflow-hidden text-left text-ellipsis whitespace-nowrap"
|
||||
>
|
||||
{hit.p}
|
||||
</div>
|
||||
|
||||
@@ -97,11 +97,11 @@
|
||||
>
|
||||
<div class="physical-select">
|
||||
{#if selectedOption?.icon}
|
||||
<span class={selectedOption.icon} aria-hidden="true" />
|
||||
<span class={selectedOption.icon} aria-hidden="true"></span>
|
||||
{/if}
|
||||
<span>{$selectedLabel || initialLabel}</span>
|
||||
</div>
|
||||
<span class="icon-cheveron-{$open ? 'up' : 'down'}" aria-hidden="true" />
|
||||
<span class="icon-cheveron-{$open ? 'up' : 'down'}" aria-hidden="true"></span>
|
||||
</button>
|
||||
|
||||
{#if $open}
|
||||
@@ -119,7 +119,7 @@
|
||||
{#each group.options as option}
|
||||
<button class="web-select-option" use:melt={$optionEl(option)}>
|
||||
{#if option.icon}
|
||||
<span class={option.icon} aria-hidden="true" />
|
||||
<span class={option.icon} aria-hidden="true"></span>
|
||||
{/if}
|
||||
<span>{option.label}</span>
|
||||
</button>
|
||||
@@ -134,7 +134,7 @@
|
||||
{#each group.options as option}
|
||||
<button class="web-select-option" use:melt={$optionEl(option)}>
|
||||
{#if option.icon}
|
||||
<span class={option.icon} aria-hidden="true" />
|
||||
<span class={option.icon} aria-hidden="true"></span>
|
||||
{/if}
|
||||
<span style:text-transform="capitalize">{option.label}</span>
|
||||
</button>
|
||||
@@ -150,7 +150,7 @@
|
||||
style:display={nativeMobile ? undefined : 'none'}
|
||||
>
|
||||
{#if selectedOption?.icon}
|
||||
<span class={selectedOption.icon} aria-hidden="true" />
|
||||
<span class={selectedOption.icon} aria-hidden="true"></span>
|
||||
{/if}
|
||||
<select {id} bind:value>
|
||||
{#each groups as group}
|
||||
@@ -172,7 +172,7 @@
|
||||
{/if}
|
||||
{/each}
|
||||
</select>
|
||||
<span class="icon-cheveron-{$open ? 'up' : 'down'}" aria-hidden="true" />
|
||||
<span class="icon-cheveron-{$open ? 'up' : 'down'}" aria-hidden="true"></span>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
|
||||
<div class="melt-switch">
|
||||
<button use:melt={$root}>
|
||||
<span class="thumb" />
|
||||
<span class="thumb"></span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -85,7 +85,9 @@
|
||||
/>
|
||||
</a>
|
||||
</li>
|
||||
<svelte:fragment slot="tooltip">{platform.name}</svelte:fragment>
|
||||
{#snippet tooltip()}
|
||||
{platform.name}
|
||||
{/snippet}
|
||||
</Tooltip>
|
||||
{/each}
|
||||
</ul>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { type Theme, currentTheme } from '$routes/+layout.svelte';
|
||||
|
||||
import Select, { type SelectOption } from './Select.svelte';
|
||||
|
||||
const options: SelectOption<Theme>[] = [
|
||||
|
||||
@@ -1,12 +1,28 @@
|
||||
<script lang="ts">
|
||||
import { createTooltip, melt } from '@melt-ui/svelte';
|
||||
import type { FloatingConfig } from '@melt-ui/svelte/internal/actions';
|
||||
import { type Snippet } from 'svelte';
|
||||
import { fly, type FlyParams } from 'svelte/transition';
|
||||
|
||||
export let placement: NonNullable<FloatingConfig>['placement'] = 'top';
|
||||
export let disabled = false;
|
||||
export let closeOnPointerDown = false;
|
||||
export let disableHoverableContent = false;
|
||||
interface Props {
|
||||
placement?: NonNullable<FloatingConfig>['placement'];
|
||||
disabled?: boolean;
|
||||
closeOnPointerDown?: boolean;
|
||||
disableHoverableContent?: boolean;
|
||||
asChild?: Snippet<[any]>;
|
||||
children?: Snippet;
|
||||
tooltip: Snippet;
|
||||
}
|
||||
|
||||
const {
|
||||
placement = 'top',
|
||||
disabled = false,
|
||||
closeOnPointerDown = false,
|
||||
disableHoverableContent = false,
|
||||
asChild,
|
||||
children,
|
||||
tooltip
|
||||
}: Props = $props();
|
||||
|
||||
const {
|
||||
elements: { trigger, content, arrow },
|
||||
@@ -21,7 +37,8 @@
|
||||
disableHoverableContent
|
||||
});
|
||||
|
||||
$: flyParams = (function getFlyParams() {
|
||||
let flyParams = $derived(
|
||||
(function getFlyParams() {
|
||||
const params: FlyParams = {
|
||||
duration: 150
|
||||
};
|
||||
@@ -51,20 +68,21 @@
|
||||
}
|
||||
|
||||
return params;
|
||||
})();
|
||||
})()
|
||||
);
|
||||
</script>
|
||||
|
||||
<slot name="asChild" trigger={$trigger} />
|
||||
|
||||
{#if !$$slots.asChild}
|
||||
{#if asChild}
|
||||
{@render asChild({ trigger: $trigger })}
|
||||
{:else if children}
|
||||
<span use:melt={$trigger}>
|
||||
<slot />
|
||||
{@render children()}
|
||||
</span>
|
||||
{/if}
|
||||
|
||||
{#if $open && !disabled}
|
||||
{#if tooltip && $open && !disabled}
|
||||
<div use:melt={$content} class="web-tooltip text-sub-body" transition:fly={flyParams}>
|
||||
<div use:melt={$arrow} />
|
||||
<slot name="tooltip" />
|
||||
<div use:melt={$arrow}></div>
|
||||
{@render tooltip()}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<span class="relative">
|
||||
{#each words as word, i}
|
||||
<span
|
||||
class="animate-text mr-2 inline-block"
|
||||
class="animate-enter mr-2 inline-block"
|
||||
style:animation-delay="{i * 75}ms
|
||||
">{word}</span
|
||||
>
|
||||
612
src/lib/components/appwrite-network/data/pins.ts
Normal file
@@ -0,0 +1,612 @@
|
||||
export const pins = {
|
||||
'pop-locations': [
|
||||
{
|
||||
lat: 39.04,
|
||||
lng: -77.49,
|
||||
city: 'Ashburn',
|
||||
code: 'ASH',
|
||||
available: true,
|
||||
offsetX: 10,
|
||||
offsetY: -10
|
||||
},
|
||||
{
|
||||
lat: 33.75,
|
||||
lng: -84.39,
|
||||
city: 'Atlanta',
|
||||
code: 'ATL',
|
||||
available: true
|
||||
},
|
||||
{
|
||||
lat: 42.36,
|
||||
lng: -71.06,
|
||||
city: 'Boston',
|
||||
code: 'BOS',
|
||||
available: true
|
||||
},
|
||||
{
|
||||
lat: 51.05,
|
||||
lng: -114.07,
|
||||
city: 'Calgary',
|
||||
code: 'CAL',
|
||||
available: true
|
||||
},
|
||||
{
|
||||
lat: 41.88,
|
||||
lng: -87.63,
|
||||
city: 'Chicago',
|
||||
code: 'CHI',
|
||||
available: true
|
||||
},
|
||||
{
|
||||
lat: 39.96,
|
||||
lng: -82.99,
|
||||
city: 'Columbus',
|
||||
code: 'COL',
|
||||
available: true
|
||||
},
|
||||
{
|
||||
lat: 32.78,
|
||||
lng: -96.8,
|
||||
city: 'Dallas',
|
||||
code: 'DAL',
|
||||
available: true
|
||||
},
|
||||
{
|
||||
lat: 39.74,
|
||||
lng: -104.99,
|
||||
city: 'Denver',
|
||||
code: 'DEN',
|
||||
available: true
|
||||
},
|
||||
{
|
||||
lat: 42.33,
|
||||
lng: -83.05,
|
||||
city: 'Detroit',
|
||||
code: 'DET',
|
||||
available: true
|
||||
},
|
||||
{
|
||||
lat: 28.31,
|
||||
lng: -125.86,
|
||||
city: 'Honolulu',
|
||||
code: 'HNL',
|
||||
available: true
|
||||
},
|
||||
{
|
||||
lat: 29.76,
|
||||
lng: -95.37,
|
||||
city: 'Houston',
|
||||
code: 'HOU',
|
||||
available: true
|
||||
},
|
||||
{
|
||||
lat: 30.33,
|
||||
lng: -81.66,
|
||||
city: 'Jacksonville',
|
||||
code: 'JAX',
|
||||
available: true
|
||||
},
|
||||
{
|
||||
lat: 39.1,
|
||||
lng: -94.58,
|
||||
city: 'Kansas City',
|
||||
code: 'KC',
|
||||
available: true
|
||||
},
|
||||
{
|
||||
lat: 34.05,
|
||||
lng: -118.24,
|
||||
city: 'Los Angeles',
|
||||
code: 'LA',
|
||||
available: true
|
||||
},
|
||||
{
|
||||
lat: 25.77,
|
||||
lng: -80.19,
|
||||
city: 'Miami',
|
||||
code: 'MIA',
|
||||
available: true
|
||||
},
|
||||
{
|
||||
lat: 44.98,
|
||||
lng: -93.27,
|
||||
city: 'Minneapolis',
|
||||
code: 'MIN',
|
||||
available: true
|
||||
},
|
||||
{
|
||||
lat: 45.5,
|
||||
lng: -73.57,
|
||||
city: 'Montreal',
|
||||
code: 'MTL',
|
||||
available: true
|
||||
},
|
||||
{
|
||||
lat: 40.71,
|
||||
lng: -74.01,
|
||||
city: 'New York',
|
||||
code: 'NYC',
|
||||
available: true
|
||||
},
|
||||
{
|
||||
lat: 33.45,
|
||||
lng: -112.07,
|
||||
city: 'Phoenix',
|
||||
code: 'PHX',
|
||||
available: true
|
||||
},
|
||||
{
|
||||
lat: 45.52,
|
||||
lng: -122.68,
|
||||
city: 'Portland',
|
||||
code: 'PDX',
|
||||
available: true
|
||||
},
|
||||
{
|
||||
lat: 37.34,
|
||||
lng: -121.89,
|
||||
city: 'San Jose',
|
||||
code: 'SJ',
|
||||
available: true
|
||||
},
|
||||
{
|
||||
lat: 47.61,
|
||||
lng: -122.33,
|
||||
city: 'Seattle',
|
||||
code: 'SEA',
|
||||
available: true
|
||||
},
|
||||
{
|
||||
lat: 38.63,
|
||||
lng: -90.2,
|
||||
city: 'St Louis',
|
||||
code: 'STL',
|
||||
available: true
|
||||
},
|
||||
{
|
||||
lat: 43.65,
|
||||
lng: -79.38,
|
||||
city: 'Toronto',
|
||||
code: 'TOR',
|
||||
available: true
|
||||
},
|
||||
{
|
||||
lat: 49.28,
|
||||
lng: -123.12,
|
||||
city: 'Vancouver',
|
||||
code: 'VAN',
|
||||
available: true
|
||||
},
|
||||
{
|
||||
lat: 4.71,
|
||||
lng: -74.07,
|
||||
city: 'Bogota',
|
||||
code: 'BOG',
|
||||
available: true
|
||||
},
|
||||
{
|
||||
lat: -34.61,
|
||||
lng: -58.38,
|
||||
city: 'Buenos Aires',
|
||||
code: 'BUE',
|
||||
available: true
|
||||
},
|
||||
{
|
||||
lat: -25.43,
|
||||
lng: -49.27,
|
||||
city: 'Curitiba',
|
||||
code: 'CUR',
|
||||
available: true
|
||||
},
|
||||
{
|
||||
lat: -8.73,
|
||||
lng: -38.52,
|
||||
city: 'Fortaleza',
|
||||
code: 'FOR',
|
||||
available: true
|
||||
},
|
||||
{
|
||||
lat: -12.05,
|
||||
lng: -77.04,
|
||||
city: 'Lima',
|
||||
code: 'LIM',
|
||||
available: true
|
||||
},
|
||||
{
|
||||
lat: -23.55,
|
||||
lng: -46.63,
|
||||
city: 'São Paulo',
|
||||
code: 'SAO',
|
||||
available: true
|
||||
},
|
||||
{
|
||||
lat: -33.45,
|
||||
lng: -70.67,
|
||||
city: 'Santiago',
|
||||
code: 'SCL',
|
||||
available: true
|
||||
},
|
||||
{
|
||||
lat: -22.91,
|
||||
lng: -43.17,
|
||||
city: 'Rio de Janeiro',
|
||||
code: 'RIO',
|
||||
available: true
|
||||
},
|
||||
{
|
||||
lat: 52.37,
|
||||
lng: 4.89,
|
||||
city: 'Amsterdam',
|
||||
code: 'AMS',
|
||||
available: true
|
||||
},
|
||||
{
|
||||
lat: 55.68,
|
||||
lng: 12.57,
|
||||
city: 'Copenhagen',
|
||||
code: 'CPH',
|
||||
available: true
|
||||
},
|
||||
{
|
||||
lat: 50.85,
|
||||
lng: 4.35,
|
||||
city: 'Brussels',
|
||||
code: 'BRU',
|
||||
available: true
|
||||
},
|
||||
{
|
||||
lat: 55.35,
|
||||
lng: -10.26,
|
||||
city: 'Dublin',
|
||||
code: 'DUB',
|
||||
available: true
|
||||
},
|
||||
{
|
||||
lat: 50.11,
|
||||
lng: 8.68,
|
||||
city: 'Frankfurt',
|
||||
code: 'FRA',
|
||||
available: true
|
||||
},
|
||||
{
|
||||
lat: 60.17,
|
||||
lng: 24.94,
|
||||
city: 'Helsinki',
|
||||
code: 'HEL',
|
||||
available: true
|
||||
},
|
||||
{
|
||||
lat: 40.72,
|
||||
lng: -12.14,
|
||||
city: 'Lisbon',
|
||||
code: 'LIS',
|
||||
available: true
|
||||
},
|
||||
{
|
||||
lat: 51.51,
|
||||
lng: -0.13,
|
||||
city: 'London',
|
||||
code: 'LON',
|
||||
available: true
|
||||
},
|
||||
{
|
||||
lat: 46.42,
|
||||
lng: -3.7,
|
||||
city: 'Madrid',
|
||||
code: 'MAD',
|
||||
available: true
|
||||
},
|
||||
{
|
||||
lat: 53.48,
|
||||
lng: -2.24,
|
||||
city: 'Manchester',
|
||||
code: 'MAN',
|
||||
available: true
|
||||
},
|
||||
{
|
||||
lat: 43.3,
|
||||
lng: 5.37,
|
||||
city: 'Marseille',
|
||||
code: 'MRS',
|
||||
available: true
|
||||
},
|
||||
{
|
||||
lat: 45.46,
|
||||
lng: 9.19,
|
||||
city: 'Milan',
|
||||
code: 'MIL',
|
||||
available: true
|
||||
},
|
||||
{
|
||||
lat: 48.14,
|
||||
lng: 11.58,
|
||||
city: 'Munich',
|
||||
code: 'MUN',
|
||||
available: true
|
||||
},
|
||||
{
|
||||
lat: 62.91,
|
||||
lng: 8.75,
|
||||
city: 'Oslo',
|
||||
code: 'OSL',
|
||||
available: true
|
||||
},
|
||||
{
|
||||
lat: 38.12,
|
||||
lng: 13.36,
|
||||
city: 'Palermo',
|
||||
code: 'PAL',
|
||||
available: true
|
||||
},
|
||||
{
|
||||
lat: 48.86,
|
||||
lng: 2.35,
|
||||
city: 'Paris',
|
||||
code: 'PAR',
|
||||
available: true
|
||||
},
|
||||
{
|
||||
lat: 41.9,
|
||||
lng: 12.5,
|
||||
city: 'Rome',
|
||||
code: 'ROM',
|
||||
available: true
|
||||
},
|
||||
{
|
||||
lat: 42.7,
|
||||
lng: 23.32,
|
||||
city: 'Sofia',
|
||||
code: 'SOF',
|
||||
available: true
|
||||
},
|
||||
{
|
||||
lat: 59.33,
|
||||
lng: 18.07,
|
||||
city: 'Stockholm',
|
||||
code: 'STO',
|
||||
available: true
|
||||
},
|
||||
{
|
||||
lat: 48.21,
|
||||
lng: 16.37,
|
||||
city: 'Vienna',
|
||||
code: 'VIE',
|
||||
available: true
|
||||
},
|
||||
{
|
||||
lat: 5.56,
|
||||
lng: -0.2,
|
||||
city: 'Accra',
|
||||
code: 'ACC',
|
||||
available: true
|
||||
},
|
||||
{
|
||||
lat: -33.93,
|
||||
lng: 18.42,
|
||||
city: 'Cape Town',
|
||||
code: 'CPT',
|
||||
available: true
|
||||
},
|
||||
{
|
||||
lat: -26.2,
|
||||
lng: 28.05,
|
||||
city: 'Johannesburg',
|
||||
code: 'JHB',
|
||||
available: true
|
||||
},
|
||||
{
|
||||
lat: 13.75,
|
||||
lng: 100.5,
|
||||
city: 'Bangkok',
|
||||
code: 'BKK',
|
||||
available: true
|
||||
},
|
||||
{
|
||||
lat: 13.08,
|
||||
lng: 80.28,
|
||||
city: 'Chennai',
|
||||
code: 'CHE',
|
||||
available: true
|
||||
},
|
||||
{
|
||||
lat: 25.27,
|
||||
lng: 55.3,
|
||||
city: 'Dubai',
|
||||
code: 'DXB',
|
||||
available: true
|
||||
},
|
||||
{
|
||||
lat: 25.12,
|
||||
lng: 56.33,
|
||||
city: 'Fujairah',
|
||||
code: 'FUJ',
|
||||
available: true
|
||||
},
|
||||
{
|
||||
lat: 22.32,
|
||||
lng: 114.17,
|
||||
city: 'Hong Kong',
|
||||
code: 'HK',
|
||||
available: true
|
||||
},
|
||||
{
|
||||
lat: 17.38,
|
||||
lng: 78.48,
|
||||
city: 'Hyderabad',
|
||||
code: 'HYD',
|
||||
available: true
|
||||
},
|
||||
{
|
||||
lat: 22.57,
|
||||
lng: 88.36,
|
||||
city: 'Kolkata',
|
||||
code: 'KOL',
|
||||
available: true
|
||||
},
|
||||
{
|
||||
lat: 3.14,
|
||||
lng: 101.69,
|
||||
city: 'Kuala Lumpur',
|
||||
code: 'KL',
|
||||
available: true
|
||||
},
|
||||
{
|
||||
lat: 14.6,
|
||||
lng: 120.98,
|
||||
city: 'Manila',
|
||||
code: 'MNL',
|
||||
available: true
|
||||
},
|
||||
{
|
||||
lat: 19.08,
|
||||
lng: 72.88,
|
||||
city: 'Mumbai',
|
||||
code: 'MUM',
|
||||
available: true
|
||||
},
|
||||
{
|
||||
lat: 28.61,
|
||||
lng: 77.21,
|
||||
city: 'New Delhi',
|
||||
code: 'DEL',
|
||||
available: true
|
||||
},
|
||||
{
|
||||
lat: 34.69,
|
||||
lng: 135.5,
|
||||
city: 'Osaka',
|
||||
code: 'OSA',
|
||||
available: true
|
||||
},
|
||||
{
|
||||
lat: 37.57,
|
||||
lng: 126.98,
|
||||
city: 'Seoul',
|
||||
code: 'SEL',
|
||||
available: true
|
||||
},
|
||||
{
|
||||
lat: 1.35,
|
||||
lng: 103.82,
|
||||
city: 'Singapore',
|
||||
code: 'SIN',
|
||||
available: true
|
||||
},
|
||||
{
|
||||
lat: 35.69,
|
||||
lng: 139.69,
|
||||
city: 'Tokyo',
|
||||
code: 'TYO',
|
||||
available: true
|
||||
},
|
||||
{
|
||||
lat: -34.93,
|
||||
lng: 138.6,
|
||||
city: 'Adelaide',
|
||||
code: 'ADL',
|
||||
available: true
|
||||
},
|
||||
{
|
||||
lat: -39.85,
|
||||
lng: 174.76,
|
||||
city: 'Auckland',
|
||||
code: 'AKL',
|
||||
available: true
|
||||
},
|
||||
{
|
||||
lat: -27.47,
|
||||
lng: 153.03,
|
||||
city: 'Brisbane',
|
||||
code: 'BNE',
|
||||
available: true
|
||||
},
|
||||
{
|
||||
lat: -48.53,
|
||||
lng: 150.64,
|
||||
city: 'Christchurch',
|
||||
code: 'CHC',
|
||||
available: true
|
||||
},
|
||||
{
|
||||
lat: -37.81,
|
||||
lng: 144.96,
|
||||
city: 'Melbourne',
|
||||
code: 'MEL',
|
||||
available: true
|
||||
},
|
||||
{
|
||||
lat: -31.95,
|
||||
lng: 115.85,
|
||||
city: 'Perth',
|
||||
code: 'PER',
|
||||
available: true
|
||||
},
|
||||
{
|
||||
lat: -33.87,
|
||||
lng: 151.21,
|
||||
city: 'Sydney',
|
||||
code: 'SYD',
|
||||
available: true
|
||||
},
|
||||
{
|
||||
lat: -45.29,
|
||||
lng: 158.78,
|
||||
city: 'Wellington',
|
||||
code: 'WLG',
|
||||
available: true
|
||||
}
|
||||
],
|
||||
edges: [
|
||||
{
|
||||
lat: 40.71,
|
||||
lng: -74.01,
|
||||
city: 'New York',
|
||||
code: 'NYC',
|
||||
available: true,
|
||||
offsetX: 10,
|
||||
offsetY: -10
|
||||
},
|
||||
{
|
||||
lat: 50.11,
|
||||
lng: 8.68,
|
||||
city: 'Frankfurt',
|
||||
code: 'FRA',
|
||||
available: true
|
||||
},
|
||||
{
|
||||
lat: -33.87,
|
||||
lng: 151.21,
|
||||
city: 'Sydney',
|
||||
code: 'AUS',
|
||||
available: true
|
||||
}
|
||||
],
|
||||
regions: [
|
||||
{
|
||||
lat: 40.71,
|
||||
lng: -74.01,
|
||||
city: 'New York',
|
||||
code: 'NYC',
|
||||
available: true,
|
||||
offsetX: 10,
|
||||
offsetY: -10
|
||||
},
|
||||
{
|
||||
lat: 50.11,
|
||||
lng: 8.68,
|
||||
city: 'Frankfurt',
|
||||
code: 'FRA',
|
||||
available: true
|
||||
},
|
||||
|
||||
{
|
||||
lat: -33.87,
|
||||
lng: 151.21,
|
||||
city: 'Sydney',
|
||||
code: 'AUS',
|
||||
available: true
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
export type PinSegment = keyof typeof pins;
|
||||
64
src/lib/components/appwrite-network/map-marker.svelte
Normal file
@@ -0,0 +1,64 @@
|
||||
<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>
|
||||
55
src/lib/components/appwrite-network/map-nav.svelte
Normal file
@@ -0,0 +1,55 @@
|
||||
<script lang="ts">
|
||||
import { classNames } from '$lib/utils/classnames';
|
||||
import { Tabs } from 'bits-ui';
|
||||
|
||||
import type { IconType } from '../ui';
|
||||
import Icon from '../ui/icon';
|
||||
|
||||
type Props = {
|
||||
onValueChange: (value: string) => void;
|
||||
};
|
||||
|
||||
const { onValueChange }: Props = $props();
|
||||
|
||||
const navItems: Array<{ label: string; value: string; icon: IconType }> = [
|
||||
{
|
||||
label: 'PoP Locations',
|
||||
value: 'pop-locations',
|
||||
icon: 'pop-locations'
|
||||
},
|
||||
{
|
||||
label: 'Edges',
|
||||
value: 'edges',
|
||||
icon: 'edge'
|
||||
},
|
||||
{
|
||||
label: 'Regions',
|
||||
value: 'regions',
|
||||
icon: 'regions'
|
||||
}
|
||||
];
|
||||
</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"
|
||||
>
|
||||
{#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>
|
||||
58
src/lib/components/appwrite-network/map-tooltip.svelte
Normal file
@@ -0,0 +1,58 @@
|
||||
<script lang="ts" module>
|
||||
import { classNames } from '$lib/utils/classnames';
|
||||
import { writable } from 'svelte/store';
|
||||
|
||||
export const tooltipData = writable<{
|
||||
city: string | null;
|
||||
code: string | null;
|
||||
available: boolean | null;
|
||||
}>({
|
||||
city: null,
|
||||
code: null,
|
||||
available: null
|
||||
});
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
type Props = {
|
||||
coords: {
|
||||
x: number;
|
||||
y: number;
|
||||
};
|
||||
};
|
||||
|
||||
const { coords }: Props = $props();
|
||||
</script>
|
||||
|
||||
{#if $tooltipData.city}
|
||||
<div
|
||||
class="pointer-events-none absolute"
|
||||
style:left="{coords.x - 50}px"
|
||||
style:top="{coords.y - 50}px"
|
||||
>
|
||||
<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'
|
||||
)}
|
||||
>
|
||||
<span class="text-primary text-caption w-fit">
|
||||
{$tooltipData.city}
|
||||
({$tooltipData.code})
|
||||
</span>
|
||||
{#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]"
|
||||
>
|
||||
<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"
|
||||
>
|
||||
<span class="text-micro -tracking-tight">Planned</span>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
125
src/lib/components/appwrite-network/map.svelte
Normal file
@@ -0,0 +1,125 @@
|
||||
<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 { 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';
|
||||
|
||||
let dimensions = $state({
|
||||
width: 0,
|
||||
height: 0
|
||||
});
|
||||
|
||||
let activeRegion = $state<string | null>(null);
|
||||
let activeMarker: HTMLElement | null = null;
|
||||
let activeSegment = $state<string>('pop-locations');
|
||||
|
||||
const { action: mousePosition, position } = useMousePosition();
|
||||
const { action: inView, animate } = useAnimateInView({});
|
||||
|
||||
const scrollMarkerIntoView = (marker: HTMLElement) => {
|
||||
return new Promise<void>((resolve) => {
|
||||
marker.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'nearest',
|
||||
inline: 'center'
|
||||
});
|
||||
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
if (entries[0].intersectionRatio > 0.5) {
|
||||
observer.disconnect();
|
||||
resolve();
|
||||
}
|
||||
},
|
||||
{ threshold: [0, 0.25, 0.5, 0.75, 1] }
|
||||
);
|
||||
|
||||
observer.observe(marker);
|
||||
});
|
||||
};
|
||||
|
||||
const handleSetActiveMarker = async (city: string) => {
|
||||
const citySlug = slugify(city);
|
||||
|
||||
if (activeRegion === citySlug) {
|
||||
activeMarker = null;
|
||||
activeRegion = null;
|
||||
return;
|
||||
}
|
||||
|
||||
activeMarker = document.querySelector(`[data-region="${citySlug}"]`);
|
||||
|
||||
if (activeMarker) {
|
||||
await scrollMarkerIntoView(activeMarker);
|
||||
activeRegion = citySlug;
|
||||
}
|
||||
};
|
||||
</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 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}
|
||||
>
|
||||
<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}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<MapTooltip coords={$position} />
|
||||
<MapNav onValueChange={(value) => (activeSegment = value)} />
|
||||
24
src/lib/components/appwrite-network/utils/projections.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
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 }
|
||||
};
|
||||
@@ -1,5 +1,6 @@
|
||||
<script context="module" lang="ts">
|
||||
import { PUBLIC_GROWTH_ENDPOINT } from '$env/static/public';
|
||||
import { Button } from '$lib/components/ui';
|
||||
|
||||
export const subscribeToNewsletter = async (email: string) => {
|
||||
const response = await fetch(`${PUBLIC_GROWTH_ENDPOINT}/newsletter/subscribe`, {
|
||||
@@ -96,9 +97,7 @@
|
||||
name="email"
|
||||
bind:value={email}
|
||||
/>
|
||||
<button type="submit" class="web-button" disabled={submitting}
|
||||
>Sign up</button
|
||||
>
|
||||
<Button type="submit" disabled={submitting}>Sign up</Button>
|
||||
</div>
|
||||
|
||||
{#if error}
|
||||
|
||||
@@ -73,14 +73,14 @@
|
||||
target="_blank"
|
||||
rel="noopener, noreferrer"
|
||||
>
|
||||
<span class={sharingOption.icon} aria-hidden="true" />
|
||||
<span class={sharingOption.icon} aria-hidden="true"></span>
|
||||
</a>
|
||||
{:else}
|
||||
<button
|
||||
aria-label={sharingOption.label}
|
||||
on:click={() => handleCopy(currentURL)}
|
||||
>
|
||||
<span class={sharingOption.icon} aria-hidden="true" />
|
||||
<span class={sharingOption.icon} aria-hidden="true"></span>
|
||||
</button>
|
||||
{/if}
|
||||
</li>
|
||||
|
||||
@@ -50,8 +50,8 @@
|
||||
'before:bg-greyscale-300 before:absolute before:top-0 before:left-0 before:h-6 before:w-px before:rounded-full before:opacity-0 before:transition-opacity',
|
||||
{
|
||||
'font-medium': parent.level && parent.level === 1,
|
||||
'ps-6': parent.level && parent.level === 2,
|
||||
'ps-12': parent.level && parent.level >= 3,
|
||||
'pl-12': parent.level && parent.level === 2,
|
||||
'ps-16': parent.level && parent.level >= 3,
|
||||
'before:opacity-100': parent.selected
|
||||
}
|
||||
)}
|
||||
@@ -92,7 +92,7 @@
|
||||
out:fade
|
||||
in:fade
|
||||
>
|
||||
<span class="web-icon-arrow-up transition group-hover:-translate-y-0.5" />
|
||||
<span class="web-icon-arrow-up transition group-hover:-translate-y-0.5"></span>
|
||||
Back to Top
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
@@ -131,12 +131,20 @@
|
||||
<div class="embla web-carousel relative overflow-hidden">
|
||||
{#if showArrows}
|
||||
{#if hasPrev}
|
||||
<button class="web-carousel-button web-carousel-button-start" on:click={onPrev}>
|
||||
<button
|
||||
class="web-carousel-button web-carousel-button-start"
|
||||
on:click={onPrev}
|
||||
aria-label="Previous slide"
|
||||
>
|
||||
<span class="web-icon-arrow-left" aria-hidden="true"></span>
|
||||
</button>
|
||||
{/if}
|
||||
{#if hasNext}
|
||||
<button class="web-carousel-button web-carousel-button-end" on:click={onNext}>
|
||||
<button
|
||||
class="web-carousel-button web-carousel-button-end"
|
||||
on:click={onNext}
|
||||
aria-label="Next slide"
|
||||
>
|
||||
<span class="web-icon-arrow-right" aria-hidden="true"></span>
|
||||
</button>
|
||||
{/if}
|
||||
@@ -147,7 +155,7 @@
|
||||
<slot />
|
||||
</ul>
|
||||
</div>
|
||||
<div class="shadow" />
|
||||
<div class="shadow"></div>
|
||||
</div>
|
||||
|
||||
{#if showBullets}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { classNames } from '$lib/utils/classnames';
|
||||
import Button from '$lib/components/ui/button.svelte';
|
||||
|
||||
type $$Props = {
|
||||
eyebrow: {
|
||||
@@ -36,7 +37,7 @@
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="border-smooth box-content flex items-center border-b bg-[url(/images/bgs/mobile-hero.png)] bg-cover bg-bottom py-12 px-5 md:bg-[url(/images/bgs/hero.png)] md:bg-center md:pt-32 md:pb-40 lg:px-8 xl:px-16"
|
||||
class="border-smooth box-content flex items-center border-b bg-[url(/images/bgs/mobile-hero.png)] bg-cover bg-bottom px-5 py-12 md:bg-[url(/images/bgs/hero.png)] md:bg-center md:pt-32 md:pb-40 lg:px-8 xl:px-16"
|
||||
>
|
||||
<div class="mx-auto grid max-w-[75rem] items-center gap-16 md:grid-cols-2">
|
||||
<div class="space-y-8">
|
||||
@@ -51,18 +52,18 @@
|
||||
>
|
||||
{title}
|
||||
</h1>
|
||||
<p class="text-description text-secondary text-pretty font-medium">
|
||||
<p class="text-description text-secondary font-medium text-pretty">
|
||||
{description}
|
||||
</p>
|
||||
|
||||
<div class="flex flex-col items-center gap-2 md:flex-row">
|
||||
<a href={cta.url} class="web-button !w-full md:!w-fit">
|
||||
<Button href={cta.url} class="!w-full md:!w-fit">
|
||||
{cta.label}
|
||||
</a>
|
||||
</Button>
|
||||
{#if secondaryCta}
|
||||
<a href={secondaryCta.url} class="web-button is-secondary !w-full md:!w-fit">
|
||||
<Button variant="secondary" href={secondaryCta.url} class="!w-full md:!w-fit">
|
||||
{secondaryCta.label}
|
||||
</a>
|
||||
</Button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -62,7 +62,7 @@
|
||||
<div class="flex items-center gap-2">
|
||||
<img src={product.icon} alt="auth" width="32" height="32" />
|
||||
<h4 class="text-body text-primary">{product.title}</h4>
|
||||
<span class="web-icon-arrow-right ml-auto" aria-hidden="true" />
|
||||
<span class="web-icon-arrow-right ml-auto" aria-hidden="true"></span>
|
||||
</div>
|
||||
<p class="text-sub-body">
|
||||
{product.description}
|
||||
|
||||
@@ -42,7 +42,7 @@
|
||||
<div class="group light flex w-fit gap-4">
|
||||
{#each Array.from({ length: 4 }) as _, i}
|
||||
<div
|
||||
class="animate-scroll flex items-center gap-8 group-hover:[animation-play-state:paused;]"
|
||||
class="animate-scroll group-hover:[animation-play-state:paused;] flex items-center gap-8"
|
||||
aria-hidden={i !== 0}
|
||||
>
|
||||
{#each testimonials as testimonial}
|
||||
|
||||
14
src/lib/components/shared/discord-link.svelte
Normal file
@@ -0,0 +1,14 @@
|
||||
<script lang="ts">
|
||||
import { Icon, Button } from '$lib/components/ui';
|
||||
|
||||
type Props = {
|
||||
class?: string;
|
||||
};
|
||||
|
||||
const { class: className }: Props = $props();
|
||||
</script>
|
||||
|
||||
<Button variant="secondary" class={className} href="https://appwrite.io/discord">
|
||||
<Icon name="discord" />
|
||||
<span class="text">Join Discord</span>
|
||||
</Button>
|
||||
22
src/lib/components/shared/github-stats.svelte
Normal file
@@ -0,0 +1,22 @@
|
||||
<script lang="ts">
|
||||
import { SOCIAL_STATS } from '$lib/constants';
|
||||
import { Icon, InlineTag, Button } from '$lib/components/ui';
|
||||
|
||||
type Props = {
|
||||
class?: string;
|
||||
};
|
||||
|
||||
const { class: className }: Props = $props();
|
||||
</script>
|
||||
|
||||
<Button
|
||||
href={SOCIAL_STATS.GITHUB.LINK}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class={className}
|
||||
variant="secondary"
|
||||
>
|
||||
<Icon name="star" aria-hidden="true" />
|
||||
<span class="text">Star on GitHub</span>
|
||||
<InlineTag>{SOCIAL_STATS.GITHUB.STAT}</InlineTag>
|
||||
</Button>
|
||||
2
src/lib/components/shared/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export { default as GithubStats } from './github-stats.svelte';
|
||||
export { default as DiscordLink } from './discord-link.svelte';
|
||||
@@ -1,71 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { classNames } from '$lib/utils/classnames';
|
||||
import type { HTMLButtonAttributes, HTMLAnchorAttributes } from 'svelte/elements';
|
||||
import { cva, type VariantProps } from 'cva';
|
||||
import InlineTag from './InlineTag.svelte';
|
||||
|
||||
const button = cva(
|
||||
[
|
||||
'flex w-fit justify-center px-[0.875rem] h-10 transition-all text-center no-underline select-none min-w-10 bg-origin-border text-white font-medium items-center gap-2 rounded-lg border border-transparent button duration-200'
|
||||
],
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
primary: [
|
||||
'bg-[linear-gradient(135deg,_var(--color-accent)_0%,_var(--color-accent)_61%,_var(--color-secondary-100)_100%)]',
|
||||
'hover:shadow-[0_0_2rem_var(--color-accent-200)] active:not:disabled:shadow-[0_0_2rem_var(--color-accent-200)]'
|
||||
],
|
||||
secondary: [
|
||||
'bg-[#fd366e0a] relative',
|
||||
'hover:shadow-[0_-6px_10px_0px_rgba(253,54,110,0.08)_inset]'
|
||||
],
|
||||
text: [
|
||||
'bg-transparent border-transparent text-white',
|
||||
'hover:backdrop-blur-md hover:bg-[linear-gradient(135deg,_rgba(255,_255,_255,_0.06)_0%,_rgba(255,_255,_255,_0.10)_54.74%,_rgba(255,_255,_255,_0.06)_100%)]'
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
type ButtonProps =
|
||||
| (HTMLButtonAttributes & { href?: undefined })
|
||||
| (HTMLAnchorAttributes & { href: string });
|
||||
|
||||
type $$Props = ButtonProps & VariantProps<typeof button>;
|
||||
|
||||
export let href: $$Props['href'] = undefined;
|
||||
export let variant: $$Props['variant'] = 'primary';
|
||||
const { class: classes, ...props } = $$restProps;
|
||||
|
||||
const buttonClasses = classNames(button({ variant }), classes, {
|
||||
secondary: variant === 'secondary',
|
||||
'leading-tight': $$slots.icon
|
||||
});
|
||||
</script>
|
||||
|
||||
{#if href}
|
||||
<a {...props} {href} class={buttonClasses}>
|
||||
{#if $$slots.icon}
|
||||
<slot name="icon" />
|
||||
{/if}
|
||||
<slot />
|
||||
{#if $$slots.tag}
|
||||
<InlineTag>
|
||||
<slot name="tag" />
|
||||
</InlineTag>
|
||||
{/if}
|
||||
</a>
|
||||
{:else}
|
||||
<button {...props} class={buttonClasses}>
|
||||
{#if $$slots.icon}
|
||||
<slot name="icon" />
|
||||
{/if}
|
||||
<slot />
|
||||
{#if $$slots.tag}
|
||||
<InlineTag>
|
||||
<slot name="tag" />
|
||||
</InlineTag>
|
||||
{/if}
|
||||
</button>
|
||||
{/if}
|
||||
@@ -1,16 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { classNames } from '$lib/utils/classnames';
|
||||
|
||||
let className = '';
|
||||
|
||||
export { className as class };
|
||||
</script>
|
||||
|
||||
<span
|
||||
class={classNames(
|
||||
'block bg-[linear-gradient(6deg,_#f8a1ba,_#fff_35%)] bg-clip-text text-transparent',
|
||||
className
|
||||
)}
|
||||
>
|
||||
<slot />
|
||||
</span>
|
||||
@@ -1,5 +0,0 @@
|
||||
<div
|
||||
class="tracking-none text-sub-body text-greyscale-900 dark:text-greyscale-100 py-0.25 -mr-0.5 rounded-[.25rem] bg-black/8 px-1 dark:bg-white/[0.12]"
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
@@ -21,8 +21,8 @@
|
||||
on:blur
|
||||
bind:value
|
||||
class={classNames(
|
||||
'focus:border-greyscale-100 bg-greyscale-800 border-greyscale-700 flex items-center gap-1 rounded-lg border py-2 px-3 text-sm font-light transition-colors focus-within:border-white active:shadow-sm active:shadow-black/30',
|
||||
'focus:border-greyscale-100 bg-greyscale-800 border-greyscale-700 flex items-center gap-1 rounded-lg border px-3 py-2 text-sm font-light transition-colors focus-within:border-white active:shadow-sm active:shadow-black/30',
|
||||
classes
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
></textarea>
|
||||
|
||||
@@ -1,21 +1,24 @@
|
||||
<script lang="ts">
|
||||
import { classNames } from '$lib/utils/classnames';
|
||||
import type { SvelteHTMLElements } from 'svelte/elements';
|
||||
import Eyebrow from './eyebrow.svelte';
|
||||
|
||||
type $$Props = SvelteHTMLElements['span'];
|
||||
|
||||
const { class: classes, ...props } = $$restProps;
|
||||
</script>
|
||||
|
||||
<Eyebrow class="text-white">
|
||||
<span
|
||||
class={classNames(
|
||||
'badge font-aeonik-fono self-start rounded-[0.375rem] py-[0.375rem] px-3 text-xs uppercase text-white backdrop-blur-2xl',
|
||||
'badge self-start rounded-[0.375rem] px-3 py-[0.375rem] backdrop-blur-2xl',
|
||||
classes
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<slot />
|
||||
</span>
|
||||
</span></Eyebrow
|
||||
>
|
||||
|
||||
<style>
|
||||
:root,
|
||||
59
src/lib/components/ui/button.svelte
Normal file
@@ -0,0 +1,59 @@
|
||||
<script lang="ts" module>
|
||||
// TODO: replace _button.scss with Tailwind classes for long-term maintainability
|
||||
const button = cva(['web-button'], {
|
||||
variants: {
|
||||
variant: {
|
||||
primary: [''],
|
||||
secondary: ['is-secondary'],
|
||||
text: ['is-text'],
|
||||
transparent: ['is-transparent'],
|
||||
small: ['is-small'],
|
||||
icon: ['is-icon', 'is-text']
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
export type Variant = VariantProps<typeof button>['variant'];
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { classNames } from '$lib/utils/classnames';
|
||||
import { type VariantProps, cva } from 'cva';
|
||||
import type { Snippet } from 'svelte';
|
||||
import type { Action } from 'svelte/action';
|
||||
import type { HTMLAnchorAttributes, HTMLButtonAttributes } from 'svelte/elements';
|
||||
import { trackEvent, type TrackEventArgs } from '$lib/actions/analytics';
|
||||
|
||||
type ButtonOrAnchorProps =
|
||||
| (HTMLButtonAttributes & { href?: undefined })
|
||||
| (HTMLAnchorAttributes & { href: string });
|
||||
|
||||
type Props = {
|
||||
action?: Action;
|
||||
children: Snippet;
|
||||
events?: TrackEventArgs;
|
||||
} & VariantProps<typeof button> &
|
||||
ButtonOrAnchorProps;
|
||||
|
||||
const {
|
||||
href,
|
||||
variant,
|
||||
action = () => {},
|
||||
children,
|
||||
class: classes,
|
||||
events,
|
||||
...rest
|
||||
}: Props = $props();
|
||||
|
||||
const buttonClasses = classNames(button({ variant }), classes);
|
||||
</script>
|
||||
|
||||
{#if href}
|
||||
<a use:action {href} class={buttonClasses} {...rest as HTMLAnchorAttributes}>
|
||||
{@render children()}
|
||||
</a>
|
||||
{:else}
|
||||
<button use:action class={buttonClasses} {...rest as HTMLButtonAttributes}>
|
||||
{@render children()}
|
||||
</button>
|
||||
{/if}
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { classNames } from '$lib/utils/classnames';
|
||||
import type { HTMLButtonAttributes, HTMLAnchorAttributes } from 'svelte/elements';
|
||||
import { cva, type VariantProps } from 'cva';
|
||||
import type { HTMLAnchorAttributes, HTMLButtonAttributes } from 'svelte/elements';
|
||||
|
||||
const card = cva(
|
||||
[
|
||||
41
src/lib/components/ui/eyebrow.svelte
Normal file
@@ -0,0 +1,41 @@
|
||||
<script lang="ts">
|
||||
import { classNames } from '$lib/utils/classnames';
|
||||
|
||||
// html text elements
|
||||
type ElementType =
|
||||
| 'p'
|
||||
| 'h1'
|
||||
| 'h2'
|
||||
| 'h3'
|
||||
| 'h4'
|
||||
| 'h5'
|
||||
| 'h6'
|
||||
| 'span'
|
||||
| 'div'
|
||||
| 'strong'
|
||||
| 'em'
|
||||
| 'small'
|
||||
| 'mark'
|
||||
| 'abbr'
|
||||
| 'blockquote'
|
||||
| 'cite'
|
||||
| 'code'
|
||||
| 'kbd'
|
||||
| 'var'
|
||||
| 'samp'
|
||||
| 'sub'
|
||||
| 'sup';
|
||||
|
||||
let className: string = '';
|
||||
export let as: ElementType = 'span';
|
||||
|
||||
export { className as class };
|
||||
</script>
|
||||
|
||||
<svelte:element
|
||||
this={as}
|
||||
class={classNames(
|
||||
'text-micro tracking-loose text-primary font-aeonik-fono uppercase',
|
||||
className
|
||||
)}><slot /></svelte:element
|
||||
>
|
||||
22
src/lib/components/ui/gradient-text.svelte
Normal file
@@ -0,0 +1,22 @@
|
||||
<script lang="ts">
|
||||
import { classNames } from '$lib/utils/classnames';
|
||||
import type { Snippet } from 'svelte';
|
||||
import type { SvelteHTMLElements } from 'svelte/elements';
|
||||
|
||||
type Props = {
|
||||
class?: string;
|
||||
children?: Snippet;
|
||||
} & SvelteHTMLElements['span'];
|
||||
|
||||
let { class: className = '', children, ...rest }: Props = $props();
|
||||
</script>
|
||||
|
||||
<span
|
||||
class={classNames(
|
||||
'block bg-[linear-gradient(6deg,_#f8a1ba,_#fff_35%)] bg-clip-text text-transparent',
|
||||
className
|
||||
)}
|
||||
{...rest}
|
||||
>
|
||||
{@render children?.()}
|
||||
</span>
|
||||
24
src/lib/components/ui/icon/icon.svelte
Normal file
@@ -0,0 +1,24 @@
|
||||
<script lang="ts">
|
||||
import { classNames } from '$lib/utils/classnames';
|
||||
import type { SvelteHTMLElements } from 'svelte/elements';
|
||||
import type { IconType } from './types';
|
||||
|
||||
type Props = SvelteHTMLElements['svg'] & {
|
||||
class?: string;
|
||||
name?: IconType;
|
||||
};
|
||||
|
||||
const {
|
||||
xmlns = 'http://www.w3.org/2000/svg',
|
||||
viewBox = '0 0 24 24',
|
||||
height = 20,
|
||||
width = 20,
|
||||
class: className = '',
|
||||
name = 'arrow-right',
|
||||
...rest
|
||||
}: Props = $props();
|
||||
</script>
|
||||
|
||||
<svg class={className} {xmlns} {height} {width} {viewBox} {...rest}>
|
||||
<use xlink:href="#{name}" />
|
||||
</svg>
|
||||
2
src/lib/components/ui/icon/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export { default } from './icon.svelte';
|
||||
export { type IconType } from './types.js';
|
||||
1
src/lib/components/ui/icon/sprite/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default as Sprite } from './sprite.svelte';
|
||||
475
src/lib/components/ui/icon/sprite/sprite.svelte
Normal file
|
After Width: | Height: | Size: 88 KiB |
60
src/lib/components/ui/icon/types.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
export type IconType =
|
||||
| 'apple'
|
||||
| 'appwrite'
|
||||
| 'arrow-down'
|
||||
| 'arrow-ext-link'
|
||||
| 'arrow-left'
|
||||
| 'arrow-right'
|
||||
| 'arrow-up'
|
||||
| 'bluesky'
|
||||
| 'calendar'
|
||||
| 'check'
|
||||
| 'chevron-down'
|
||||
| 'chevron-left'
|
||||
| 'chevron-right'
|
||||
| 'chevron-up'
|
||||
| 'close'
|
||||
| 'command'
|
||||
| 'copy'
|
||||
| 'daily-dev'
|
||||
| 'dark'
|
||||
| 'discord'
|
||||
| 'divider-vertical'
|
||||
| 'download'
|
||||
| 'edge'
|
||||
| 'ext-link'
|
||||
| 'firebase'
|
||||
| 'github'
|
||||
| 'google'
|
||||
| 'hamburger-menu'
|
||||
| 'instagram'
|
||||
| 'light'
|
||||
| 'linkedin'
|
||||
| 'location'
|
||||
| 'logout-left'
|
||||
| 'logout-right'
|
||||
| 'mailgun'
|
||||
| 'mcp'
|
||||
| 'message'
|
||||
| 'microsoft'
|
||||
| 'minus'
|
||||
| 'nuxt'
|
||||
| 'platform'
|
||||
| 'play'
|
||||
| 'plus'
|
||||
| 'pop-locations'
|
||||
| 'product-hunt'
|
||||
| 'refine'
|
||||
| 'regions'
|
||||
| 'rest'
|
||||
| 'search'
|
||||
| 'sendgrid'
|
||||
| 'star'
|
||||
| 'system'
|
||||
| 'textmagic'
|
||||
| 'tiktok'
|
||||
| 'twitter'
|
||||
| 'vue'
|
||||
| 'x'
|
||||
| 'ycombinator'
|
||||
| 'youtube';
|
||||
4
src/lib/components/ui/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export { default as Button, type Variant } from './button.svelte';
|
||||
export { default as InlineTag } from './inline-tag.svelte';
|
||||
export { default as Icon, type IconType } from './icon';
|
||||
export { default as Select } from './select.svelte';
|
||||
5
src/lib/components/ui/inline-tag.svelte
Normal file
@@ -0,0 +1,5 @@
|
||||
<div
|
||||
class="web-inline-tag tracking-none text-sub-body text-greyscale-900 dark:text-greyscale-100 -mr-0.5 rounded-[.25rem] bg-black/8 px-1 py-0.25 dark:bg-white/[0.12]"
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
@@ -1,36 +1,41 @@
|
||||
<!-- @migration-task Error while migrating Svelte code: migrating this component would require adding a `$props` rune but there's already a variable named props.
|
||||
Rename the variable and try again or migrate by hand. -->
|
||||
<script lang="ts">
|
||||
import { classNames } from '$lib/utils/classnames';
|
||||
import type { Snippet } from 'svelte';
|
||||
import type { HTMLInputAttributes } from 'svelte/elements';
|
||||
|
||||
type $$Props = HTMLInputAttributes & {
|
||||
interface Props extends HTMLInputAttributes {
|
||||
label?: string;
|
||||
};
|
||||
icon?: Snippet;
|
||||
}
|
||||
|
||||
export let label: $$Props['label'] = '';
|
||||
export let type: $$Props['type'] = 'text';
|
||||
export let value: $$Props['value'] = '';
|
||||
const { class: classes, name, ...props } = $$restProps;
|
||||
let {
|
||||
label = '',
|
||||
type = 'text',
|
||||
value = $bindable(''),
|
||||
icon,
|
||||
class: classes,
|
||||
name,
|
||||
...rest
|
||||
}: Props = $props();
|
||||
</script>
|
||||
|
||||
{#if $$slots.icon}
|
||||
{#if icon}
|
||||
<label
|
||||
class={classNames(
|
||||
'focus:border-greyscale-100 bg-greyscale-800 border-greyscale-700 flex items-center gap-1 rounded-lg border py-2 px-3 text-sm font-light transition-colors focus-within:border-white active:shadow-sm active:shadow-black/30',
|
||||
'focus:border-greyscale-100 bg-greyscale-800 border-greyscale-700 flex items-center gap-1 rounded-lg border px-3 py-2 text-sm font-light transition-colors focus-within:border-white active:shadow-sm active:shadow-black/30',
|
||||
classes
|
||||
)}
|
||||
>
|
||||
<slot name="icon" />
|
||||
{@render icon?.()}
|
||||
{#key type}
|
||||
<input
|
||||
{name}
|
||||
{...{ type }}
|
||||
bind:value
|
||||
on:input
|
||||
on:change
|
||||
on:focus
|
||||
on:blur
|
||||
class="w-full border-0 ring-0 outline-none"
|
||||
{...props}
|
||||
{...rest}
|
||||
/>
|
||||
{/key}
|
||||
</label>
|
||||
@@ -45,15 +50,11 @@
|
||||
{name}
|
||||
{...{ type }}
|
||||
bind:value
|
||||
on:input
|
||||
on:change
|
||||
on:focus
|
||||
on:blur
|
||||
class={classNames(
|
||||
'focus:border-greyscale-100 bg-greyscale-800 border-greyscale-700 mt-2 flex w-full items-center gap-1 rounded-lg border py-2 px-3 text-sm font-light transition-colors focus-within:border-white active:shadow-sm active:shadow-black/30',
|
||||
'focus:border-greyscale-100 bg-greyscale-800 border-greyscale-700 mt-2 flex w-full items-center gap-1 rounded-lg border px-3 py-2 text-sm font-light transition-colors focus-within:border-white active:shadow-sm active:shadow-black/30',
|
||||
classes
|
||||
)}
|
||||
{...props}
|
||||
{...rest}
|
||||
/>
|
||||
{/key}
|
||||
{/if}
|
||||
46
src/lib/components/ui/select.svelte
Normal file
@@ -0,0 +1,46 @@
|
||||
<script lang="ts">
|
||||
import { Select, type SelectProps } from 'melt/builders';
|
||||
|
||||
type Props = {
|
||||
options: Array<{
|
||||
label: string;
|
||||
value: string;
|
||||
icon: string;
|
||||
}>;
|
||||
defaultValue?: string;
|
||||
value?: string;
|
||||
} & SelectProps<string>;
|
||||
|
||||
const {
|
||||
options,
|
||||
value = $bindable(),
|
||||
onValueChange,
|
||||
defaultValue = 'Select a value',
|
||||
...rest
|
||||
}: Props = $props();
|
||||
|
||||
const select = new Select<Props['options'][number]['value']>({
|
||||
value,
|
||||
sameWidth: true,
|
||||
forceVisible: false,
|
||||
...rest
|
||||
});
|
||||
</script>
|
||||
|
||||
<button
|
||||
class="border-gradient text-primary relative flex h-8 min-w-[145px] cursor-pointer items-center text-sm leading-[1] select-none"
|
||||
{...select.trigger}
|
||||
>
|
||||
{select.value ?? defaultValue}
|
||||
</button>
|
||||
|
||||
<div
|
||||
class="text-primary relative box-border flex max-w-xs flex-col gap-2 overflow-y-auto rounded-xl p-1 text-sm leading-[1.2] select-none"
|
||||
{...select.content}
|
||||
>
|
||||
{#each options as option}
|
||||
<div {...select.getOption(option.value)}>
|
||||
{option.label}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
35
src/lib/components/ui/switch.svelte
Normal file
@@ -0,0 +1,35 @@
|
||||
<script lang="ts">
|
||||
import { classNames } from '$lib/utils/classnames';
|
||||
import { createSwitch, melt } from '@melt-ui/svelte';
|
||||
|
||||
export let checked = false;
|
||||
|
||||
const {
|
||||
elements: { root },
|
||||
states: { checked: meltChecked }
|
||||
} = createSwitch({
|
||||
onCheckedChange({ next }) {
|
||||
checked = next;
|
||||
return next;
|
||||
}
|
||||
});
|
||||
|
||||
$: meltChecked.set(checked);
|
||||
</script>
|
||||
|
||||
<div class="flex items-center">
|
||||
<button
|
||||
use:melt={$root}
|
||||
class={classNames(
|
||||
'bg-smooth group relative h-6 w-9 cursor-default rounded-full transition duration-150',
|
||||
'data-[state="checked"]:bg-accent'
|
||||
)}
|
||||
>
|
||||
<span
|
||||
class={classNames(
|
||||
'absolute top-1/2 block size-5 translate-x-0.5 -translate-y-1/2 rounded-full bg-white transition duration-150',
|
||||
'group-[data-state="checked"]:translate-x-3.5'
|
||||
)}
|
||||
></span>
|
||||
</button>
|
||||
</div>
|
||||
@@ -1,8 +1,40 @@
|
||||
export const GITHUB_STARS = '47K';
|
||||
export const GITHUB_REPO_LINK = 'https://github.com/appwrite/appwrite';
|
||||
type SocialStats = {
|
||||
[K in 'GITHUB' | 'DISCORD' | 'TWITTER' | 'YOUTUBE']: {
|
||||
STAT: string;
|
||||
LINK: string;
|
||||
EXTRA?: Record<string, string> | undefined;
|
||||
};
|
||||
};
|
||||
|
||||
export const SOCIAL_STATS: SocialStats = {
|
||||
GITHUB: {
|
||||
STAT: '48K',
|
||||
LINK: 'https://github.com/appwrite/appwrite',
|
||||
EXTRA: {
|
||||
COMMITS: '24K+',
|
||||
PULL_REQUESTS: '4.5K+',
|
||||
ISSUES: '3K+',
|
||||
OPEN_ISSUES: '500+',
|
||||
CLOSED_ISSUES: '3.3K+',
|
||||
FORKS: '4.3K+',
|
||||
CONTRIBUTORS: '800+'
|
||||
}
|
||||
},
|
||||
DISCORD: {
|
||||
STAT: '22K+',
|
||||
LINK: '/discord'
|
||||
},
|
||||
TWITTER: {
|
||||
STAT: '128K+',
|
||||
LINK: 'https://twitter.com/intent/follow?screen_name=appwrite'
|
||||
},
|
||||
YOUTUBE: {
|
||||
STAT: '11K+',
|
||||
LINK: 'https://www.youtube.com/c/appwrite?sub_confirmation=1'
|
||||
}
|
||||
};
|
||||
|
||||
export const BANNER_KEY: Banners = 'discord-banner-01'; // Change key to force banner to show again
|
||||
export const SENTRY_DSN =
|
||||
'https://27d41dc8bb67b596f137924ab8599e59@o1063647.ingest.us.sentry.io/4507497727000576';
|
||||
|
||||
export const BLOG_POSTS_PER_PAGE = 12;
|
||||
|
||||
|
||||