Merge branch 'eldadfux-patch-network' into update-for-network
@@ -2,6 +2,7 @@ PUBLIC_APPWRITE_COL_MESSAGES_ID=
|
|||||||
PUBLIC_APPWRITE_COL_THREADS_ID=
|
PUBLIC_APPWRITE_COL_THREADS_ID=
|
||||||
PUBLIC_APPWRITE_DB_MAIN_ID=
|
PUBLIC_APPWRITE_DB_MAIN_ID=
|
||||||
PUBLIC_APPWRITE_FN_TLDR_ID=
|
PUBLIC_APPWRITE_FN_TLDR_ID=
|
||||||
|
PUBLIC_APPWRITE_DASHBOARD=
|
||||||
PUBLIC_APPWRITE_ENDPOINT=
|
PUBLIC_APPWRITE_ENDPOINT=
|
||||||
PUBLIC_APPWRITE_PROJECT_ID=
|
PUBLIC_APPWRITE_PROJECT_ID=
|
||||||
PUBLIC_APPWRITE_PROJECT_INIT_ID=
|
PUBLIC_APPWRITE_PROJECT_INIT_ID=
|
||||||
|
|||||||
@@ -1,13 +0,0 @@
|
|||||||
.DS_Store
|
|
||||||
node_modules
|
|
||||||
/build
|
|
||||||
/.svelte-kit
|
|
||||||
/package
|
|
||||||
.env
|
|
||||||
.env.*
|
|
||||||
!.env.example
|
|
||||||
|
|
||||||
# Ignore files for PNPM, NPM and YARN
|
|
||||||
pnpm-lock.yaml
|
|
||||||
package-lock.json
|
|
||||||
yarn.lock
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
root: true,
|
|
||||||
extends: [
|
|
||||||
'eslint:recommended',
|
|
||||||
'plugin:@typescript-eslint/recommended',
|
|
||||||
'plugin:svelte/recommended',
|
|
||||||
'prettier'
|
|
||||||
],
|
|
||||||
parser: '@typescript-eslint/parser',
|
|
||||||
plugins: ['@typescript-eslint'],
|
|
||||||
parserOptions: {
|
|
||||||
sourceType: 'module',
|
|
||||||
ecmaVersion: 2020,
|
|
||||||
extraFileExtensions: ['.svelte']
|
|
||||||
},
|
|
||||||
env: {
|
|
||||||
browser: true,
|
|
||||||
es2017: true,
|
|
||||||
node: true
|
|
||||||
},
|
|
||||||
overrides: [
|
|
||||||
{
|
|
||||||
files: ['*.svelte'],
|
|
||||||
parser: 'svelte-eslint-parser',
|
|
||||||
parserOptions: {
|
|
||||||
parser: '@typescript-eslint/parser'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
};
|
|
||||||
2
.github/workflows/production.yml
vendored
@@ -72,7 +72,7 @@ jobs:
|
|||||||
echo "_APP_VERSION=${{ env.TAG }}" >> .env
|
echo "_APP_VERSION=${{ env.TAG }}" >> .env
|
||||||
echo "_APP_DOMAIN=${{ secrets.PRD_APP_DOMAIN }}" >> .env
|
echo "_APP_DOMAIN=${{ secrets.PRD_APP_DOMAIN }}" >> .env
|
||||||
echo "_APP_SYSTEM_SECURITY_EMAIL_ADDRESS=${{ secrets.APP_SYSTEM_SECURITY_EMAIL_ADDRESS }}" >> .env
|
echo "_APP_SYSTEM_SECURITY_EMAIL_ADDRESS=${{ secrets.APP_SYSTEM_SECURITY_EMAIL_ADDRESS }}" >> .env
|
||||||
echo "SEMATEXT_TOKEN=${{ secrets.SEMATEXT_TOKEN }}" >> .env
|
echo "_APP_BETTER_STACK_INCIDENT_URL=${{ secrets.BETTER_STACK_INCIDENT_URL }}" >> .env
|
||||||
|
|
||||||
echo ${{ secrets.GH_REGISTRY_TOKEN }} | docker login ghcr.io --username ${{ env.REGISTRY_USERNAME }} --password-stdin
|
echo ${{ secrets.GH_REGISTRY_TOKEN }} | docker login ghcr.io --username ${{ env.REGISTRY_USERNAME }} --password-stdin
|
||||||
docker-compose -f ${{ env.STACK_FILE }} config
|
docker-compose -f ${{ env.STACK_FILE }} config
|
||||||
|
|||||||
2
.github/workflows/staging.yml
vendored
@@ -74,7 +74,7 @@ jobs:
|
|||||||
echo "_APP_VERSION=${{ env.TAG }}" >> .env
|
echo "_APP_VERSION=${{ env.TAG }}" >> .env
|
||||||
echo "_APP_DOMAIN=${{ secrets.STG_APP_DOMAIN }}" >> .env
|
echo "_APP_DOMAIN=${{ secrets.STG_APP_DOMAIN }}" >> .env
|
||||||
echo "_APP_SYSTEM_SECURITY_EMAIL_ADDRESS=${{ secrets.APP_SYSTEM_SECURITY_EMAIL_ADDRESS }}" >> .env
|
echo "_APP_SYSTEM_SECURITY_EMAIL_ADDRESS=${{ secrets.APP_SYSTEM_SECURITY_EMAIL_ADDRESS }}" >> .env
|
||||||
echo "SEMATEXT_TOKEN=${{ secrets.SEMATEXT_TOKEN }}" >> .env
|
echo "_APP_BETTER_STACK_INCIDENT_URL=${{ secrets.BETTER_STACK_INCIDENT_URL }}" >> .env
|
||||||
|
|
||||||
echo ${{ secrets.GH_REGISTRY_TOKEN }} | docker login ghcr.io --username ${{ env.REGISTRY_USERNAME }} --password-stdin
|
echo ${{ secrets.GH_REGISTRY_TOKEN }} | docker login ghcr.io --username ${{ env.REGISTRY_USERNAME }} --password-stdin
|
||||||
docker-compose -f ${{ env.STACK_FILE }} config
|
docker-compose -f ${{ env.STACK_FILE }} config
|
||||||
|
|||||||
3
.gitignore
vendored
@@ -18,4 +18,5 @@ package-lock.json
|
|||||||
.history
|
.history
|
||||||
terraform/**/.t*
|
terraform/**/.t*
|
||||||
terraform/**/.env
|
terraform/**/.env
|
||||||
terraform/**/**/*.tfstate*
|
terraform/**/**/*.tfstate*
|
||||||
|
/.cache
|
||||||
|
|||||||
@@ -1,9 +0,0 @@
|
|||||||
{
|
|
||||||
"useTabs": false,
|
|
||||||
"tabWidth": 4,
|
|
||||||
"singleQuote": true,
|
|
||||||
"trailingComma": "none",
|
|
||||||
"printWidth": 100,
|
|
||||||
"plugins": ["prettier-plugin-svelte", "prettier-plugin-tailwindcss"],
|
|
||||||
"overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }]
|
|
||||||
}
|
|
||||||
@@ -296,7 +296,7 @@ Configure FCM for push notification to Android and Apple devices.
|
|||||||
|
|
||||||
#### Accordions
|
#### Accordions
|
||||||
|
|
||||||
Use accordions to reduce page size and collapse information that's not important when a reader is skilling the page.
|
Use accordions to reduce page size and collapse information that's not important when a reader is scrolling the page.
|
||||||
|
|
||||||
```
|
```
|
||||||
{% accordion %}
|
{% accordion %}
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ The Appwrite Website has been built with the following frameworks:
|
|||||||
|
|
||||||
## Development
|
## 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:
|
To get the repo up and running in your local environment, use the following command:
|
||||||
|
|
||||||
|
|||||||
@@ -119,23 +119,23 @@ services:
|
|||||||
- TIME_BETWEEN_RUNS=3600
|
- TIME_BETWEEN_RUNS=3600
|
||||||
- UNUSED_TIME=6h
|
- UNUSED_TIME=6h
|
||||||
|
|
||||||
sematext-agent:
|
resource-monitor:
|
||||||
image: sematext/agent:latest
|
image: ghcr.io/appwrite/monitoring:0.1.0
|
||||||
environment:
|
entrypoint: monitoring
|
||||||
REGION: EU
|
command:
|
||||||
INFRA_TOKEN: $SEMATEXT_TOKEN
|
- '--url=${_APP_BETTER_STACK_INCIDENT_URL}'
|
||||||
deploy:
|
- '--interval=60'
|
||||||
mode: global
|
- '--cpu-limit=85'
|
||||||
restart_policy:
|
- '--memory-limit=80'
|
||||||
condition: any
|
- '--disk-limit=85'
|
||||||
|
hostname: '{{.Node.Hostname}}'
|
||||||
|
<<: *x-logging
|
||||||
volumes:
|
volumes:
|
||||||
- /:/hostfs:ro
|
- /mnt:/mnt:ro
|
||||||
- /etc/passwd:/etc/passwd:ro
|
deploy:
|
||||||
- /etc/group:/etc/group:ro
|
<<: *x-update-config
|
||||||
- /sys:/host/sys:ro
|
endpoint_mode: dnsrr
|
||||||
- /dev:/hostfs/dev:ro
|
mode: global
|
||||||
- /var/run:/var/run
|
|
||||||
- /sys/kernel/debug:/sys/kernel/debug
|
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
cloud:
|
cloud:
|
||||||
|
|||||||
@@ -114,23 +114,23 @@ services:
|
|||||||
volumes:
|
volumes:
|
||||||
- /var/run/docker.sock:/var/run/docker.sock
|
- /var/run/docker.sock:/var/run/docker.sock
|
||||||
|
|
||||||
sematext-agent:
|
resource-monitor:
|
||||||
image: sematext/agent:latest
|
image: ghcr.io/appwrite/monitoring:0.1.0
|
||||||
environment:
|
entrypoint: monitoring
|
||||||
REGION: EU
|
command:
|
||||||
INFRA_TOKEN: $SEMATEXT_TOKEN
|
- '--url=${_APP_BETTER_STACK_INCIDENT_URL}'
|
||||||
deploy:
|
- '--interval=60'
|
||||||
mode: global
|
- '--cpu-limit=85'
|
||||||
restart_policy:
|
- '--memory-limit=80'
|
||||||
condition: any
|
- '--disk-limit=85'
|
||||||
|
hostname: '{{.Node.Hostname}}'
|
||||||
|
<<: *x-logging
|
||||||
volumes:
|
volumes:
|
||||||
- /:/hostfs:ro
|
- /mnt:/mnt:ro
|
||||||
- /etc/passwd:/etc/passwd:ro
|
deploy:
|
||||||
- /etc/group:/etc/group:ro
|
<<: *x-update-config
|
||||||
- /sys:/host/sys:ro
|
endpoint_mode: dnsrr
|
||||||
- /dev:/hostfs/dev:ro
|
mode: global
|
||||||
- /var/run:/var/run
|
|
||||||
- /sys/kernel/debug:/sys/kernel/debug
|
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
cloud:
|
cloud:
|
||||||
|
|||||||
33
eslint.config.js
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import prettier from 'eslint-config-prettier';
|
||||||
|
import js from '@eslint/js';
|
||||||
|
import svelte from 'eslint-plugin-svelte';
|
||||||
|
import globals from 'globals';
|
||||||
|
import ts from 'typescript-eslint';
|
||||||
|
|
||||||
|
export default ts.config(
|
||||||
|
js.configs.recommended,
|
||||||
|
...ts.configs.recommended,
|
||||||
|
...svelte.configs['flat/recommended'],
|
||||||
|
prettier,
|
||||||
|
...svelte.configs['flat/prettier'],
|
||||||
|
{
|
||||||
|
languageOptions: {
|
||||||
|
globals: {
|
||||||
|
...globals.browser,
|
||||||
|
...globals.node
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
files: ['**/*.svelte'],
|
||||||
|
|
||||||
|
languageOptions: {
|
||||||
|
parserOptions: {
|
||||||
|
parser: ts.parser
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ignores: ['build/', '.svelte-kit/', 'dist/']
|
||||||
|
}
|
||||||
|
);
|
||||||
@@ -7,6 +7,10 @@
|
|||||||
"type": "esm",
|
"type": "esm",
|
||||||
"property": "default",
|
"property": "default",
|
||||||
"watch": true
|
"watch": true
|
||||||
|
},
|
||||||
|
"partials": {
|
||||||
|
"auth-security.md": "./src/partials/auth-security.md",
|
||||||
|
"prohibited-activities.md": "./src/partials/prohibited-activities.md"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
67
package.json
@@ -19,47 +19,48 @@
|
|||||||
"test": "npm run test:integration && npm run test:unit",
|
"test": "npm run test:integration && npm run test:unit",
|
||||||
"test:integration": "playwright test",
|
"test:integration": "playwright test",
|
||||||
"test:unit": "vitest",
|
"test:unit": "vitest",
|
||||||
"optimize": "node ./scripts/optimize-assets.js"
|
"optimize": "node ./scripts/optimize-assets.js",
|
||||||
|
"optimize:all": "node ./scripts/optimize-all.js"
|
||||||
},
|
},
|
||||||
"packageManager": "pnpm@9.4.0+sha512.f549b8a52c9d2b8536762f99c0722205efc5af913e77835dbccc3b0b0b2ca9e7dc8022b78062c17291c48e88749c70ce88eb5a74f1fa8c4bf5e18bb46c8bd83a",
|
"packageManager": "pnpm@9.4.0+sha512.f549b8a52c9d2b8536762f99c0722205efc5af913e77835dbccc3b0b0b2ca9e7dc8022b78062c17291c48e88749c70ce88eb5a74f1fa8c4bf5e18bb46c8bd83a",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@sentry/sveltekit": "^8.12.0",
|
"@sentry/sveltekit": "^8.38.0",
|
||||||
"h3": "^1.12.0",
|
"h3": "^1.13.0",
|
||||||
"sharp": "^0.33.4"
|
"sharp": "^0.33.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@appwrite.io/console": "^0.6.2",
|
"@appwrite.io/console": "^0.6.4",
|
||||||
"@appwrite.io/pink": "~0.26.0",
|
"@appwrite.io/pink": "~0.26.0",
|
||||||
"@appwrite.io/pink-icons": "~0.26.0",
|
"@appwrite.io/pink-icons": "~0.26.0",
|
||||||
"@appwrite.io/repo": "github:appwrite/appwrite#feat-multi-region-docs",
|
"@appwrite.io/repo": "github:appwrite/appwrite#feat-multi-region-docs",
|
||||||
"@internationalized/date": "3.5.0",
|
"@internationalized/date": "3.5.0",
|
||||||
"@melt-ui/pp": "^0.3.2",
|
"@melt-ui/pp": "^0.3.2",
|
||||||
"@melt-ui/svelte": "^0.74.4",
|
"@melt-ui/svelte": "^0.86.0",
|
||||||
"@playwright/test": "^1.44.1",
|
"@playwright/test": "^1.49.0",
|
||||||
"@sveltejs/adapter-node": "^4.0.1",
|
"@sveltejs/adapter-node": "^4.0.1",
|
||||||
"@sveltejs/enhanced-img": "^0.1.9",
|
"@sveltejs/enhanced-img": "^0.1.9",
|
||||||
"@sveltejs/kit": "^2.5.17",
|
"@sveltejs/kit": "^2.8.1",
|
||||||
"@sveltejs/vite-plugin-svelte": "^3.1.1",
|
"@sveltejs/vite-plugin-svelte": "^3.1.2",
|
||||||
"@tailwindcss/postcss": "4.0.0-alpha.17",
|
"@tailwindcss/postcss": "4.0.0-alpha.17",
|
||||||
"@types/compression": "^1.7.5",
|
"@types/compression": "^1.7.5",
|
||||||
"@types/glob": "^8.1.0",
|
"@types/glob": "^8.1.0",
|
||||||
"@types/markdown-it": "^13.0.8",
|
"@types/markdown-it": "^13.0.9",
|
||||||
"@types/morgan": "^1.9.9",
|
"@types/morgan": "^1.9.9",
|
||||||
"@typescript-eslint/eslint-plugin": "^7.13.1",
|
|
||||||
"@typescript-eslint/parser": "^7.13.1",
|
|
||||||
"analytics": "^0.8.14",
|
"analytics": "^0.8.14",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"cva": "npm:class-variance-authority@^0.7.0",
|
"cva": "npm:class-variance-authority@^0.7.0",
|
||||||
"date-fns": "^3.6.0",
|
"date-fns": "^3.6.0",
|
||||||
"dequal": "^2.0.3",
|
"dequal": "^2.0.3",
|
||||||
"embla-carousel": "^8.1.5",
|
"embla-carousel": "^8.4.0",
|
||||||
"embla-carousel-svelte": "^8.1.5",
|
"embla-carousel-auto-scroll": "^8.5.1",
|
||||||
|
"embla-carousel-svelte": "^8.4.0",
|
||||||
"embla-carousel-wheel-gestures": "^8.0.1",
|
"embla-carousel-wheel-gestures": "^8.0.1",
|
||||||
"eslint": "^8.57.0",
|
"eslint": "^9.15.0",
|
||||||
"eslint-config-prettier": "^8.10.0",
|
"eslint-config-prettier": "^9.1.0",
|
||||||
"eslint-plugin-svelte": "^2.40.0",
|
"eslint-plugin-svelte": "^2.46.0",
|
||||||
"fuse.js": "^7.0.0",
|
"fuse.js": "^7.0.0",
|
||||||
"highlight.js": "^11.9.0",
|
"globals": "^15.12.0",
|
||||||
|
"highlight.js": "^11.10.0",
|
||||||
"markdown-it": "^14.1.0",
|
"markdown-it": "^14.1.0",
|
||||||
"meilisearch": "^0.37.0",
|
"meilisearch": "^0.37.0",
|
||||||
"motion": "^10.18.0",
|
"motion": "^10.18.0",
|
||||||
@@ -67,24 +68,26 @@
|
|||||||
"openapi-types": "^12.1.3",
|
"openapi-types": "^12.1.3",
|
||||||
"oslllo-svg-fixer": "^3.0.0",
|
"oslllo-svg-fixer": "^3.0.0",
|
||||||
"plausible-tracker": "^0.3.9",
|
"plausible-tracker": "^0.3.9",
|
||||||
"postcss": "^8.4.39",
|
"postcss": "^8.4.49",
|
||||||
"prettier": "^3.3.3",
|
"prettier": "^3.3.3",
|
||||||
"prettier-plugin-svelte": "^3.2.5",
|
"prettier-plugin-svelte": "^3.2.8",
|
||||||
"prettier-plugin-tailwindcss": "^0.6.6",
|
"prettier-plugin-tailwindcss": "^0.6.8",
|
||||||
"remeda": "^2.10.0",
|
"remeda": "^2.17.3",
|
||||||
"sass": "^1.77.6",
|
"sass": "^1.81.0",
|
||||||
"svelte": "^4.2.18",
|
"svelte": "^4.2.19",
|
||||||
"svelte-check": "^3.8.1",
|
"svelte-check": "^3.8.6",
|
||||||
"svelte-markdoc-preprocess": "^2.0.0",
|
"svelte-markdoc-preprocess": "^2.1.0",
|
||||||
"svelte-markdown": "^0.4.1",
|
"svelte-markdown": "^0.4.1",
|
||||||
"svgtofont": "^4.2.1",
|
"svgtofont": "^4.2.3",
|
||||||
"tailwind-merge": "^2.5.2",
|
"tailwind-merge": "^2.5.4",
|
||||||
"tailwindcss": "4.0.0-alpha.17",
|
"tailwindcss": "4.0.0-alpha.17",
|
||||||
"tslib": "^2.6.3",
|
"tslib": "^2.8.1",
|
||||||
"typescript": "^5.5.2",
|
"typescript": "^5.6.3",
|
||||||
"vite": "^5.3.1",
|
"typescript-eslint": "^8.15.0",
|
||||||
"vite-plugin-dynamic-import": "^1.5.0",
|
"vite": "^5.4.11",
|
||||||
|
"vite-plugin-dynamic-import": "^1.6.0",
|
||||||
"vite-plugin-image-optimizer": "^1.1.8",
|
"vite-plugin-image-optimizer": "^1.1.8",
|
||||||
|
"vite-plugin-manifest-sri": "^0.2.0",
|
||||||
"vitest": "^1.6.0"
|
"vitest": "^1.6.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
1241
pnpm-lock.yaml
generated
15
prettier.config.js
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
/** @type {import("prettier").Config} */
|
||||||
|
export default {
|
||||||
|
useTabs: false,
|
||||||
|
tabWidth: 4,
|
||||||
|
singleQuote: true,
|
||||||
|
trailingComma: 'none',
|
||||||
|
printWidth: 100,
|
||||||
|
plugins: ['prettier-plugin-svelte', 'prettier-plugin-tailwindcss'],
|
||||||
|
overrides: [
|
||||||
|
{
|
||||||
|
files: '*.svelte',
|
||||||
|
options: { parser: 'svelte' }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
153
scripts/optimize-all.js
Normal file
@@ -0,0 +1,153 @@
|
|||||||
|
import { readdirSync, statSync } from 'fs';
|
||||||
|
import { join, relative, resolve } from 'path';
|
||||||
|
import sharp from 'sharp';
|
||||||
|
import { fileURLToPath } from 'url';
|
||||||
|
|
||||||
|
const __dirname = fileURLToPath(new URL('.', import.meta.url));
|
||||||
|
const project_root = resolve(__dirname, '..');
|
||||||
|
|
||||||
|
// Directories to search in
|
||||||
|
const search_dirs = ['src', 'static', 'routes', 'lib'];
|
||||||
|
|
||||||
|
// Directories to skip
|
||||||
|
const excluded_dirs = ['node_modules', '.svelte-kit', 'build', '.git', 'assets/'];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @type {{
|
||||||
|
* jpeg: sharp.JpegOptions,
|
||||||
|
* webp: sharp.WebpOptions,
|
||||||
|
* png: sharp.PngOptions,
|
||||||
|
* gif: sharp.GifOptions,
|
||||||
|
* avif: sharp.AvifOptions
|
||||||
|
* }}
|
||||||
|
*/
|
||||||
|
const config = {
|
||||||
|
jpeg: {
|
||||||
|
quality: 100
|
||||||
|
},
|
||||||
|
webp: {
|
||||||
|
lossless: true
|
||||||
|
},
|
||||||
|
png: {
|
||||||
|
quality: 100
|
||||||
|
},
|
||||||
|
gif: {
|
||||||
|
quality: 100
|
||||||
|
},
|
||||||
|
avif: {
|
||||||
|
lossless: true
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/** @type {sharp.ResizeOptions} */
|
||||||
|
const resize_config = {
|
||||||
|
width: 1280,
|
||||||
|
height: 1280,
|
||||||
|
fit: sharp.fit.inside,
|
||||||
|
withoutEnlargement: true
|
||||||
|
};
|
||||||
|
|
||||||
|
function* walk_directory(dir) {
|
||||||
|
try {
|
||||||
|
const files = readdirSync(dir);
|
||||||
|
|
||||||
|
for (const file of files) {
|
||||||
|
const pathToFile = join(dir, file);
|
||||||
|
const relativePath = relative(project_root, pathToFile);
|
||||||
|
|
||||||
|
// Skip excluded directories
|
||||||
|
if (excluded_dirs.some((excluded) => relativePath.includes(excluded))) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isDirectory = statSync(pathToFile).isDirectory();
|
||||||
|
if (isDirectory) {
|
||||||
|
yield* walk_directory(pathToFile);
|
||||||
|
} else {
|
||||||
|
yield pathToFile;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error accessing directory ${dir}:`, error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function is_image(file) {
|
||||||
|
const extension = file.split('.').pop()?.toLowerCase();
|
||||||
|
return extension && Object.keys(config).includes(extension);
|
||||||
|
}
|
||||||
|
|
||||||
|
function format_size(bytes) {
|
||||||
|
const sizes = ['B', 'KB', 'MB'];
|
||||||
|
const i = Math.floor(Math.log(bytes) / Math.log(1024));
|
||||||
|
return `${(bytes / Math.pow(1024, i)).toFixed(2)} ${sizes[i]}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function optimize_image(file) {
|
||||||
|
const is_animated = file.endsWith('.gif');
|
||||||
|
const image = sharp(file, { animated: is_animated });
|
||||||
|
|
||||||
|
try {
|
||||||
|
const size_before = (await image.toBuffer()).length;
|
||||||
|
const meta = await image.metadata();
|
||||||
|
|
||||||
|
if (!meta.format || !config[meta.format]) {
|
||||||
|
console.warn(`Unsupported format for file: ${file}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const buffer = await image[meta.format](config[meta.format])
|
||||||
|
.resize(resize_config)
|
||||||
|
.toBuffer();
|
||||||
|
const size_after = buffer.length;
|
||||||
|
|
||||||
|
if (size_after >= size_before) {
|
||||||
|
console.log(`Skipping ${relative(project_root, file)} - no size reduction possible`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const savings = (((size_before - size_after) / size_before) * 100).toFixed(2);
|
||||||
|
console.log(`Optimizing ${relative(project_root, file)}`);
|
||||||
|
console.log(` Before: ${format_size(size_before)}`);
|
||||||
|
console.log(` After: ${format_size(size_after)}`);
|
||||||
|
console.log(` Saved: ${savings}%`);
|
||||||
|
|
||||||
|
await sharp(buffer).toFile(file);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error processing ${file}:`, error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
let total_files = 0;
|
||||||
|
let processed_files = 0;
|
||||||
|
|
||||||
|
console.log('Starting image optimization...\n');
|
||||||
|
|
||||||
|
for (const search_dir of search_dirs) {
|
||||||
|
const full_path = join(project_root, search_dir);
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (!statSync(full_path).isDirectory()) continue;
|
||||||
|
|
||||||
|
for (const file of walk_directory(full_path)) {
|
||||||
|
if (!is_image(file)) continue;
|
||||||
|
total_files++;
|
||||||
|
|
||||||
|
await optimize_image(file);
|
||||||
|
processed_files++;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
if (error.code === 'ENOENT') {
|
||||||
|
console.log(`Directory ${search_dir} not found - skipping`);
|
||||||
|
} else {
|
||||||
|
console.error(`Error processing ${search_dir}:`, error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`\nOptimization complete!`);
|
||||||
|
console.log(`Processed ${processed_files} of ${total_files} image files`);
|
||||||
|
}
|
||||||
|
|
||||||
|
await main();
|
||||||
46
src/app.css
@@ -8,6 +8,7 @@
|
|||||||
--color-primary: hsl(var(--color-primary));
|
--color-primary: hsl(var(--color-primary));
|
||||||
--color-secondary: hsl(var(--color-secondary));
|
--color-secondary: hsl(var(--color-secondary));
|
||||||
--color-accent: var(--color-secondary);
|
--color-accent: var(--color-secondary);
|
||||||
|
--color-smooth: var(--color-smooth);
|
||||||
|
|
||||||
/* pink */
|
/* pink */
|
||||||
--color-pink-200: hsl(var(--color-pink-hue) 98% 84%);
|
--color-pink-200: hsl(var(--color-pink-hue) 98% 84%);
|
||||||
@@ -45,6 +46,9 @@
|
|||||||
--color-blue-500: calc(hsl(var(--color-blue-hue) - 1) 99% 70%);
|
--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-700: calc(hsl(var(--color-blue-hue) - 1) 42% 42%);
|
||||||
|
|
||||||
|
/* green */
|
||||||
|
--color-green-700: #0a714f;
|
||||||
|
|
||||||
/* secondary */
|
/* secondary */
|
||||||
--color-secondary-100: hsl(var(--color-secondary-hue) 99% 66%);
|
--color-secondary-100: hsl(var(--color-secondary-hue) 99% 66%);
|
||||||
--color-accent-200: hsl(var(--color-secondary-hue), 78%, 60%, 0.32);
|
--color-accent-200: hsl(var(--color-secondary-hue), 78%, 60%, 0.32);
|
||||||
@@ -53,7 +57,7 @@
|
|||||||
--color-white: hsl(0 0% 100%);
|
--color-white: hsl(0 0% 100%);
|
||||||
--color-black: hsl(0 0% 0%);
|
--color-black: hsl(0 0% 0%);
|
||||||
--color-transparent: rgba(0, 0, 0, 0);
|
--color-transparent: rgba(0, 0, 0, 0);
|
||||||
--color-smooth: hsl(var(--color-greyscale-hue) 6%, 10%, 0.04);
|
--color-offset: hsl(var(--color-greyscale-hue) 2%, 11%, 0.94);
|
||||||
--color-greyscale-25: hsl(var(--color-greyscale-hue) 11% 98%);
|
--color-greyscale-25: hsl(var(--color-greyscale-hue) 11% 98%);
|
||||||
--color-greyscale-50: hsl(var(--color-greyscale-hue) 11% 94%);
|
--color-greyscale-50: hsl(var(--color-greyscale-hue) 11% 94%);
|
||||||
--color-greyscale-100: hsl(var(--color-greyscale-hue) 6% 90%);
|
--color-greyscale-100: hsl(var(--color-greyscale-hue) 6% 90%);
|
||||||
@@ -77,8 +81,12 @@
|
|||||||
|
|
||||||
/* Animations */
|
/* Animations */
|
||||||
--animate-scale-in: scale-in 200ms ease-out forwards;
|
--animate-scale-in: scale-in 200ms ease-out forwards;
|
||||||
--animate-text: fade-in 0.75s ease-in-out both, blur 0.75s ease-in-out both,
|
--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;
|
up 0.75s ease-in-out both;
|
||||||
|
--animate-scroll: scroll 60s linear infinite;
|
||||||
|
--animate-fade-in: fade 0.5s ease-in-out both;
|
||||||
|
--animate-marquee: marquee var(--speed, 30s) linear infinite var(--direction, forwards);
|
||||||
|
|
||||||
/* Pink polyfills */
|
/* Pink polyfills */
|
||||||
--transition: 0.2s;
|
--transition: 0.2s;
|
||||||
@@ -93,6 +101,18 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@keyframes caret-blink {
|
||||||
|
0%,
|
||||||
|
70%,
|
||||||
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
20%,
|
||||||
|
50% {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@keyframes blur {
|
@keyframes blur {
|
||||||
0% {
|
0% {
|
||||||
filter: blur(5px);
|
filter: blur(5px);
|
||||||
@@ -120,6 +140,21 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@keyframes scroll {
|
||||||
|
0% {
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: translateX(-100%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes marquee {
|
||||||
|
to {
|
||||||
|
transform: translateX(-50%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* Fonts */
|
/* Fonts */
|
||||||
--font-family-sans: 'Inter', arial, sans-serif;
|
--font-family-sans: 'Inter', arial, sans-serif;
|
||||||
--font-family-mono: 'Fira Code', monospace;
|
--font-family-mono: 'Fira Code', monospace;
|
||||||
@@ -128,9 +163,12 @@
|
|||||||
--font-family-archia: 'Archia', arial, sans-serif;
|
--font-family-archia: 'Archia', arial, sans-serif;
|
||||||
|
|
||||||
/* Font sizes */
|
/* 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: 0.75rem;
|
||||||
--font-size-micro--line-height: 1rem;
|
--font-size-micro--line-height: 1rem;
|
||||||
--font-size-micro--letter-spacing: var(--letter-spacing-loose);
|
--font-size-micro--letter-spacing: var(--letter-spacing-tighter);
|
||||||
--font-size-caption: 0.875rem;
|
--font-size-caption: 0.875rem;
|
||||||
--font-size-caption--line-height: 1.375rem;
|
--font-size-caption--line-height: 1.375rem;
|
||||||
--font-size-caption--letter-spacing: var(--letter-spacing-tight);
|
--font-size-caption--letter-spacing: var(--letter-spacing-tight);
|
||||||
@@ -190,6 +228,7 @@
|
|||||||
--color-accent: var(--color-pink-600);
|
--color-accent: var(--color-pink-600);
|
||||||
--color-badge-bg: var(--color-badge-bg-light);
|
--color-badge-bg: var(--color-badge-bg-light);
|
||||||
--color-badge-border: var(--color-badge-border-light);
|
--color-badge-border: var(--color-badge-border-light);
|
||||||
|
--color-smooth: hsl(var(--color-greyscale-hue) 6%, 10%, 0.04);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* dark theme */
|
/* dark theme */
|
||||||
@@ -198,6 +237,7 @@
|
|||||||
--color-secondary: var(--color-greyscale-300);
|
--color-secondary: var(--color-greyscale-300);
|
||||||
--color-badge-bg: var(--color-badge-bg-dark);
|
--color-badge-bg: var(--color-badge-bg-dark);
|
||||||
--color-badge-border: var(--color-badge-border-dark);
|
--color-badge-border: var(--color-badge-border-dark);
|
||||||
|
--color-smooth: hsl(0 0%, 100%, 0.06);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Container */
|
/* Container */
|
||||||
|
|||||||
@@ -28,6 +28,51 @@ const redirecter: Handle = async ({ event, resolve }) => {
|
|||||||
return await resolve(event);
|
return await resolve(event);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const securityheaders: Handle = async ({ event, resolve }) => {
|
||||||
|
const nonce = Buffer.from(crypto.randomUUID()).toString('base64');
|
||||||
|
(event.locals as { nonce: string }).nonce = nonce;
|
||||||
|
|
||||||
|
const response = await resolve(event, {
|
||||||
|
transformPageChunk: ({ html }) => {
|
||||||
|
return html.replace(/%sveltekit.nonce%/g, nonce);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const cspDirectives = [
|
||||||
|
"default-src 'self'",
|
||||||
|
"script-src 'self' 'unsafe-inline' 'unsafe-eval'",
|
||||||
|
"style-src 'self' 'unsafe-inline'",
|
||||||
|
"img-src 'self' data: https:",
|
||||||
|
"font-src 'self'",
|
||||||
|
"object-src 'none'",
|
||||||
|
"base-uri 'self'",
|
||||||
|
"form-action 'self'",
|
||||||
|
"frame-ancestors 'self' https://www.youtube.com https://*.vimeo.com",
|
||||||
|
'block-all-mixed-content',
|
||||||
|
'upgrade-insecure-requests',
|
||||||
|
"connect-src 'self' https://cloud.appwrite.io",
|
||||||
|
"frame-src 'self' https://www.youtube.com https://status.appwrite.online https://www.youtube-nocookie.com https://player.vimeo.com"
|
||||||
|
];
|
||||||
|
|
||||||
|
// Set security headers
|
||||||
|
response.headers.set('Content-Security-Policy', cspDirectives.join('; '));
|
||||||
|
|
||||||
|
// HTTP Strict Transport Security
|
||||||
|
// max-age is set to 1 year in seconds
|
||||||
|
response.headers.set(
|
||||||
|
'Strict-Transport-Security',
|
||||||
|
'max-age=31536000; includeSubDomains; preload'
|
||||||
|
);
|
||||||
|
|
||||||
|
// X-Content-Type-Options
|
||||||
|
response.headers.set('X-Content-Type-Options', 'nosniff');
|
||||||
|
|
||||||
|
// X-Frame-Options
|
||||||
|
response.headers.set('X-Frame-Options', 'DENY');
|
||||||
|
|
||||||
|
return response;
|
||||||
|
};
|
||||||
|
|
||||||
const bannerRewriter: Handle = async ({ event, resolve }) => {
|
const bannerRewriter: Handle = async ({ event, resolve }) => {
|
||||||
const response = await resolve(event, {
|
const response = await resolve(event, {
|
||||||
transformPageChunk: ({ html }) => html.replace('%aw_banner_key%', BANNER_KEY)
|
transformPageChunk: ({ html }) => html.replace('%aw_banner_key%', BANNER_KEY)
|
||||||
@@ -35,5 +80,5 @@ const bannerRewriter: Handle = async ({ event, resolve }) => {
|
|||||||
return response;
|
return response;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const handle = sequence(Sentry.sentryHandle(), redirecter, bannerRewriter);
|
export const handle = sequence(Sentry.sentryHandle(), redirecter, bannerRewriter, securityheaders);
|
||||||
export const handleError = Sentry.handleErrorWithSentry();
|
export const handleError = Sentry.handleErrorWithSentry();
|
||||||
|
|||||||
@@ -69,17 +69,4 @@ export const trackEvent = async (name: string, data: object = {}) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export function isTrackingAllowed() {
|
export const isTrackingAllowed = () => !ENV.TEST;
|
||||||
if (ENV.TEST) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (window.navigator?.doNotTrack) {
|
|
||||||
if (navigator.doNotTrack === '1' || navigator.doNotTrack === 'yes') {
|
|
||||||
return false;
|
|
||||||
} else {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 210 KiB After Width: | Height: | Size: 111 KiB |
|
Before Width: | Height: | Size: 183 KiB After Width: | Height: | Size: 102 KiB |
|
Before Width: | Height: | Size: 197 KiB After Width: | Height: | Size: 100 KiB |
|
Before Width: | Height: | Size: 300 KiB After Width: | Height: | Size: 120 KiB |
|
Before Width: | Height: | Size: 98 KiB After Width: | Height: | Size: 59 KiB |
|
Before Width: | Height: | Size: 265 KiB After Width: | Height: | Size: 130 KiB |
@@ -158,7 +158,6 @@
|
|||||||
import { postController } from './post';
|
import { postController } from './post';
|
||||||
import Post from './post/post.svelte';
|
import Post from './post/post.svelte';
|
||||||
import { anyify } from '$lib/utils/anyify';
|
import { anyify } from '$lib/utils/anyify';
|
||||||
import Badge from '$lib/components/ui/Badge.svelte';
|
|
||||||
|
|
||||||
/* Basic Animation setup */
|
/* Basic Animation setup */
|
||||||
let scrollInfo = {
|
let scrollInfo = {
|
||||||
@@ -508,10 +507,6 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.75rem;
|
gap: 0.75rem;
|
||||||
|
|
||||||
.web-label {
|
|
||||||
margin-block-start: 0.25rem;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
h4 {
|
h4 {
|
||||||
|
|||||||
@@ -108,10 +108,6 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.75rem;
|
gap: 0.75rem;
|
||||||
|
|
||||||
.web-label {
|
|
||||||
margin-block-start: 0.25rem;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
h4 {
|
h4 {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { flip } from '$lib/utils/flip';
|
import { flip } from '$lib/utils/flip';
|
||||||
import { crossfade, scale, slide } from 'svelte/transition';
|
import { scale, slide } from 'svelte/transition';
|
||||||
import { functionsController } from '.';
|
import { functionsController } from '.';
|
||||||
|
|
||||||
const { state } = functionsController;
|
const { state } = functionsController;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { safeAnimate, sleep } from '$lib/animations';
|
import { safeAnimate } from '$lib/animations';
|
||||||
import { createResettable } from '$lib/utils/resettable';
|
import { createResettable } from '$lib/utils/resettable';
|
||||||
import { animate } from 'motion';
|
import { animate } from 'motion';
|
||||||
import { getElSelector } from '../Products.svelte';
|
import { getElSelector } from '../Products.svelte';
|
||||||
|
|||||||
@@ -231,6 +231,20 @@ export function write(text: string, cb: (v: string) => void, duration = 500) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function unwrite(text: string, cb: (v: string) => void, duration = 500) {
|
||||||
|
const step = duration / text.length;
|
||||||
|
let i = text.length;
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
cb(text.slice(0, --i));
|
||||||
|
if (i === 0) {
|
||||||
|
clearInterval(interval);
|
||||||
|
resolve(undefined);
|
||||||
|
}
|
||||||
|
}, step);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export function sleep(duration: number) {
|
export function sleep(duration: number) {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
setTimeout(resolve, duration);
|
setTimeout(resolve, duration);
|
||||||
|
|||||||
@@ -8,12 +8,12 @@
|
|||||||
<summary
|
<summary
|
||||||
class="collapsible-button flex cursor-pointer list-none appearance-none items-center justify-between marker:hidden [&::-webkit-details-marker]:hidden"
|
class="collapsible-button flex cursor-pointer list-none appearance-none items-center justify-between marker:hidden [&::-webkit-details-marker]:hidden"
|
||||||
>
|
>
|
||||||
<span class="text">{title}</span>
|
<span class="text-primary text-sub-body font-medium">{title}</span>
|
||||||
<div class="icon text-primary transition-transform group-[&[open]]:rotate-180">
|
<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" />
|
||||||
</div>
|
</div>
|
||||||
</summary>
|
</summary>
|
||||||
<div class="collapsible-content flex flex-col">
|
<div class="collapsible-content text-secondary text-sub-body flex flex-col">
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
</details>
|
</details>
|
||||||
|
|||||||
83
src/lib/components/AppwriteIn100Seconds.svelte
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { fade, scale } from 'svelte/transition';
|
||||||
|
import { trackEvent } from '$lib/actions/analytics';
|
||||||
|
|
||||||
|
let show = false;
|
||||||
|
|
||||||
|
function handleKeypress(event: KeyboardEvent) {
|
||||||
|
if (event.key.toLowerCase() === 'escape' || event.key.toLowerCase() === 'esc') {
|
||||||
|
event.preventDefault();
|
||||||
|
show = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:window on:keydown={handleKeypress} />
|
||||||
|
|
||||||
|
<button
|
||||||
|
on:click={() => {
|
||||||
|
show = true;
|
||||||
|
trackEvent('Appwrite in 100 seconds');
|
||||||
|
}}
|
||||||
|
class="web-button is-secondary cursor-pointer"
|
||||||
|
>
|
||||||
|
<span class="web-icon-play" style:color="unset" />
|
||||||
|
<span>Appwrite in 100 seconds</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{#if show}
|
||||||
|
<!-- `on:keypress={null}` silences the a11y warnings -->
|
||||||
|
<div
|
||||||
|
tabindex="0"
|
||||||
|
role="button"
|
||||||
|
class="overlay"
|
||||||
|
on:keypress={null}
|
||||||
|
on:click={() => (show = false)}
|
||||||
|
transition:fade={{ duration: 150 }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div class="web-media content" transition:scale={{ duration: 250, start: 0.95 }}>
|
||||||
|
<iframe
|
||||||
|
src="https://www.youtube-nocookie.com/embed/L07xPMyL8sY?si=Odrwj1tHzlm12Fi2&controls=0"
|
||||||
|
title="YouTube video player"
|
||||||
|
frameborder="0"
|
||||||
|
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
|
||||||
|
allowfullscreen
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.overlay {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
background-color: rgba(0, 0, 0, 0.5);
|
||||||
|
|
||||||
|
transition: 200ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
position: fixed;
|
||||||
|
left: 50%;
|
||||||
|
top: 50%;
|
||||||
|
translate: -50% -50%;
|
||||||
|
|
||||||
|
display: block;
|
||||||
|
object-fit: contain;
|
||||||
|
|
||||||
|
max-height: 75vh;
|
||||||
|
width: calc(80%);
|
||||||
|
aspect-ratio: 16 / 9;
|
||||||
|
|
||||||
|
z-index: 1000;
|
||||||
|
|
||||||
|
transform: scale(0.975);
|
||||||
|
transition: 200ms ease;
|
||||||
|
|
||||||
|
iframe {
|
||||||
|
display: block;
|
||||||
|
inline-size: 100%;
|
||||||
|
block-size: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -4,27 +4,17 @@
|
|||||||
export let label: string = 'Get started';
|
export let label: string = 'Get started';
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="call-to-action">
|
<div
|
||||||
<div class="details">
|
class="bg relative mt-12 !-mb-28 flex min-h-[12rem] items-center justify-center overflow-hidden border-t border-[hsl(var(--web-color-subtle))] py-12 lg:!-mb-[184px]"
|
||||||
|
>
|
||||||
|
<div class="flex max-w-3xs flex-col items-center justify-center gap-5 text-center">
|
||||||
<h2 class="text-label">{heading}</h2>
|
<h2 class="text-label">{heading}</h2>
|
||||||
<a href={url} class="web-button">{label}</a>
|
<a href={url} class="web-button">{label}</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
.call-to-action {
|
.bg {
|
||||||
border-top: 1px solid hsl(var(--web-color-subtle));
|
|
||||||
padding: 48px 0;
|
|
||||||
min-height: 180px;
|
|
||||||
margin-top: 48px;
|
|
||||||
display: flex;
|
|
||||||
margin-bottom: -24px;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
position: relative;
|
|
||||||
overflow: hidden;
|
|
||||||
|
|
||||||
&::before {
|
&::before {
|
||||||
content: '';
|
content: '';
|
||||||
position: absolute;
|
position: absolute;
|
||||||
@@ -36,22 +26,6 @@
|
|||||||
rgba(253, 54, 110, 0.09),
|
rgba(253, 54, 110, 0.09),
|
||||||
transparent 85%
|
transparent 85%
|
||||||
);
|
);
|
||||||
//filter: blur(10px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.details {
|
|
||||||
gap: 20px;
|
|
||||||
max-width: 250px;
|
|
||||||
color: #fff;
|
|
||||||
text-align: center;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
|
|
||||||
h2 {
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { page } from '$app/stores';
|
import { page } from '$app/stores';
|
||||||
|
import { fade } from 'svelte/transition';
|
||||||
|
import { loggedIn, user } from '$lib/utils/console';
|
||||||
|
import { PUBLIC_GROWTH_ENDPOINT } from '$env/static/public';
|
||||||
|
|
||||||
export let date: string | undefined = undefined;
|
export let date: string | undefined = undefined;
|
||||||
let showFeedback = false;
|
let showFeedback = false;
|
||||||
@@ -13,7 +16,10 @@
|
|||||||
async function handleSubmit() {
|
async function handleSubmit() {
|
||||||
submitting = true;
|
submitting = true;
|
||||||
error = undefined;
|
error = undefined;
|
||||||
const response = await fetch('https://growth.appwrite.io/v1/feedback/docs', {
|
|
||||||
|
const userId = loggedIn && $user?.$id ? $user.$id : undefined;
|
||||||
|
|
||||||
|
const response = await fetch(`${PUBLIC_GROWTH_ENDPOINT}/feedback/docs`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json'
|
||||||
@@ -22,7 +28,10 @@
|
|||||||
email,
|
email,
|
||||||
type: feedbackType,
|
type: feedbackType,
|
||||||
route: $page.route.id,
|
route: $page.route.id,
|
||||||
comment
|
comment,
|
||||||
|
metaFields: {
|
||||||
|
userId
|
||||||
|
}
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
submitting = false;
|
submitting = false;
|
||||||
@@ -32,6 +41,7 @@
|
|||||||
}
|
}
|
||||||
comment = email = '';
|
comment = email = '';
|
||||||
submitted = true;
|
submitted = true;
|
||||||
|
setTimeout(() => (showFeedback = false), 500);
|
||||||
}
|
}
|
||||||
|
|
||||||
function reset() {
|
function reset() {
|
||||||
@@ -44,6 +54,10 @@
|
|||||||
$: if (!showFeedback) {
|
$: if (!showFeedback) {
|
||||||
reset();
|
reset();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$: if (showFeedback && loggedIn && $user?.email) {
|
||||||
|
email = $user?.email;
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<section class="web-content-footer">
|
<section class="web-content-footer">
|
||||||
@@ -59,7 +73,7 @@
|
|||||||
class="web-radio-button"
|
class="web-radio-button"
|
||||||
aria-label="helpful"
|
aria-label="helpful"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
showFeedback = feedbackType === 'positive' ? false : true;
|
showFeedback = feedbackType !== 'positive';
|
||||||
feedbackType = 'positive';
|
feedbackType = 'positive';
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -69,7 +83,7 @@
|
|||||||
class="web-radio-button"
|
class="web-radio-button"
|
||||||
aria-label="unhelpful"
|
aria-label="unhelpful"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
showFeedback = feedbackType === 'negative' ? false : true;
|
showFeedback = feedbackType !== 'negative';
|
||||||
feedbackType = 'negative';
|
feedbackType = 'negative';
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -103,6 +117,7 @@
|
|||||||
on:submit|preventDefault={handleSubmit}
|
on:submit|preventDefault={handleSubmit}
|
||||||
class="web-card is-normal"
|
class="web-card is-normal"
|
||||||
style="--card-padding:1rem"
|
style="--card-padding:1rem"
|
||||||
|
out:fade={{ duration: 450 }}
|
||||||
>
|
>
|
||||||
<div class="flex flex-col gap-2">
|
<div class="flex flex-col gap-2">
|
||||||
<label for="message">
|
<label for="message">
|
||||||
|
|||||||
@@ -32,11 +32,11 @@
|
|||||||
{ label: 'Solid', href: '/docs/quick-starts/solid' }
|
{ label: 'Solid', href: '/docs/quick-starts/solid' }
|
||||||
],
|
],
|
||||||
Products: [
|
Products: [
|
||||||
{ label: 'Auth', href: '/docs/products/auth' },
|
{ label: 'Auth', href: '/products/auth' },
|
||||||
{ label: 'Databases', href: '/docs/products/databases' },
|
{ label: 'Databases', href: '/docs/products/databases' },
|
||||||
{ label: 'Functions', href: '/docs/products/functions' },
|
{ label: 'Functions', href: '/products/functions' },
|
||||||
{ label: 'Messaging', href: '/products/messaging' },
|
{ label: 'Messaging', href: '/products/messaging' },
|
||||||
{ label: 'Storage', href: '/docs/products/storage' },
|
{ label: 'Storage', href: '/products/storage' },
|
||||||
{ label: 'Realtime', href: '/docs/apis/realtime' }
|
{ label: 'Realtime', href: '/docs/apis/realtime' }
|
||||||
],
|
],
|
||||||
Learn: [
|
Learn: [
|
||||||
|
|||||||
@@ -1,16 +1,26 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Button from './ui/Button.svelte';
|
|
||||||
import { PUBLIC_APPWRITE_DASHBOARD } from '$env/static/public';
|
import { PUBLIC_APPWRITE_DASHBOARD } from '$env/static/public';
|
||||||
import { classNames } from '$lib/utils/classnames';
|
import { classNames } from '$lib/utils/classnames';
|
||||||
import { trackEvent } from '$lib/actions/analytics';
|
import { trackEvent } from '$lib/actions/analytics';
|
||||||
|
import { browser } from '$app/environment';
|
||||||
|
|
||||||
export let classes = '';
|
export let classes = '';
|
||||||
|
|
||||||
|
function getTrackingEventName() {
|
||||||
|
return browser
|
||||||
|
? 'loggedIn' in document.body.dataset
|
||||||
|
? 'Go to console'
|
||||||
|
: 'Get started'
|
||||||
|
: 'Get started';
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<a
|
<a
|
||||||
class={classNames('web-button web-u-inline-width-100-percent-mobile', classes)}
|
class={classNames('web-button web-u-inline-width-100-percent-mobile', classes)}
|
||||||
href={PUBLIC_APPWRITE_DASHBOARD}
|
href={PUBLIC_APPWRITE_DASHBOARD}
|
||||||
on:click={() => trackEvent('Get started/go to console in header')}
|
on:click={() => {
|
||||||
|
trackEvent(`${getTrackingEventName()} in header`);
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<span class="hidden group-[&[data-logged-in]]/body:block">Go to Console</span>
|
<span class="hidden group-[&[data-logged-in]]/body:block">Go to Console</span>
|
||||||
<span class="block group-[&[data-logged-in]]/body:hidden">Get started</span>
|
<span class="block group-[&[data-logged-in]]/body:hidden">Get started</span>
|
||||||
|
|||||||
@@ -87,7 +87,7 @@
|
|||||||
<ul
|
<ul
|
||||||
class="web-u-padding-block-start-80 grid grid-cols-3 text-center md:grid-cols-6 md:gap-10"
|
class="web-u-padding-block-start-80 grid grid-cols-3 text-center md:grid-cols-6 md:gap-10"
|
||||||
>
|
>
|
||||||
{#each logos as { src, alt, width, height }, i}
|
{#each logos as { src, alt, width, height }}
|
||||||
<li class="grid place-content-center">
|
<li class="grid place-content-center">
|
||||||
<img {src} {alt} {width} {height} />
|
<img {src} {alt} {width} {height} />
|
||||||
</li>
|
</li>
|
||||||
|
|||||||
@@ -92,6 +92,13 @@
|
|||||||
display: grid;
|
display: grid;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media #{devices.$break1} {
|
||||||
|
.status {
|
||||||
|
height: 55px;
|
||||||
|
margin-bottom: 6px; /* balancing due to style:margin-top="-4px" & the `iframe` has some spacings too I think */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.e-main-footer {
|
.e-main-footer {
|
||||||
display: flex;
|
display: flex;
|
||||||
@media #{devices.$break1} {
|
@media #{devices.$break1} {
|
||||||
|
|||||||
71
src/lib/components/MainNav.svelte
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
<script lang="ts" context="module">
|
||||||
|
export type NavLink = {
|
||||||
|
label: string;
|
||||||
|
href?: string;
|
||||||
|
showBadge?: boolean;
|
||||||
|
submenu?: ComponentType;
|
||||||
|
mobileSubmenu?: ComponentType;
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { classNames } from '$lib/utils/classnames';
|
||||||
|
import type { ComponentType } from 'svelte';
|
||||||
|
|
||||||
|
export let initialized = false;
|
||||||
|
|
||||||
|
export let links: NavLink[] = [];
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<nav class="web-main-header-nav" aria-label="Main">
|
||||||
|
<ul class="web-main-header-nav-list flex items-center">
|
||||||
|
{#each links as link}
|
||||||
|
<li class="web-main-header-nav-item text-primary hover:text-accent">
|
||||||
|
{#if link.submenu}
|
||||||
|
<div
|
||||||
|
class="web-main-header-nav-item-button"
|
||||||
|
aria-haspopup="true"
|
||||||
|
aria-expanded="false"
|
||||||
|
aria-controls="submenu"
|
||||||
|
data-submenu-button
|
||||||
|
>
|
||||||
|
<svelte:component this={link.submenu} label={link.label} />
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<a
|
||||||
|
class={classNames(
|
||||||
|
'data-[badge]:after:animate-scale-in data-[badge]:relative data-[badge]:after:absolute data-[badge]:after:size-1.5 data-[badge]:after:translate-full data-[badge]:after:rounded-full'
|
||||||
|
)}
|
||||||
|
href={link.href}
|
||||||
|
data-initialized={initialized ? '' : undefined}
|
||||||
|
data-badge={link.showBadge ? '' : undefined}
|
||||||
|
>{link.label}
|
||||||
|
</a>
|
||||||
|
{/if}
|
||||||
|
</li>
|
||||||
|
{/each}
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
[data-badge] {
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
background-color: hsl(var(--color-accent));
|
||||||
|
border-radius: 100%;
|
||||||
|
width: 0.375rem;
|
||||||
|
height: 0.375rem;
|
||||||
|
|
||||||
|
inset-block-start: -2px;
|
||||||
|
inset-inline-end: -4px;
|
||||||
|
translate: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:not([data-initialized])::after {
|
||||||
|
animation: scale-in 0.2s ease-out;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
import { afterNavigate } from '$app/navigation';
|
import { afterNavigate } from '$app/navigation';
|
||||||
import { IsLoggedIn } from '$lib/components';
|
import { IsLoggedIn } from '$lib/components';
|
||||||
import { GITHUB_REPO_LINK, GITHUB_STARS } from '$lib/constants';
|
import { GITHUB_REPO_LINK, GITHUB_STARS } from '$lib/constants';
|
||||||
import type { NavLink } from '$lib/layouts/Main.svelte';
|
import type { NavLink } from './MainNav.svelte';
|
||||||
|
|
||||||
export let open = false;
|
export let open = false;
|
||||||
export let links: NavLink[];
|
export let links: NavLink[];
|
||||||
@@ -16,7 +16,7 @@
|
|||||||
|
|
||||||
<nav class="web-side-nav web-is-not-desktop" class:hidden={!open}>
|
<nav class="web-side-nav web-is-not-desktop" class:hidden={!open}>
|
||||||
<div class="web-side-nav-wrapper ps-4 pe-4">
|
<div class="web-side-nav-wrapper ps-4 pe-4">
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2 px-4">
|
||||||
<a href="https://cloud.appwrite.io/register" class="web-button is-secondary flex-1">
|
<a href="https://cloud.appwrite.io/register" class="web-button is-secondary flex-1">
|
||||||
Sign up
|
Sign up
|
||||||
</a>
|
</a>
|
||||||
@@ -25,11 +25,15 @@
|
|||||||
<div class="web-side-nav-scroll">
|
<div class="web-side-nav-scroll">
|
||||||
<section>
|
<section>
|
||||||
<ul>
|
<ul>
|
||||||
{#each links as { href, label }}
|
{#each links as { href, label, mobileSubmenu }}
|
||||||
<li>
|
<li>
|
||||||
<a class="web-side-nav-button" {href}>
|
{#if mobileSubmenu}
|
||||||
<span class="text-caption">{label}</span>
|
<svelte:component this={mobileSubmenu} {label} />
|
||||||
</a>
|
{:else}
|
||||||
|
<a class="web-side-nav-button" {href}>
|
||||||
|
<span class="text-caption">{label}</span>
|
||||||
|
</a>
|
||||||
|
{/if}
|
||||||
</li>
|
</li>
|
||||||
{/each}
|
{/each}
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
95
src/lib/components/MultiCodeContextless.svelte
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { platformMap } from '$lib/utils/references';
|
||||||
|
import { writable } from 'svelte/store';
|
||||||
|
import { getCodeHtml, type Language } from '$lib/utils/code';
|
||||||
|
import { copy } from '$lib/utils/copy';
|
||||||
|
import { Select, Tooltip } from '$lib/components';
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
$: snippets = writable(new Set(data.map((d) => d.language)));
|
||||||
|
|
||||||
|
$: content = data.find((d) => d.language === selected)?.content ?? '';
|
||||||
|
|
||||||
|
$: platform = data.find((d) => d.language === selected)?.platform ?? '';
|
||||||
|
|
||||||
|
snippets?.subscribe((n) => {
|
||||||
|
if (selected === null && n.size > 0) {
|
||||||
|
selected = Array.from(n)[0] as Language;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
enum CopyStatus {
|
||||||
|
Copy = 'Copy',
|
||||||
|
Copied = 'Copied!'
|
||||||
|
}
|
||||||
|
let copyText = CopyStatus.Copy;
|
||||||
|
async function handleCopy() {
|
||||||
|
await copy(content);
|
||||||
|
|
||||||
|
copyText = CopyStatus.Copied;
|
||||||
|
setTimeout(() => {
|
||||||
|
copyText = CopyStatus.Copy;
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
$: result = getCodeHtml({
|
||||||
|
content,
|
||||||
|
language: selected ?? 'sh',
|
||||||
|
withLineNumbers: true
|
||||||
|
});
|
||||||
|
$: options = Array.from($snippets).map((language) => ({
|
||||||
|
value: language,
|
||||||
|
label: platformMap[language]
|
||||||
|
}));
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<section
|
||||||
|
class="dark web-code-snippet mx-auto lg:!max-w-[90vw]"
|
||||||
|
aria-label="code-snippet panel"
|
||||||
|
style={`width: ${width ? width / 16 + 'rem' : 'inherit'}; height: ${
|
||||||
|
height ? height / 16 + 'rem' : 'inherit'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<header class="web-code-snippet-header">
|
||||||
|
<div class="web-code-snippet-header-start">
|
||||||
|
<div class="flex gap-4">
|
||||||
|
{#if platform}
|
||||||
|
<div class="web-tag"><span class="text">{platform}</span></div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="web-code-snippet-header-end">
|
||||||
|
<ul class="buttons-list flex gap-3">
|
||||||
|
{#if $snippets.entries.length}
|
||||||
|
<li class="buttons-list-item flex self-center">
|
||||||
|
<Select bind:value={selected} bind:options />
|
||||||
|
</li>
|
||||||
|
{/if}
|
||||||
|
<li class="buttons-list-item" style="padding-inline-start: 13px">
|
||||||
|
<Tooltip>
|
||||||
|
<button
|
||||||
|
on:click={handleCopy}
|
||||||
|
class="web-icon-button"
|
||||||
|
aria-label="copy code from code-snippet"
|
||||||
|
><span class="web-icon-copy" aria-hidden="true" /></button
|
||||||
|
>
|
||||||
|
<svelte:fragment slot="tooltip">
|
||||||
|
{copyText}
|
||||||
|
</svelte:fragment>
|
||||||
|
</Tooltip>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
<div
|
||||||
|
class="web-code-snippet-content"
|
||||||
|
style={`height: ${height ? height / 16 + 'rem' : 'inherit'}`}
|
||||||
|
>
|
||||||
|
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
|
||||||
|
{@html result}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
@@ -1,14 +1,15 @@
|
|||||||
<script context="module" lang="ts">
|
<script context="module" lang="ts">
|
||||||
|
import { PUBLIC_GROWTH_ENDPOINT } from '$env/static/public';
|
||||||
|
|
||||||
export async function newsletter(name: string, email: string) {
|
export async function newsletter(name: string, email: string) {
|
||||||
const response = await fetch('https://growth.appwrite.io/v1/newsletter/subscribe', {
|
const response = await fetch(`${PUBLIC_GROWTH_ENDPOINT}/newsletter/subscribe`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json'
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
name,
|
name,
|
||||||
email,
|
email
|
||||||
cloud: true /* not optional on the growth endpoint. */
|
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
return response;
|
return response;
|
||||||
|
|||||||
@@ -3,7 +3,13 @@
|
|||||||
import { trackEvent } from '$lib/actions/analytics';
|
import { trackEvent } from '$lib/actions/analytics';
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<img src="/images/bgs/pre-footer.png" alt="" class="web-pre-footer-bg" style="z-index:-1" />
|
<img
|
||||||
|
src="/images/bgs/pre-footer.png"
|
||||||
|
alt=""
|
||||||
|
class="web-pre-footer-bg"
|
||||||
|
loading="lazy"
|
||||||
|
style="z-index:-1"
|
||||||
|
/>
|
||||||
|
|
||||||
<div class="web-u-row-gap-80 relative grid gap-8 md:grid-cols-2">
|
<div class="web-u-row-gap-80 relative grid gap-8 md:grid-cols-2">
|
||||||
<section class="web-hero flex items-center justify-center gap-y-8">
|
<section class="web-hero flex items-center justify-center gap-y-8">
|
||||||
@@ -152,5 +158,6 @@
|
|||||||
height: auto;
|
height: auto;
|
||||||
max-inline-size: unset;
|
max-inline-size: unset;
|
||||||
max-block-size: unset;
|
max-block-size: unset;
|
||||||
|
filter: blur(100px);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
87
src/lib/components/ProductsMobileSubmenu.svelte
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { classNames } from '$lib/utils/classnames';
|
||||||
|
import { melt, createCollapsible } from '@melt-ui/svelte';
|
||||||
|
import { slide } from 'svelte/transition';
|
||||||
|
import { products, sublinks } from './ProductsSubmenu.svelte';
|
||||||
|
import { dev } from '$app/environment';
|
||||||
|
|
||||||
|
export let label: string;
|
||||||
|
|
||||||
|
const {
|
||||||
|
elements: { root, content, trigger },
|
||||||
|
states: { open }
|
||||||
|
} = createCollapsible();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div use:melt={$root} class="relative mx-auto block md:hidden">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<button
|
||||||
|
use:melt={$trigger}
|
||||||
|
class="text-caption web-side-nav-button flex items-center justify-between"
|
||||||
|
>{label}
|
||||||
|
<span
|
||||||
|
class={classNames('web-icon-chevron-down transition-transform', {
|
||||||
|
'rotate-180': $open
|
||||||
|
})}
|
||||||
|
/></button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
{#if $open}
|
||||||
|
<div use:melt={$content} transition:slide class="py-3 px-4">
|
||||||
|
<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"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="flex size-12 shrink-0 items-center justify-center rounded-lg border border-white/12 bg-white/6"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={product.icon}
|
||||||
|
alt={product.name}
|
||||||
|
class="size-6 grayscale transition-all group-focus:grayscale-0"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="">
|
||||||
|
<span class="text-sub-body text-primary font-medium"
|
||||||
|
>{product.name}
|
||||||
|
|
||||||
|
{#if product.beta}
|
||||||
|
<span
|
||||||
|
class="text-caption bg-accent/24 ml-1 rounded py-1 px-2 font-medium text-white"
|
||||||
|
>Coming soon</span
|
||||||
|
>
|
||||||
|
{/if}
|
||||||
|
</span>
|
||||||
|
<p class="text-caption text-secondary text-pretty">
|
||||||
|
{product.description}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{#if dev}
|
||||||
|
<div class="mt-8">
|
||||||
|
<span
|
||||||
|
class="font-aeonik-fono tracking-loose text-secondary block text-xs uppercase"
|
||||||
|
>This is a title<span class="text-accent">_</span></span
|
||||||
|
>
|
||||||
|
<div class="mt-3 space-y-3">
|
||||||
|
{#each sublinks as sublink}
|
||||||
|
<a
|
||||||
|
href={sublink.href}
|
||||||
|
class="text-caption text-primary flex items-center gap-2"
|
||||||
|
>
|
||||||
|
{sublink.label} <span class="web-icon-chevron-right" />
|
||||||
|
</a>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
209
src/lib/components/ProductsSubmenu.svelte
Normal file
@@ -0,0 +1,209 @@
|
|||||||
|
<script lang="ts" context="module">
|
||||||
|
export type SubmenuItem = {
|
||||||
|
name: string;
|
||||||
|
href: string;
|
||||||
|
description: string;
|
||||||
|
icon: string;
|
||||||
|
beta?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SubLink = {
|
||||||
|
label: string;
|
||||||
|
href: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const products: Array<SubmenuItem> = [
|
||||||
|
{
|
||||||
|
name: 'Auth',
|
||||||
|
href: '/products/auth',
|
||||||
|
description: 'Secure login with multi-factor auth.',
|
||||||
|
icon: '/images/icons/illustrated/dark/auth.png'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Databases',
|
||||||
|
href: '/docs/products/databases',
|
||||||
|
description: 'Scalable and robust databases.',
|
||||||
|
icon: '/images/icons/illustrated/dark/databases.png'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Storage',
|
||||||
|
href: '/products/storage',
|
||||||
|
description: 'Advanced compression and encryption.',
|
||||||
|
icon: '/images/icons/illustrated/dark/storage.png'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Functions',
|
||||||
|
href: '/products/functions',
|
||||||
|
description: 'Deploy & scale serverless functions.',
|
||||||
|
icon: '/images/icons/illustrated/dark/functions.png'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Messaging',
|
||||||
|
href: '/products/messaging',
|
||||||
|
description: 'Set up a full-functioning messaging service.',
|
||||||
|
icon: '/images/icons/illustrated/dark/messaging.png'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Realtime',
|
||||||
|
href: '/docs/apis/realtime',
|
||||||
|
description: 'Subscribe and react to any event.',
|
||||||
|
icon: '/images/icons/illustrated/dark/realtime.png'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
export const sublinks: Array<SubLink> = [
|
||||||
|
{
|
||||||
|
label: 'See more',
|
||||||
|
href: '/'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'See more',
|
||||||
|
href: '/'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'See more',
|
||||||
|
href: '/'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { dev } from '$app/environment';
|
||||||
|
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 },
|
||||||
|
states: { open }
|
||||||
|
} = createDropdownMenu({
|
||||||
|
loop: true
|
||||||
|
});
|
||||||
|
|
||||||
|
export let label: string;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<button
|
||||||
|
class={classNames(
|
||||||
|
'text-primary focus:text-accent hover:text-accent inline-flex cursor-pointer items-center justify-between outline-none',
|
||||||
|
{
|
||||||
|
'text-accent': $open
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
use:melt={$trigger}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
|
||||||
|
<span
|
||||||
|
class={classNames('web-icon-chevron-down block transition-transform', {
|
||||||
|
'rotate-180': $open
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
</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'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div class="is-special-padding w-full rounded-2xl border border-white/8 bg-[#232325] p-6">
|
||||||
|
<div class="grid w-full grid-cols-1 place-content-between gap-16 lg:grid-cols-12">
|
||||||
|
<div class="col-span-8 -mr-12 pr-12">
|
||||||
|
<span
|
||||||
|
class="font-aeonik-fono text-secondary tracking-loose mb-4 block text-xs uppercase"
|
||||||
|
>{label}<span class="text-accent">_</span></span
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="grid grid-flow-col-dense grid-cols-1 gap-2 md:grid-cols-2 md:grid-rows-4"
|
||||||
|
>
|
||||||
|
{#each products as product}
|
||||||
|
<a
|
||||||
|
href={product.href}
|
||||||
|
use:melt={$item}
|
||||||
|
on:click={() => trackEvent(`${product.name} in products submenu`)}
|
||||||
|
class="group flex gap-3 rounded-xl p-1 text-white outline-none transition-colors focus:bg-white/8"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="flex size-12 shrink-0 items-center justify-center rounded-lg border border-white/12 bg-white/6"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={product.icon}
|
||||||
|
alt={product.name}
|
||||||
|
class="size-6 grayscale transition-all group-focus:grayscale-0"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="">
|
||||||
|
<span class="text-sub-body text-primary font-medium"
|
||||||
|
>{product.name}
|
||||||
|
|
||||||
|
{#if product.beta}
|
||||||
|
<span
|
||||||
|
class="text-caption bg-accent/24 ml-1 rounded py-1 px-2 font-medium text-white"
|
||||||
|
>Coming soon</span
|
||||||
|
>
|
||||||
|
{/if}
|
||||||
|
</span>
|
||||||
|
<p class="text-caption text-secondary text-pretty">
|
||||||
|
{product.description}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-span-4 -ml-12 border-l border-white/6 pl-12">
|
||||||
|
<a
|
||||||
|
href="/blog/post/case-study-undo"
|
||||||
|
use:melt={$item}
|
||||||
|
class="block rounded-2xl border border-white/12 bg-white/6 p-4 outline-none focus-within:bg-white/12"
|
||||||
|
>
|
||||||
|
<header class="flex items-center justify-between">
|
||||||
|
<span
|
||||||
|
class="font-aeonik-fono tracking-loose text-secondary block text-xs uppercase"
|
||||||
|
>Customer stories<span class="text-accent">_</span></span
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
href="/blog/category/case-studies"
|
||||||
|
class="text-primary text-caption flex items-center gap-2"
|
||||||
|
>See more <span class="web-icon-chevron-right" /></a
|
||||||
|
>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="flex-1 outline-none">
|
||||||
|
<img
|
||||||
|
src="/images/blog/case-study-undo/cover.png"
|
||||||
|
alt="Case study cover"
|
||||||
|
class="my-6 aspect-[3/1] rounded-xl object-cover"
|
||||||
|
/>
|
||||||
|
<p>
|
||||||
|
Pioneering asset management solutions for the circular economy with UNDŌ
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
{#if dev}
|
||||||
|
<div class="mt-8">
|
||||||
|
<span
|
||||||
|
class="font-aeonik-fono tracking-loose text-secondary block text-xs uppercase"
|
||||||
|
>This is a title<span class="text-accent">_</span></span
|
||||||
|
>
|
||||||
|
<div class="mt-3 space-y-3">
|
||||||
|
{#each sublinks as sublink}
|
||||||
|
<a
|
||||||
|
href={sublink.href}
|
||||||
|
class="text-caption text-primary flex items-center gap-2"
|
||||||
|
>
|
||||||
|
{sublink.label} <span class="web-icon-chevron-right" />
|
||||||
|
</a>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
use:melt={$overlay}
|
||||||
|
class="data-[state=closed]:animate-fade-out fixed inset-0 bg-black/60"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
@@ -26,6 +26,8 @@
|
|||||||
change: unknown;
|
change: unknown;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
export let initialLabel: string = 'Select an option';
|
||||||
|
|
||||||
const {
|
const {
|
||||||
elements: { trigger, menu, option: optionEl, group: groupEl, groupLabel },
|
elements: { trigger, menu, option: optionEl, group: groupEl, groupLabel },
|
||||||
states: { open, selected, selectedLabel }
|
states: { open, selected, selectedLabel }
|
||||||
@@ -80,6 +82,8 @@
|
|||||||
duration: 150,
|
duration: 150,
|
||||||
y: placement === 'top' ? 4 : -4
|
y: placement === 'top' ? 4 : -4
|
||||||
} as FlyParams;
|
} as FlyParams;
|
||||||
|
|
||||||
|
console.log({ initialLabel, $selectedLabel });
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
@@ -93,7 +97,7 @@
|
|||||||
{#if selectedOption?.icon}
|
{#if selectedOption?.icon}
|
||||||
<span class={selectedOption.icon} aria-hidden="true" />
|
<span class={selectedOption.icon} aria-hidden="true" />
|
||||||
{/if}
|
{/if}
|
||||||
<span>{$selectedLabel}</span>
|
<span>{$selectedLabel || initialLabel}</span>
|
||||||
</div>
|
</div>
|
||||||
<span class="icon-cheveron-{$open ? 'up' : 'down'}" aria-hidden="true" />
|
<span class="icon-cheveron-{$open ? 'up' : 'down'}" aria-hidden="true" />
|
||||||
</button>
|
</button>
|
||||||
@@ -115,7 +119,7 @@
|
|||||||
{#if option.icon}
|
{#if option.icon}
|
||||||
<span class={option.icon} aria-hidden="true" />
|
<span class={option.icon} aria-hidden="true" />
|
||||||
{/if}
|
{/if}
|
||||||
<span style:text-transform="capitalize">{option.label}</span>
|
<span>{option.label}</span>
|
||||||
</button>
|
</button>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { getTocCtx } from './TocRoot.svelte';
|
|
||||||
import TocTree from './TocTree.svelte';
|
import TocTree from './TocTree.svelte';
|
||||||
|
import { cubicOut } from 'svelte/easing';
|
||||||
|
import { getTocCtx } from './TocRoot.svelte';
|
||||||
|
import { onMount } from 'svelte';
|
||||||
|
|
||||||
export let showToc = true;
|
export let showToc = true;
|
||||||
|
|
||||||
@@ -12,94 +14,214 @@
|
|||||||
} = getTocCtx();
|
} = getTocCtx();
|
||||||
|
|
||||||
$: progress = Math.max(...$activeHeadingIdxs) / ($headingsTree.length - 1);
|
$: progress = Math.max(...$activeHeadingIdxs) / ($headingsTree.length - 1);
|
||||||
|
|
||||||
|
function slideFade(
|
||||||
|
node: HTMLElement,
|
||||||
|
{
|
||||||
|
delay = 0,
|
||||||
|
duration = 400,
|
||||||
|
easing = cubicOut
|
||||||
|
}: { delay?: number; duration?: number; easing?: (t: number) => number } = {}
|
||||||
|
) {
|
||||||
|
const initialHeight = node.offsetHeight;
|
||||||
|
|
||||||
|
return {
|
||||||
|
delay,
|
||||||
|
duration,
|
||||||
|
easing,
|
||||||
|
css: (t: number) => {
|
||||||
|
return `
|
||||||
|
opacity: ${t};
|
||||||
|
height: ${t * initialHeight}px;
|
||||||
|
overflow: hidden;
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// show toc by default on desktop
|
||||||
|
function handleResizeForTocTree() {
|
||||||
|
showToc = window.innerWidth >= 1024;
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(handleResizeForTocTree);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<aside class="web-grid-120-1fr-auto-side" class:web-is-mobile-closed={!showToc}>
|
<svelte:window on:resize={handleResizeForTocTree} />
|
||||||
<div class="web-page-steps">
|
|
||||||
<div
|
<section class="web-mobile-header">
|
||||||
class="web-page-steps-location web-is-not-mobile"
|
<div class="web-is-only-mobile">
|
||||||
style="--location:{progress * 100}%;"
|
<button
|
||||||
|
on:click={() => (showToc = !showToc)}
|
||||||
|
class="flex w-full items-center justify-between"
|
||||||
>
|
>
|
||||||
<span class="web-page-steps-location-button">
|
<span class="flex w-full items-center justify-between">
|
||||||
<svg
|
<span class="text-description">Table of contents</span>
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
<span
|
||||||
width="16"
|
aria-hidden="true"
|
||||||
height="16"
|
class="toggle-icon {showToc ? 'web-icon-close' : 'icon-menu-alt-4'}"
|
||||||
viewBox="0 0 16 16"
|
></span>
|
||||||
fill="none"
|
|
||||||
>
|
|
||||||
<g clip-path="url(#clip0_1684_10747)">
|
|
||||||
<g filter="url(#filter0_b_1684_10747)">
|
|
||||||
<circle
|
|
||||||
cx="8"
|
|
||||||
cy="8"
|
|
||||||
r="8"
|
|
||||||
fill="url(#paint0_linear_1684_10747)"
|
|
||||||
fill-opacity="0.32"
|
|
||||||
/>
|
|
||||||
<circle
|
|
||||||
cx="8"
|
|
||||||
cy="8"
|
|
||||||
r="7.75"
|
|
||||||
stroke="url(#paint1_linear_1684_10747)"
|
|
||||||
stroke-width="0.5"
|
|
||||||
/>
|
|
||||||
</g>
|
|
||||||
<circle cx="8" cy="7.99219" r="3" fill="white" />
|
|
||||||
</g>
|
|
||||||
<defs>
|
|
||||||
<filter
|
|
||||||
id="filter0_b_1684_10747"
|
|
||||||
x="-200"
|
|
||||||
y="-200"
|
|
||||||
width="416"
|
|
||||||
height="416"
|
|
||||||
filterUnits="userSpaceOnUse"
|
|
||||||
color-interpolation-filters="sRGB"
|
|
||||||
>
|
|
||||||
<feFlood flood-opacity="0" result="BackgroundImageFix" />
|
|
||||||
<feGaussianBlur in="BackgroundImageFix" stdDeviation="100" />
|
|
||||||
<feComposite
|
|
||||||
in2="SourceAlpha"
|
|
||||||
operator="in"
|
|
||||||
result="effect1_backgroundBlur_1684_10747"
|
|
||||||
/>
|
|
||||||
<feBlend
|
|
||||||
mode="normal"
|
|
||||||
in="SourceGraphic"
|
|
||||||
in2="effect1_backgroundBlur_1684_10747"
|
|
||||||
result="shape"
|
|
||||||
/>
|
|
||||||
</filter>
|
|
||||||
<linearGradient
|
|
||||||
id="paint0_linear_1684_10747"
|
|
||||||
x1="2.02105"
|
|
||||||
y1="1.10843"
|
|
||||||
x2="16.3872"
|
|
||||||
y2="17.2901"
|
|
||||||
gradientUnits="userSpaceOnUse"
|
|
||||||
>
|
|
||||||
<stop stop-color="white" stop-opacity="0.4" />
|
|
||||||
<stop offset="1" stop-color="white" stop-opacity="0" />
|
|
||||||
</linearGradient>
|
|
||||||
<linearGradient
|
|
||||||
id="paint1_linear_1684_10747"
|
|
||||||
x1="7.45643"
|
|
||||||
y1="-1.10615"
|
|
||||||
x2="5.53812"
|
|
||||||
y2="17.9973"
|
|
||||||
gradientUnits="userSpaceOnUse"
|
|
||||||
>
|
|
||||||
<stop stop-color="white" stop-opacity="0.16" />
|
|
||||||
<stop offset="1" stop-color="white" stop-opacity="0" />
|
|
||||||
</linearGradient>
|
|
||||||
<clipPath id="clip0_1684_10747">
|
|
||||||
<rect width="16" height="16" fill="white" />
|
|
||||||
</clipPath>
|
|
||||||
</defs>
|
|
||||||
</svg>
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</button>
|
||||||
<TocTree tree={$headingsTree} activeHeadingIdxs={$activeHeadingIdxs} {item} />
|
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
|
||||||
|
{#if showToc}
|
||||||
|
<aside
|
||||||
|
class="web-grid-120-1fr-auto-side"
|
||||||
|
class:web-is-mobile-closed={!showToc}
|
||||||
|
transition:slideFade={{ duration: 300 }}
|
||||||
|
>
|
||||||
|
<div class="web-page-steps">
|
||||||
|
<div
|
||||||
|
class="web-page-steps-location web-is-not-mobile"
|
||||||
|
style="--location:{progress * 100}%;"
|
||||||
|
>
|
||||||
|
<span class="web-page-steps-location-button">
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="16"
|
||||||
|
height="16"
|
||||||
|
viewBox="0 0 16 16"
|
||||||
|
fill="none"
|
||||||
|
>
|
||||||
|
<g clip-path="url(#clip0_1684_10747)">
|
||||||
|
<g filter="url(#filter0_b_1684_10747)">
|
||||||
|
<circle
|
||||||
|
cx="8"
|
||||||
|
cy="8"
|
||||||
|
r="8"
|
||||||
|
fill="url(#paint0_linear_1684_10747)"
|
||||||
|
fill-opacity="0.32"
|
||||||
|
/>
|
||||||
|
<circle
|
||||||
|
cx="8"
|
||||||
|
cy="8"
|
||||||
|
r="7.75"
|
||||||
|
stroke="url(#paint1_linear_1684_10747)"
|
||||||
|
stroke-width="0.5"
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
<circle cx="8" cy="7.99219" r="3" fill="white" />
|
||||||
|
</g>
|
||||||
|
<defs>
|
||||||
|
<filter
|
||||||
|
id="filter0_b_1684_10747"
|
||||||
|
x="-200"
|
||||||
|
y="-200"
|
||||||
|
width="416"
|
||||||
|
height="416"
|
||||||
|
filterUnits="userSpaceOnUse"
|
||||||
|
color-interpolation-filters="sRGB"
|
||||||
|
>
|
||||||
|
<feFlood flood-opacity="0" result="BackgroundImageFix" />
|
||||||
|
<feGaussianBlur in="BackgroundImageFix" stdDeviation="100" />
|
||||||
|
<feComposite
|
||||||
|
in2="SourceAlpha"
|
||||||
|
operator="in"
|
||||||
|
result="effect1_backgroundBlur_1684_10747"
|
||||||
|
/>
|
||||||
|
<feBlend
|
||||||
|
mode="normal"
|
||||||
|
in="SourceGraphic"
|
||||||
|
in2="effect1_backgroundBlur_1684_10747"
|
||||||
|
result="shape"
|
||||||
|
/>
|
||||||
|
</filter>
|
||||||
|
<linearGradient
|
||||||
|
id="paint0_linear_1684_10747"
|
||||||
|
x1="2.02105"
|
||||||
|
y1="1.10843"
|
||||||
|
x2="16.3872"
|
||||||
|
y2="17.2901"
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
>
|
||||||
|
<stop stop-color="white" stop-opacity="0.4" />
|
||||||
|
<stop offset="1" stop-color="white" stop-opacity="0" />
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient
|
||||||
|
id="paint1_linear_1684_10747"
|
||||||
|
x1="7.45643"
|
||||||
|
y1="-1.10615"
|
||||||
|
x2="5.53812"
|
||||||
|
y2="17.9973"
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
>
|
||||||
|
<stop stop-color="white" stop-opacity="0.16" />
|
||||||
|
<stop offset="1" stop-color="white" stop-opacity="0" />
|
||||||
|
</linearGradient>
|
||||||
|
<clipPath id="clip0_1684_10747">
|
||||||
|
<rect width="16" height="16" fill="white" />
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="toc-tree-holder">
|
||||||
|
<TocTree
|
||||||
|
tree={$headingsTree}
|
||||||
|
activeHeadingIdxs={$activeHeadingIdxs}
|
||||||
|
{item}
|
||||||
|
bind:showToc
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
{/if}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.web-mobile-header {
|
||||||
|
top: 5rem;
|
||||||
|
grid-area: side;
|
||||||
|
background: unset;
|
||||||
|
max-height: fit-content;
|
||||||
|
border-block-end: unset;
|
||||||
|
border-block-start: unset;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1024px) {
|
||||||
|
.web-mobile-header {
|
||||||
|
top: 0;
|
||||||
|
margin: 1rem 0;
|
||||||
|
display: block;
|
||||||
|
position: sticky;
|
||||||
|
padding: 1.375rem 0;
|
||||||
|
align-content: center;
|
||||||
|
/** 1.5rem covers main header completely so fragments of it are not shown during scroll */
|
||||||
|
padding-block: 1.5rem;
|
||||||
|
padding-inline: 1.25rem;
|
||||||
|
background: hsl(var(--p-body-bg-color));
|
||||||
|
border-block-end: solid 1px var(--p-mobile-header-border-color);
|
||||||
|
border-block-start: solid 1px var(--p-mobile-header-border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toc-tree-holder {
|
||||||
|
margin-top: 1.5rem;
|
||||||
|
margin-left: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.web-icon-close {
|
||||||
|
max-width: 20px;
|
||||||
|
max-height: 24px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 1280px) {
|
||||||
|
.web-mobile-header {
|
||||||
|
top: 7rem;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 1024px) {
|
||||||
|
.web-mobile-header {
|
||||||
|
border-block-end: unset;
|
||||||
|
border-block-start: unset;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toc-tree-holder {
|
||||||
|
margin-top: unset;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -1,17 +1,25 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { type TableOfContentsItem, type TableOfContentsElements, melt } from '@melt-ui/svelte';
|
import { type TableOfContentsItem, type TableOfContentsElements, melt } from '@melt-ui/svelte';
|
||||||
import { getTocCtx } from './TocRoot.svelte';
|
import { getTocCtx } from './TocRoot.svelte';
|
||||||
|
import { browser } from '$app/environment';
|
||||||
|
|
||||||
export let tree: TableOfContentsItem[] = [];
|
export let tree: TableOfContentsItem[] = [];
|
||||||
export let activeHeadingIdxs: number[];
|
export let activeHeadingIdxs: number[];
|
||||||
export let item: TableOfContentsElements['item'];
|
export let item: TableOfContentsElements['item'];
|
||||||
export let level = 1;
|
export let level = 1;
|
||||||
|
|
||||||
|
export let showToc = true;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
toc: {
|
toc: {
|
||||||
helpers: { isActive }
|
helpers: { isActive }
|
||||||
}
|
}
|
||||||
} = getTocCtx();
|
} = getTocCtx();
|
||||||
|
|
||||||
|
function onItemClick() {
|
||||||
|
const isDesktop = browser ? window.innerWidth >= 1024 : false;
|
||||||
|
if (!isDesktop) showToc = !showToc;
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<ul class="web-page-steps-list text-sub-body font-medium">
|
<ul class="web-page-steps-list text-sub-body font-medium">
|
||||||
@@ -22,6 +30,7 @@
|
|||||||
class:is-selected={$isActive(heading.id)}
|
class:is-selected={$isActive(heading.id)}
|
||||||
href="#{heading.id}"
|
href="#{heading.id}"
|
||||||
use:melt={$item(heading.id)}
|
use:melt={$item(heading.id)}
|
||||||
|
on:click|preventDefault={onItemClick}
|
||||||
>
|
>
|
||||||
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
|
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
|
||||||
{@html heading.node.innerHTML}
|
{@html heading.node.innerHTML}
|
||||||
|
|||||||
@@ -7,10 +7,16 @@
|
|||||||
type EmblaPluginType
|
type EmblaPluginType
|
||||||
} from 'embla-carousel';
|
} from 'embla-carousel';
|
||||||
import { WheelGesturesPlugin } from 'embla-carousel-wheel-gestures';
|
import { WheelGesturesPlugin } from 'embla-carousel-wheel-gestures';
|
||||||
|
import AutoScrollPlugin, { type AutoScrollOptionsType } from 'embla-carousel-auto-scroll';
|
||||||
|
|
||||||
let emblaApi: EmblaCarouselType;
|
let emblaApi: EmblaCarouselType;
|
||||||
|
|
||||||
let options: EmblaOptionsType = {
|
export let showBullets: boolean = true;
|
||||||
|
export let showArrows: boolean = true;
|
||||||
|
export let autoScrollOptions: AutoScrollOptionsType = {
|
||||||
|
active: false
|
||||||
|
};
|
||||||
|
export let options: EmblaOptionsType = {
|
||||||
align: 'center',
|
align: 'center',
|
||||||
skipSnaps: true,
|
skipSnaps: true,
|
||||||
loop: true
|
loop: true
|
||||||
@@ -27,7 +33,7 @@
|
|||||||
else hasNext = false;
|
else hasNext = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
let plugins: EmblaPluginType[] = [WheelGesturesPlugin()];
|
let plugins: EmblaPluginType[] = [WheelGesturesPlugin(), AutoScrollPlugin(autoScrollOptions)];
|
||||||
|
|
||||||
let selectedScrollIndex = 0;
|
let selectedScrollIndex = 0;
|
||||||
const onSelect = (index: number) => {
|
const onSelect = (index: number) => {
|
||||||
@@ -123,15 +129,17 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="embla web-carousel relative overflow-hidden">
|
<div class="embla web-carousel relative overflow-hidden">
|
||||||
{#if hasPrev}
|
{#if showArrows}
|
||||||
<button class="web-carousel-button web-carousel-button-start" on:click={onPrev}>
|
{#if hasPrev}
|
||||||
<span class="web-icon-arrow-left" aria-hidden="true"></span>
|
<button class="web-carousel-button web-carousel-button-start" on:click={onPrev}>
|
||||||
</button>
|
<span class="web-icon-arrow-left" aria-hidden="true"></span>
|
||||||
{/if}
|
</button>
|
||||||
{#if hasNext}
|
{/if}
|
||||||
<button class="web-carousel-button web-carousel-button-end" on:click={onNext}>
|
{#if hasNext}
|
||||||
<span class="web-icon-arrow-right" aria-hidden="true"></span>
|
<button class="web-carousel-button web-carousel-button-end" on:click={onNext}>
|
||||||
</button>
|
<span class="web-icon-arrow-right" aria-hidden="true"></span>
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<div class="embla__viewport" use:embla={{ options, plugins }} on:emblaInit={onEmblaInit}>
|
<div class="embla__viewport" use:embla={{ options, plugins }} on:emblaInit={onEmblaInit}>
|
||||||
@@ -139,19 +147,45 @@
|
|||||||
<slot />
|
<slot />
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="shadow" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="web-carousel-bullets">
|
{#if showBullets}
|
||||||
<ul class="web-carousel-bullets-list">
|
<div class="web-carousel-bullets">
|
||||||
{#each Array.from({ length: emblaApi?.scrollSnapList().length }) as _, i}
|
<ul class="web-carousel-bullets-list">
|
||||||
<li class="web-carousel-bullets-item rounded-full">
|
{#each Array.from({ length: emblaApi?.scrollSnapList().length }) as _, i}
|
||||||
<button
|
<li class="web-carousel-bullets-item rounded-full">
|
||||||
class="web-carousel-bullets-button"
|
<button
|
||||||
class:is-selected={selectedScrollIndex === i}
|
class="web-carousel-bullets-button"
|
||||||
aria-label={`gallery item ${i + 1}`}
|
class:is-selected={selectedScrollIndex === i}
|
||||||
on:click={() => onSelect(i)}
|
aria-label={`gallery item ${i + 1}`}
|
||||||
></button>
|
on:click={() => onSelect(i)}
|
||||||
</li>
|
></button>
|
||||||
{/each}
|
</li>
|
||||||
</ul>
|
{/each}
|
||||||
</div>
|
</ul>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.shadow {
|
||||||
|
width: 100vw;
|
||||||
|
height: 80vh;
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
pointer-events: none;
|
||||||
|
transform: translateX(-50%) translateY(-50%);
|
||||||
|
z-index: 10;
|
||||||
|
backdrop-filter: blur(2px);
|
||||||
|
background-color: hsl(var(--web-color-background) / 50%);
|
||||||
|
mask-composite: intersect;
|
||||||
|
mask-image: linear-gradient(
|
||||||
|
to right,
|
||||||
|
rgba(0, 0, 0, 1) 0%,
|
||||||
|
transparent,
|
||||||
|
transparent,
|
||||||
|
rgba(0, 0, 0, 1) 100%
|
||||||
|
);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -1,25 +1,36 @@
|
|||||||
<li class="slide web-carousel-item">
|
<script lang="ts">
|
||||||
|
import { classNames } from '$lib/utils/classnames';
|
||||||
|
|
||||||
|
let className = '';
|
||||||
|
|
||||||
|
export { className as class };
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<li
|
||||||
|
class={classNames(
|
||||||
|
'slide web-carousel-item mr-2 flex-[0_0_100%] cursor-grab active:cursor-grabbing md:flex-[0_0_50%]',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
>
|
||||||
<div class="embla__slide__number">
|
<div class="embla__slide__number">
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
@use '$scss/abstract' as *;
|
@use '$scss/abstract/functions' as f;
|
||||||
|
|
||||||
.slide {
|
.slide {
|
||||||
cursor: grab;
|
cursor: grab;
|
||||||
|
flex: 0 0 50%;
|
||||||
&:active {
|
min-width: 0;
|
||||||
cursor: grabbing;
|
margin-right: f.pxToRem(16);
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
flex: 0 0 100%;
|
flex: 0 0 100%;
|
||||||
}
|
}
|
||||||
|
&:active {
|
||||||
flex: 0 0 50%;
|
cursor: grabbing;
|
||||||
min-width: 0;
|
}
|
||||||
margin-right: pxToRem(16);
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
78
src/lib/components/product-pages/hero.svelte
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { classNames } from '$lib/utils/classnames';
|
||||||
|
|
||||||
|
type $$Props = {
|
||||||
|
eyebrow: {
|
||||||
|
label: string;
|
||||||
|
icon: string;
|
||||||
|
};
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
cta: {
|
||||||
|
label: string;
|
||||||
|
url: string;
|
||||||
|
};
|
||||||
|
secondaryCta?: {
|
||||||
|
label: string;
|
||||||
|
url: string;
|
||||||
|
};
|
||||||
|
image: {
|
||||||
|
url: string;
|
||||||
|
alt?: string;
|
||||||
|
};
|
||||||
|
mobileImage?: {
|
||||||
|
url: string;
|
||||||
|
alt?: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export let eyebrow: $$Props['eyebrow'];
|
||||||
|
export let title: $$Props['title'];
|
||||||
|
export let description: $$Props['description'];
|
||||||
|
export let cta: $$Props['cta'];
|
||||||
|
export let secondaryCta: $$Props['secondaryCta'] = undefined;
|
||||||
|
export let image: $$Props['image'];
|
||||||
|
export let mobileImage: $$Props['mobileImage'] = undefined;
|
||||||
|
</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"
|
||||||
|
>
|
||||||
|
<div class="mx-auto grid max-w-[75rem] items-center gap-16 md:grid-cols-2">
|
||||||
|
<div class="space-y-8">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<img src={eyebrow.icon} class="size-8" alt="" />
|
||||||
|
<span class="text-micro font-aeonik-fono tracking-loose text-primary uppercase">
|
||||||
|
{eyebrow.label}<span class="web-u-color-text-accent">_</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<h1
|
||||||
|
class="text-display font-aeonik-pro text-primary text-pretty max-sm:max-w-[300px] md:max-w-md"
|
||||||
|
>
|
||||||
|
{title}
|
||||||
|
</h1>
|
||||||
|
<p class="text-description text-secondary text-pretty font-medium">
|
||||||
|
{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">
|
||||||
|
{cta.label}
|
||||||
|
</a>
|
||||||
|
{#if secondaryCta}
|
||||||
|
<a href={secondaryCta.url} class="web-button is-secondary !w-full md:!w-fit">
|
||||||
|
{secondaryCta.label}
|
||||||
|
</a>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<img
|
||||||
|
class={classNames({ 'hidden md:block': mobileImage })}
|
||||||
|
src={image.url}
|
||||||
|
alt={image.alt ?? ''}
|
||||||
|
/>
|
||||||
|
{#if mobileImage}
|
||||||
|
<img class="block md:hidden" src={mobileImage.url} alt={mobileImage.alt ?? ''} />
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
75
src/lib/components/product-pages/product-cards.svelte
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
const allProducts = {
|
||||||
|
messaging: {
|
||||||
|
title: 'Messaging',
|
||||||
|
description: 'Use Appwrite messaging to send email, SMS, and push notifications.',
|
||||||
|
icon: '/images/icons/illustrated/dark/messaging.png',
|
||||||
|
url: '/docs/products/messaging'
|
||||||
|
},
|
||||||
|
auth: {
|
||||||
|
title: 'Auth',
|
||||||
|
description: 'Build secure authentication and manage your users.',
|
||||||
|
icon: '/images/icons/illustrated/dark/auth.png',
|
||||||
|
url: '/docs/products/auth'
|
||||||
|
},
|
||||||
|
functions: {
|
||||||
|
title: 'Functions',
|
||||||
|
description: ' Scale big and unlock limitless potential with Appwrite functions.',
|
||||||
|
icon: '/images/icons/illustrated/dark/functions.png',
|
||||||
|
url: '/docs/products/functions'
|
||||||
|
},
|
||||||
|
databases: {
|
||||||
|
title: 'Databases',
|
||||||
|
description: 'Store and query structured data, ensuring scalable storage.',
|
||||||
|
icon: '/images/icons/illustrated/dark/databases.png',
|
||||||
|
url: '/docs/products/databases'
|
||||||
|
},
|
||||||
|
storage: {
|
||||||
|
title: 'Storage',
|
||||||
|
description: 'Manage your files project, using convenient APIs and utilities.',
|
||||||
|
icon: '/images/icons/illustrated/dark/storage.png',
|
||||||
|
url: '/docs/products/storage'
|
||||||
|
}
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export let exclude:
|
||||||
|
| 'messaging'
|
||||||
|
| 'functions'
|
||||||
|
| 'databases'
|
||||||
|
| 'storage'
|
||||||
|
| 'auth'
|
||||||
|
| 'realtime'
|
||||||
|
| undefined = undefined;
|
||||||
|
|
||||||
|
const products = Object.entries(allProducts)
|
||||||
|
.filter(([key]) => key !== exclude)
|
||||||
|
.map(([_, value]) => value);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<section class="border-smooth border-t py-20 md:py-40">
|
||||||
|
<div class="container">
|
||||||
|
<h4 class="text-label text-primary text-center">Keep exploring our products</h4>
|
||||||
|
<div class="mt-8 grid grid-cols-1 gap-8 md:grid-cols-2 lg:grid-cols-3">
|
||||||
|
{#each products as product}
|
||||||
|
<a
|
||||||
|
class="web-card is-normal"
|
||||||
|
href={product.url}
|
||||||
|
style="background: rgba(255, 255, 255, 0.04);"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="web-u-padding-inline-8 web-u-padding-block-end-8 flex flex-col gap-2"
|
||||||
|
>
|
||||||
|
<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" />
|
||||||
|
</div>
|
||||||
|
<p class="text-sub-body">
|
||||||
|
{product.description}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
74
src/lib/components/product-pages/testimonials.svelte
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
const testimonials = [
|
||||||
|
{
|
||||||
|
name: 'Ryan O’Conner',
|
||||||
|
copy: `The switch to using Appwrite brought infinite value that I'm still discovering today, but a major impact that it made was the amount of time and stress that it saved me as it simply just works.`,
|
||||||
|
image: '/images/testimonials/ryan-oconner.png',
|
||||||
|
title: 'Founder',
|
||||||
|
company: 'K-Collect'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'David Forster',
|
||||||
|
copy: `We really loved working with Appwrite for launching our bootstrapped "Open Mind" App. I am still surprised how easy the implementation into Flutter was.`,
|
||||||
|
image: '/images/testimonials/david-forster.png'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Marius Bolik',
|
||||||
|
copy: `The integrated user authentication and the ease of creating data structures have undoubtedly saved us several weeks' worth of time.`,
|
||||||
|
image: '/images/testimonials/marius-bolik2.png',
|
||||||
|
title: 'CTO',
|
||||||
|
company: 'mySHOEFITTER'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Sergio Ponguta',
|
||||||
|
copy: `Just go for it, don’t think twice. Try Appwrite, and you will love it!`,
|
||||||
|
image: '/images/testimonials/smartbee.png',
|
||||||
|
title: 'Founder',
|
||||||
|
company: 'Smart Bee'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Phil McClusky',
|
||||||
|
copy: 'Just like a Swiss Army Knife, you can choose and use the tools that you need with Appwrite.',
|
||||||
|
image: '/images/testimonials/majik.png',
|
||||||
|
title: 'Developer',
|
||||||
|
company: 'Majik Kids'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="relative w-full max-w-[100vw] overflow-hidden">
|
||||||
|
<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;]"
|
||||||
|
aria-hidden={i !== 0}
|
||||||
|
>
|
||||||
|
{#each testimonials as testimonial}
|
||||||
|
<div
|
||||||
|
class="flex h-fit w-[90vw] flex-col justify-center rounded-2xl bg-white p-6 transition-all md:w-lg"
|
||||||
|
>
|
||||||
|
<p class="text-sub-body text-secondary flex-1 font-medium">
|
||||||
|
{testimonial.copy}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="mt-4 flex items-center gap-3">
|
||||||
|
<img
|
||||||
|
src={testimonial.image}
|
||||||
|
class="size-12 rounded-full"
|
||||||
|
alt="{testimonial.company} Logo"
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<span class="text-secondary text-sub-body block font-medium">
|
||||||
|
{testimonial.name}
|
||||||
|
</span>
|
||||||
|
<span class="text-sub-body text-secondary block"
|
||||||
|
>{testimonial.title} // {testimonial.company}</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -1,3 +1,16 @@
|
|||||||
<span class="block bg-[linear-gradient(6deg,_#f8a1ba,_#fff_35%)] bg-clip-text text-transparent">
|
<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 />
|
<slot />
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -34,7 +34,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
const CTX_KEY = Symbol('docs');
|
const CTX_KEY = Symbol('docs');
|
||||||
|
const TUT_CTX_KEY = Symbol('tut-docs');
|
||||||
export const isInDocs = () => getContext<boolean>(CTX_KEY) ?? false;
|
export const isInDocs = () => getContext<boolean>(CTX_KEY) ?? false;
|
||||||
|
export const isInTutorialDocs = () => getContext<boolean>(TUT_CTX_KEY) ?? false;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
@@ -43,6 +45,7 @@
|
|||||||
import { getContext, setContext } from 'svelte';
|
import { getContext, setContext } from 'svelte';
|
||||||
import { GITHUB_REPO_LINK, GITHUB_STARS } from '$lib/constants';
|
import { GITHUB_REPO_LINK, GITHUB_STARS } from '$lib/constants';
|
||||||
import { PUBLIC_APPWRITE_DASHBOARD } from '$env/static/public';
|
import { PUBLIC_APPWRITE_DASHBOARD } from '$env/static/public';
|
||||||
|
import { page } from '$app/stores';
|
||||||
|
|
||||||
export let variant: DocsLayoutVariant = 'default';
|
export let variant: DocsLayoutVariant = 'default';
|
||||||
export let isReferences = false;
|
export let isReferences = false;
|
||||||
@@ -63,7 +66,9 @@
|
|||||||
showSidenav: false
|
showSidenav: false
|
||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
setContext(CTX_KEY, true);
|
|
||||||
|
const key = $page.route.id?.includes('tutorials') ? TUT_CTX_KEY : CTX_KEY;
|
||||||
|
setContext(key, true);
|
||||||
|
|
||||||
const handleKeydown = (e: KeyboardEvent) => {
|
const handleKeydown = (e: KeyboardEvent) => {
|
||||||
if (e.key === 'Escape' && ($layoutState.showReferences || $layoutState.showSidenav)) {
|
if (e.key === 'Escape' && ($layoutState.showReferences || $layoutState.showSidenav)) {
|
||||||
|
|||||||
@@ -13,14 +13,17 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { scrollToTop } from '$lib/actions/scrollToTop';
|
import { setContext } from 'svelte';
|
||||||
|
import { writable } from 'svelte/store';
|
||||||
import { Feedback } from '$lib/components';
|
import { Feedback } from '$lib/components';
|
||||||
|
import { scrollToTop } from '$lib/actions/scrollToTop';
|
||||||
|
|
||||||
export let title: string;
|
export let title: string;
|
||||||
export let toc: Array<TocItem>;
|
export let toc: Array<TocItem>;
|
||||||
export let back: string | undefined = undefined;
|
export let back: string | undefined = undefined;
|
||||||
export let date: string | undefined = undefined;
|
export let date: string | undefined = undefined;
|
||||||
|
|
||||||
|
const reducedArticleSize = setContext('articleHasNumericBadge', writable(false));
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<main class="contents" id="main">
|
<main class="contents" id="main">
|
||||||
@@ -59,8 +62,9 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="web-article-header-end" />
|
<div class="web-article-header-end" />
|
||||||
</header>
|
</header>
|
||||||
<div class="web-article-content">
|
<div class="web-article-content" class:web-reduced-article-size={$reducedArticleSize}>
|
||||||
<slot />
|
<slot />
|
||||||
|
|
||||||
<Feedback {date} />
|
<Feedback {date} />
|
||||||
</div>
|
</div>
|
||||||
<aside class="web-references-menu ps-6">
|
<aside class="web-references-menu ps-6">
|
||||||
@@ -110,3 +114,12 @@
|
|||||||
</aside>
|
</aside>
|
||||||
</article>
|
</article>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
@media (min-width: 1280px) and (max-width: 1330px) {
|
||||||
|
.web-reduced-article-size {
|
||||||
|
/* original/default is 41.5rem */
|
||||||
|
max-inline-size: 40.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -4,7 +4,8 @@
|
|||||||
import type { Tutorial } from '$markdoc/layouts/Tutorial.svelte';
|
import type { Tutorial } from '$markdoc/layouts/Tutorial.svelte';
|
||||||
import type { TocItem } from './DocsArticle.svelte';
|
import type { TocItem } from './DocsArticle.svelte';
|
||||||
import Heading from '$markdoc/nodes/Heading.svelte';
|
import Heading from '$markdoc/nodes/Heading.svelte';
|
||||||
import { onMount } from 'svelte';
|
import { onMount, tick } from 'svelte';
|
||||||
|
import { page } from '$app/stores';
|
||||||
|
|
||||||
export let toc: Array<TocItem>;
|
export let toc: Array<TocItem>;
|
||||||
export let back: string;
|
export let back: string;
|
||||||
@@ -31,13 +32,66 @@
|
|||||||
|
|
||||||
let slotContent: HTMLElement | null = null;
|
let slotContent: HTMLElement | null = null;
|
||||||
|
|
||||||
|
function scrollToElement(pageHash: string) {
|
||||||
|
const element = document.getElementById(pageHash);
|
||||||
|
if (element) {
|
||||||
|
const offset = 50;
|
||||||
|
const rect = element.getBoundingClientRect();
|
||||||
|
window.scroll({ top: window.scrollY + rect.top - offset });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Due to underlying logic with anchor links & the auto-scroll via hash values in the URL,
|
||||||
|
* we have an issue where if the first item is not scrolled enough it isn't marked as `selected`.
|
||||||
|
*
|
||||||
|
* We do below workaround for the time being without breaking things to scroll to the first item.
|
||||||
|
*/
|
||||||
|
async function preSelectItemOnInit() {
|
||||||
|
await tick();
|
||||||
|
|
||||||
|
if (!$page.url.hash) return;
|
||||||
|
const tocItem = toc.slice(1);
|
||||||
|
|
||||||
|
// no sub-items, return.
|
||||||
|
if (!tocItem.length) return;
|
||||||
|
|
||||||
|
const pageHash = $page.url.hash.replace('#', '');
|
||||||
|
const tocItemHref = tocItem[0].href.replace('#', '');
|
||||||
|
|
||||||
|
if (pageHash !== tocItemHref) return;
|
||||||
|
|
||||||
|
scrollToElement(pageHash);
|
||||||
|
}
|
||||||
|
|
||||||
|
// same issue as above, only happens on the first item.
|
||||||
|
function scrollToItem(parent: TocItem, index: number) {
|
||||||
|
const tocItem = toc.slice(1);
|
||||||
|
|
||||||
|
if (!tocItem.length) return;
|
||||||
|
const tocItemHref = parent.href.replace('#', '');
|
||||||
|
|
||||||
|
const element = document.getElementById(tocItemHref);
|
||||||
|
|
||||||
|
if (index === 0) {
|
||||||
|
scrollToElement(tocItemHref);
|
||||||
|
} else {
|
||||||
|
element?.scrollIntoView();
|
||||||
|
}
|
||||||
|
|
||||||
|
// because we used `preventDefault`.
|
||||||
|
history.pushState(null, '', parent.href);
|
||||||
|
}
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
if (!slotContent) return;
|
if (!slotContent) return;
|
||||||
|
|
||||||
// dynamically modify all `label` headers to `body`.
|
// dynamically modify all `label` headers to `body`.
|
||||||
slotContent.querySelectorAll<HTMLHeadingElement>('h2.web-label').forEach((header) => {
|
slotContent.querySelectorAll<HTMLHeadingElement>('h2.text-label').forEach((header) => {
|
||||||
header.classList.replace('web-label', 'web-main-body-500');
|
header.classList.replace('text-label', 'web-main-body-500');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
preSelectItemOnInit();
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -77,7 +131,9 @@
|
|||||||
/>
|
/>
|
||||||
</a>
|
</a>
|
||||||
{/if}
|
{/if}
|
||||||
<h1 class="web-title lg:-ml-5">{firstStepItem?.title}</h1>
|
<h1 class="web-title {currentStep === 1 ? 'lg:-ml-5' : ''}">
|
||||||
|
{firstStepItem?.title}
|
||||||
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="web-article-header-end" />
|
<div class="web-article-header-end" />
|
||||||
@@ -87,16 +143,18 @@
|
|||||||
<section class="web-article-content-sub-section">
|
<section class="web-article-content-sub-section">
|
||||||
<header class="web-article-content-header">
|
<header class="web-article-content-header">
|
||||||
<span class="web-numeric-badge">{currentStep}</span>
|
<span class="web-numeric-badge">{currentStep}</span>
|
||||||
<Heading level={1} id={currentStepItem.href} step={currentStep}>
|
<div class="tutorial-heading">
|
||||||
{getCorrectTitle(currentStepItem, 1)}
|
<Heading level={1} id={currentStepItem.href} step={currentStep}>
|
||||||
</Heading>
|
{getCorrectTitle(currentStepItem, 1)}
|
||||||
|
</Heading>
|
||||||
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div class="u-padding-block-start-32" bind:this={slotContent}>
|
<div class="web-u-padding-block-start-32" bind:this={slotContent}>
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex justify-between">
|
<div class="web-u-padding-block-start-32 flex justify-between">
|
||||||
{#if prevStep}
|
{#if prevStep}
|
||||||
<a href={prevStep.href} class="web-button is-text previous-step-anchor">
|
<a href={prevStep.href} class="web-button is-text previous-step-anchor">
|
||||||
<span class="icon-cheveron-left" aria-hidden="true" />
|
<span class="icon-cheveron-left" aria-hidden="true" />
|
||||||
@@ -135,6 +193,7 @@
|
|||||||
<ol class="web-references-menu-list">
|
<ol class="web-references-menu-list">
|
||||||
{#each tutorials as tutorial, index}
|
{#each tutorials as tutorial, index}
|
||||||
{@const isCurrentStep = currentStep === tutorial.step}
|
{@const isCurrentStep = currentStep === tutorial.step}
|
||||||
|
{@const absoluteToc = toc.slice(1)}
|
||||||
<li class="web-references-menu-item">
|
<li class="web-references-menu-item">
|
||||||
<a
|
<a
|
||||||
href={tutorial.href}
|
href={tutorial.href}
|
||||||
@@ -148,14 +207,16 @@
|
|||||||
>{index === 0 ? 'Introduction' : tutorial.title}</span
|
>{index === 0 ? 'Introduction' : tutorial.title}</span
|
||||||
>
|
>
|
||||||
</a>
|
</a>
|
||||||
{#if isCurrentStep && toc.length}
|
{#if isCurrentStep && absoluteToc.length}
|
||||||
<ol
|
<ol
|
||||||
class="web-references-menu-list u-margin-block-start-16 u-margin-inline-start-32"
|
class="web-references-menu-list u-margin-block-start-16 u-margin-inline-start-32"
|
||||||
>
|
>
|
||||||
{#each toc.slice(1) as parent}
|
{#each absoluteToc as parent, innerIndex}
|
||||||
<li class="web-references-menu-item">
|
<li class="web-references-menu-item">
|
||||||
<a
|
<a
|
||||||
href={parent.href}
|
href={parent.href}
|
||||||
|
on:click|preventDefault={() =>
|
||||||
|
scrollToItem(parent, innerIndex)}
|
||||||
class="web-references-menu-link is-inner"
|
class="web-references-menu-link is-inner"
|
||||||
class:tutorial-scroll-indicator={parent.selected}
|
class:tutorial-scroll-indicator={parent.selected}
|
||||||
class:is-selected={parent.selected}
|
class:is-selected={parent.selected}
|
||||||
@@ -208,4 +269,49 @@
|
|||||||
background: unset;
|
background: unset;
|
||||||
padding-inline-start: unset;
|
padding-inline-start: unset;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.u-margin-block-start-16 {
|
||||||
|
margin-block-start: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.u-margin-inline-start-32 {
|
||||||
|
margin-inline-start: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.web-references-menu-item:has(.is-selected)::before {
|
||||||
|
/* maintains the distance correctly for the children items */
|
||||||
|
inset-inline-start: -3.55rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Static slider: default slider for each selected link */
|
||||||
|
.web-references-menu-list > .web-references-menu-item > .is-selected::before {
|
||||||
|
content: ' ';
|
||||||
|
position: absolute;
|
||||||
|
inset-block-start: 0;
|
||||||
|
block-size: 1.375rem;
|
||||||
|
inline-size: 0.0625rem;
|
||||||
|
inset-inline-start: -1.3125rem;
|
||||||
|
background-color: hsl(var(--p-references-menu-link-color-text));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hide static slider if any child menu item is selected */
|
||||||
|
.web-references-menu-list
|
||||||
|
> .web-references-menu-item:has(.web-references-menu-list .is-selected)
|
||||||
|
> .is-selected::before {
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Transparent slider for selected child items because we use parent level */
|
||||||
|
.web-references-menu-list
|
||||||
|
> .web-references-menu-item
|
||||||
|
> .web-references-menu-list
|
||||||
|
> .web-references-menu-item
|
||||||
|
> .is-selected::before {
|
||||||
|
content: '';
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.tutorial-heading h2) {
|
||||||
|
margin-bottom: unset;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,11 +1,6 @@
|
|||||||
<script lang="ts" context="module">
|
<script lang="ts" context="module">
|
||||||
import { writable } from 'svelte/store';
|
import { writable } from 'svelte/store';
|
||||||
|
|
||||||
export type NavLink = {
|
|
||||||
label: string;
|
|
||||||
href: string;
|
|
||||||
showBadge?: boolean;
|
|
||||||
};
|
|
||||||
export const isHeaderHidden = writable(false);
|
export const isHeaderHidden = writable(false);
|
||||||
export const isMobileNavOpen = writable(false);
|
export const isMobileNavOpen = writable(false);
|
||||||
const initialized = writable(false);
|
const initialized = writable(false);
|
||||||
@@ -21,12 +16,13 @@
|
|||||||
import { addEventListener } from '@melt-ui/svelte/internal/helpers';
|
import { addEventListener } from '@melt-ui/svelte/internal/helpers';
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
import { page } from '$app/stores';
|
import { page } from '$app/stores';
|
||||||
import { classNames } from '$lib/utils/classnames';
|
import ProductsSubmenu from '$lib/components/ProductsSubmenu.svelte';
|
||||||
|
import ProductsMobileSubmenu from '$lib/components/ProductsMobileSubmenu.svelte';
|
||||||
import { PUBLIC_APPWRITE_DASHBOARD } from '$env/static/public';
|
import { PUBLIC_APPWRITE_DASHBOARD } from '$env/static/public';
|
||||||
import AnnouncementBanner from '$lib/components/AnnouncementBanner.svelte';
|
import AnnouncementBanner from '$lib/components/AnnouncementBanner.svelte';
|
||||||
import InitBanner from '$lib/components/InitBanner.svelte';
|
import InitBanner from '$lib/components/InitBanner.svelte';
|
||||||
import Button from '$lib/components/ui/Button.svelte';
|
|
||||||
import { trackEvent } from '$lib/actions/analytics';
|
import { trackEvent } from '$lib/actions/analytics';
|
||||||
|
import MainNav, { type NavLink } from '$lib/components/MainNav.svelte';
|
||||||
|
|
||||||
export let omitMainId = false;
|
export let omitMainId = false;
|
||||||
let theme: 'light' | 'dark' | null = 'dark';
|
let theme: 'light' | 'dark' | null = 'dark';
|
||||||
@@ -102,6 +98,11 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
let navLinks: NavLink[] = [
|
let navLinks: NavLink[] = [
|
||||||
|
{
|
||||||
|
label: 'Products',
|
||||||
|
submenu: ProductsSubmenu,
|
||||||
|
mobileSubmenu: ProductsMobileSubmenu
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: 'Docs',
|
label: 'Docs',
|
||||||
href: '/docs'
|
href: '/docs'
|
||||||
@@ -240,23 +241,7 @@
|
|||||||
width="130"
|
width="130"
|
||||||
/>
|
/>
|
||||||
</a>
|
</a>
|
||||||
<nav class="web-main-header-nav" aria-label="Main">
|
<MainNav initialized={$initialized} links={navLinks} />
|
||||||
<ul class="web-main-header-nav-list">
|
|
||||||
{#each navLinks as navLink}
|
|
||||||
<li class="web-main-header-nav-item text-primary hover:text-accent">
|
|
||||||
<a
|
|
||||||
class={classNames(
|
|
||||||
'data-[badge]:after:animate-scale-in data-[badge]:relative data-[badge]:after:absolute data-[badge]:after:size-1.5 data-[badge]:after:translate-full data-[badge]:after:rounded-full'
|
|
||||||
)}
|
|
||||||
href={navLink.href}
|
|
||||||
data-initialized={$initialized ? '' : undefined}
|
|
||||||
data-badge={navLink.showBadge ? '' : undefined}
|
|
||||||
>{navLink.label}
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
{/each}
|
|
||||||
</ul>
|
|
||||||
</nav>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="web-main-header-end">
|
<div class="web-main-header-end">
|
||||||
<a
|
<a
|
||||||
@@ -303,25 +288,4 @@
|
|||||||
.is-special-padding {
|
.is-special-padding {
|
||||||
padding-inline: clamp(1.25rem, 4vw, 120rem);
|
padding-inline: clamp(1.25rem, 4vw, 120rem);
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-badge] {
|
|
||||||
position: relative;
|
|
||||||
|
|
||||||
&::after {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
background-color: hsl(var(--web-color-accent));
|
|
||||||
border-radius: 100%;
|
|
||||||
width: 0.375rem;
|
|
||||||
height: 0.375rem;
|
|
||||||
|
|
||||||
inset-block-start: -2px;
|
|
||||||
inset-inline-end: -4px;
|
|
||||||
translate: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:not([data-initialized])::after {
|
|
||||||
animation: scale-in 0.2s ease-out;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<a
|
<a
|
||||||
class="web-side-nav-button"
|
class="web-side-nav-button flex size-10 w-full items-center whitespace-nowrap rounded-lg p-2"
|
||||||
class:is-selected={$page.url?.pathname === groupItem.href}
|
class:is-selected={$page.url?.pathname === groupItem.href}
|
||||||
href={groupItem.href}
|
href={groupItem.href}
|
||||||
target={groupItem.openInNewTab ? '_blank' : '_self'}
|
target={groupItem.openInNewTab ? '_blank' : '_self'}
|
||||||
|
|||||||
@@ -57,6 +57,15 @@ export enum Platform {
|
|||||||
ServerRest = 'server-rest'
|
ServerRest = 'server-rest'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum Framework {
|
||||||
|
NextJs = 'Next.js',
|
||||||
|
SvelteKit = 'SvelteKit',
|
||||||
|
VueJs = 'Vue.js',
|
||||||
|
Nuxt3 = 'Nuxt3',
|
||||||
|
Astro = 'Astro',
|
||||||
|
Remix = 'Remix'
|
||||||
|
}
|
||||||
|
|
||||||
export const platformMap: Record<Language | string, string> = {
|
export const platformMap: Record<Language | string, string> = {
|
||||||
[Platform.ClientApple]: 'Apple',
|
[Platform.ClientApple]: 'Apple',
|
||||||
[Platform.ClientFlutter]: 'Flutter',
|
[Platform.ClientFlutter]: 'Flutter',
|
||||||
|
|||||||
@@ -199,28 +199,19 @@ export function getSchema(id: string, api: OpenAPIV3.Document): OpenAPIV3.Schema
|
|||||||
}
|
}
|
||||||
|
|
||||||
const specs = import.meta.glob(
|
const specs = import.meta.glob(
|
||||||
'$appwrite/app/config/specs/open-api3*-(client|server|console).json',
|
'$appwrite/app/config/specs/open-api3*-(client|server|console).json'
|
||||||
{
|
|
||||||
query: '?raw',
|
|
||||||
import: 'default'
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
async function getSpec(version: string, platform: string) {
|
|
||||||
|
export async function getApi(version: string, platform: string): Promise<OpenAPIV3.Document> {
|
||||||
const isClient = platform.startsWith('client-');
|
const isClient = platform.startsWith('client-');
|
||||||
const isServer = platform.startsWith('server-');
|
const isServer = platform.startsWith('server-');
|
||||||
const target = `/node_modules/@appwrite.io/repo/app/config/specs/open-api3-${version}-${
|
const target = `/node_modules/@appwrite.io/repo/app/config/specs/open-api3-${version}-${
|
||||||
isServer ? 'server' : isClient ? 'client' : 'console'
|
isServer ? 'server' : isClient ? 'client' : 'console'
|
||||||
}.json`;
|
}.json`;
|
||||||
|
|
||||||
return specs[target]();
|
return specs[target]();
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getApi(version: string, platform: string): Promise<OpenAPIV3.Document> {
|
|
||||||
const raw = await getSpec(version, platform);
|
|
||||||
const api = JSON.parse(raw);
|
|
||||||
|
|
||||||
return api;
|
|
||||||
}
|
|
||||||
|
|
||||||
const descriptions = import.meta.glob(
|
const descriptions = import.meta.glob(
|
||||||
'/src/routes/docs/references/[version]/[platform]/[service]/descriptions/*.md',
|
'/src/routes/docs/references/[version]/[platform]/[service]/descriptions/*.md',
|
||||||
{
|
{
|
||||||
@@ -236,9 +227,7 @@ export async function getDescription(service: string): Promise<string> {
|
|||||||
throw new Error('Missing service description');
|
throw new Error('Missing service description');
|
||||||
}
|
}
|
||||||
|
|
||||||
const description = descriptions[target]();
|
return descriptions[target]();
|
||||||
|
|
||||||
return description;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getService(
|
export async function getService(
|
||||||
@@ -327,9 +316,11 @@ export async function getService(
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const demo = await examples[path]();
|
||||||
|
|
||||||
data.methods.push({
|
data.methods.push({
|
||||||
id: operation['x-appwrite'].method,
|
id: operation['x-appwrite'].method,
|
||||||
demo: await examples[path](),
|
demo: demo ?? '',
|
||||||
title: operation.summary ?? '',
|
title: operation.summary ?? '',
|
||||||
description: operation.description ?? '',
|
description: operation.description ?? '',
|
||||||
parameters: parameters ?? [],
|
parameters: parameters ?? [],
|
||||||
@@ -370,9 +361,9 @@ export function resolveReference(
|
|||||||
|
|
||||||
export const generateExample = (
|
export const generateExample = (
|
||||||
schema: OpenAPIV3.SchemaObject,
|
schema: OpenAPIV3.SchemaObject,
|
||||||
api: OpenAPIV3.Document<{}>,
|
api: OpenAPIV3.Document<object>,
|
||||||
modelType: ModelType = ModelType.REST
|
modelType: ModelType = ModelType.REST
|
||||||
): Object => {
|
): object => {
|
||||||
const properties = Object.keys(schema.properties ?? {}).map((key) => {
|
const properties = Object.keys(schema.properties ?? {}).map((key) => {
|
||||||
const name = key;
|
const name = key;
|
||||||
const fields = schema.properties?.[key];
|
const fields = schema.properties?.[key];
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import type { Tutorial } from '$markdoc/layouts/Tutorial.svelte';
|
|||||||
|
|
||||||
export function globToTutorial(data: { tutorials: Record<string, unknown>; pathname: string }) {
|
export function globToTutorial(data: { tutorials: Record<string, unknown>; pathname: string }) {
|
||||||
let isFound = false;
|
let isFound = false;
|
||||||
let difficulty, readtime;
|
let difficulty: string | undefined, readtime: string | undefined;
|
||||||
|
|
||||||
return Object.entries(data.tutorials)
|
return Object.entries(data.tutorials)
|
||||||
.map(([filepath, tutorial]) => {
|
.map(([filepath, tutorial]) => {
|
||||||
|
|||||||
17
src/lib/utils/utm.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
export function getReferrerAndUtmSource() {
|
||||||
|
if (sessionStorage) {
|
||||||
|
let values = {};
|
||||||
|
if (sessionStorage.getItem('utmReferral')) {
|
||||||
|
values = { ...values, utmReferral: sessionStorage.getItem('utmReferral') };
|
||||||
|
}
|
||||||
|
if (sessionStorage.getItem('utmSource')) {
|
||||||
|
values = { ...values, utmSource: sessionStorage.getItem('utmSource') };
|
||||||
|
}
|
||||||
|
if (sessionStorage.getItem('utmMedium')) {
|
||||||
|
values = { ...values, utmMedium: sessionStorage.getItem('utmMedium') };
|
||||||
|
}
|
||||||
|
|
||||||
|
return values;
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
@@ -45,11 +45,11 @@
|
|||||||
<span>Back to blog</span>
|
<span>Back to blog</span>
|
||||||
</a>
|
</a>
|
||||||
<div class="web-category-header mt-6">
|
<div class="web-category-header mt-6">
|
||||||
<div class="web-category-header-content">
|
<div class="flex flex-col justify-between gap-6 md:flex-row md:items-center">
|
||||||
<h1 class="text-display font-aeonik-pro text-primary">
|
<h1 class="text-display font-aeonik-pro text-primary">
|
||||||
{name}
|
{name}
|
||||||
</h1>
|
</h1>
|
||||||
<p class="web-category-header-description text-description">
|
<p class="text-secondary text-description">
|
||||||
{description}
|
{description}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
import { Root, Slide } from '$lib/components/carousel';
|
import { Root, Slide } from '$lib/components/carousel';
|
||||||
import FooterNav from '$lib/components/FooterNav.svelte';
|
import FooterNav from '$lib/components/FooterNav.svelte';
|
||||||
import MainFooter from '$lib/components/MainFooter.svelte';
|
import MainFooter from '$lib/components/MainFooter.svelte';
|
||||||
import ProductsGrid from '$lib/components/ProductsGrid.svelte';
|
|
||||||
import { Main } from '$lib/layouts';
|
import { Main } from '$lib/layouts';
|
||||||
import { DEFAULT_HOST } from '$lib/utils/metadata';
|
import { DEFAULT_HOST } from '$lib/utils/metadata';
|
||||||
import type { Integration } from '$routes/integrations/+page';
|
import type { Integration } from '$routes/integrations/+page';
|
||||||
@@ -44,7 +43,7 @@
|
|||||||
class="web-u-sep-block-end pb-0"
|
class="web-u-sep-block-end pb-0"
|
||||||
style="background-color:rgba(23, 23, 26, 1); margin-block-end: 2.5rem"
|
style="background-color:rgba(23, 23, 26, 1); margin-block-end: 2.5rem"
|
||||||
>
|
>
|
||||||
<div class="container">
|
<div class="container dark">
|
||||||
<div class="web-integrations-top-section">
|
<div class="web-integrations-top-section">
|
||||||
<div class="web-carousel-wrapper">
|
<div class="web-carousel-wrapper">
|
||||||
<a href="/integrations" class="web-button is-text mb-12">
|
<a href="/integrations" class="web-button is-text mb-12">
|
||||||
@@ -181,13 +180,8 @@
|
|||||||
</Main>
|
</Main>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
@use '$scss/abstract' as *;
|
@use '$scss/abstract/functions' as f;
|
||||||
|
@use '$scss/abstract/variables/devices';
|
||||||
.cta {
|
|
||||||
min-height: pxToRem(560);
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.web-pre-footer-bg {
|
.web-pre-footer-bg {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
@@ -199,24 +193,15 @@
|
|||||||
max-inline-size: unset;
|
max-inline-size: unset;
|
||||||
max-block-size: unset;
|
max-block-size: unset;
|
||||||
}
|
}
|
||||||
/* more tha 9 items */
|
|
||||||
|
|
||||||
.l-side-column {
|
|
||||||
display: flex;
|
|
||||||
gap: pxToRem(16);
|
|
||||||
@media #{$break1} {
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.l-grid-2-1 {
|
.l-grid-2-1 {
|
||||||
@media #{$break1} {
|
@media #{devices.$break1} {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
@media #{$break2open} {
|
@media #{devices.$break2open} {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: pxToRem(64);
|
gap: f.pxToRem(64);
|
||||||
grid-template-columns: repeat(12, 1fr);
|
grid-template-columns: repeat(12, 1fr);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -72,23 +72,14 @@
|
|||||||
<header class="web-grid-120-1fr-auto-header">
|
<header class="web-grid-120-1fr-auto-header">
|
||||||
<h1 class="text-title font-aeonik-pro text-primary">{title}</h1>
|
<h1 class="text-title font-aeonik-pro text-primary">{title}</h1>
|
||||||
</header>
|
</header>
|
||||||
<button
|
|
||||||
class="toc-btn web-u-padding-20 web-u-margin-inline-20-negative text-primary web-is-only-mobile web-u-sep-block
|
<TocNav bind:showToc />
|
||||||
web-u-filter-blur-8 sticky mt-6 flex
|
|
||||||
w-full items-center justify-between"
|
|
||||||
style:--inset-block-start="4.5rem"
|
|
||||||
style:inline-size="100vw"
|
|
||||||
style:background-color="hsl(var(--p-body-bg-color) / 0.1)"
|
|
||||||
style:translate="0 {$isHeaderHidden ? '-4.5rem' : '0'}"
|
|
||||||
style:z-index="1"
|
|
||||||
on:click={() => (showToc = !showToc)}
|
|
||||||
>
|
|
||||||
<span class="text-description">Table of contents</span>
|
|
||||||
<span class="icon-menu-alt-4" aria-hidden="true" />
|
|
||||||
</button>
|
|
||||||
<TocNav />
|
|
||||||
<main class="web-grid-120-1fr-auto-main /web-is-mobile-closed" id="main">
|
<main class="web-grid-120-1fr-auto-main /web-is-mobile-closed" id="main">
|
||||||
<div class="web-content is-count-headers" class:web-is-mobile-closed={showToc}>
|
<div
|
||||||
|
class="web-content is-count-headers"
|
||||||
|
class:web-is-mobile-closed={showToc && !showToc}
|
||||||
|
>
|
||||||
<!-- svelte-ignore a11y-hidden -->
|
<!-- svelte-ignore a11y-hidden -->
|
||||||
<h2 aria-hidden="true">Introduction</h2>
|
<h2 aria-hidden="true">Introduction</h2>
|
||||||
<slot />
|
<slot />
|
||||||
@@ -107,7 +98,20 @@
|
|||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.toc-btn {
|
@media (max-width: 768px) {
|
||||||
transition: translate 0.3s ease;
|
.container {
|
||||||
|
padding-left: 0;
|
||||||
|
padding-right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
header {
|
||||||
|
padding-block-end: unset;
|
||||||
|
}
|
||||||
|
|
||||||
|
header,
|
||||||
|
main {
|
||||||
|
padding-left: var(--spacing-5, 1.25rem);
|
||||||
|
padding-right: var(--spacing-5, 1.25rem);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -236,7 +236,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="web-u-sep-block-start py-10">
|
<div class="web-u-sep-block-start pt-10">
|
||||||
<div class="web-big-padding-section-level-2">
|
<div class="web-big-padding-section-level-2">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<h3 class="text-label text-primary">Read next</h3>
|
<h3 class="text-label text-primary">Read next</h3>
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
import { copy } from '$lib/utils/copy';
|
import { copy } from '$lib/utils/copy';
|
||||||
import type { CodeContext } from '../tags/MultiCode.svelte';
|
import type { CodeContext } from '../tags/MultiCode.svelte';
|
||||||
import { melt } from '@melt-ui/svelte';
|
import { melt } from '@melt-ui/svelte';
|
||||||
|
import { isInTutorialDocs } from '$lib/layouts/Docs.svelte';
|
||||||
|
|
||||||
export let content: string;
|
export let content: string;
|
||||||
export let toCopy: string | undefined = undefined;
|
export let toCopy: string | undefined = undefined;
|
||||||
@@ -15,6 +16,7 @@
|
|||||||
export let withLineNumbers = true;
|
export let withLineNumbers = true;
|
||||||
export let badge: string | null = null;
|
export let badge: string | null = null;
|
||||||
|
|
||||||
|
const inTutorialDocs = isInTutorialDocs();
|
||||||
const insideMultiCode = hasContext('multi-code');
|
const insideMultiCode = hasContext('multi-code');
|
||||||
const selected = insideMultiCode ? getContext<CodeContext>('multi-code').selected : null;
|
const selected = insideMultiCode ? getContext<CodeContext>('multi-code').selected : null;
|
||||||
|
|
||||||
@@ -61,7 +63,11 @@
|
|||||||
{@html result}
|
{@html result}
|
||||||
{/if}
|
{/if}
|
||||||
{:else}
|
{:else}
|
||||||
<section class="dark web-code-snippet" aria-label="code-snippet panel">
|
<section
|
||||||
|
class="dark web-code-snippet"
|
||||||
|
class:no-top-margin={inTutorialDocs}
|
||||||
|
aria-label="code-snippet panel"
|
||||||
|
>
|
||||||
<header class="web-code-snippet-header">
|
<header class="web-code-snippet-header">
|
||||||
<div class="web-code-snippet-header-start">
|
<div class="web-code-snippet-header-start">
|
||||||
{#if badgeValue}
|
{#if badgeValue}
|
||||||
@@ -98,3 +104,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.no-top-margin {
|
||||||
|
margin-top: unset !important;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -6,7 +6,13 @@
|
|||||||
|
|
||||||
const isExternal = ['http://', 'https://'].some((prefix) => href.startsWith(prefix));
|
const isExternal = ['http://', 'https://'].some((prefix) => href.startsWith(prefix));
|
||||||
const target = isExternal ? '_blank' : undefined;
|
const target = isExternal ? '_blank' : undefined;
|
||||||
const rel = isExternal ? 'noopener nofollow' : undefined;
|
|
||||||
|
const doFollow = href.includes('?dofollow=true') || href.includes('&dofollow=true');
|
||||||
|
if (doFollow) {
|
||||||
|
href = href.replace(/[?&]dofollow=true/g, '').replace(/[?&]$/, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
const rel = isExternal ? `noopener${doFollow ? '' : ' nofollow'}` : undefined;
|
||||||
|
|
||||||
const inChangelog = isInChangelog();
|
const inChangelog = isInChangelog();
|
||||||
|
|
||||||
|
|||||||
@@ -3,10 +3,11 @@
|
|||||||
import { isInPolicy } from '$markdoc/layouts/Policy.svelte';
|
import { isInPolicy } from '$markdoc/layouts/Policy.svelte';
|
||||||
import { getContext, hasContext } from 'svelte';
|
import { getContext, hasContext } from 'svelte';
|
||||||
import { isInTable } from './Table.svelte';
|
import { isInTable } from './Table.svelte';
|
||||||
import { isInDocs } from '$lib/layouts/Docs.svelte';
|
import { isInDocs, isInTutorialDocs } from '$lib/layouts/Docs.svelte';
|
||||||
|
|
||||||
const noParagraph = hasContext('no-paragraph') ? getContext('no-paragraph') : false;
|
const noParagraph = hasContext('no-paragraph') ? getContext('no-paragraph') : false;
|
||||||
const inDocs = isInDocs();
|
const inDocs = isInDocs();
|
||||||
|
const inTutorialDocs = isInTutorialDocs();
|
||||||
const inPolicy = isInPolicy();
|
const inPolicy = isInPolicy();
|
||||||
const inChangelog = isInChangelog();
|
const inChangelog = isInChangelog();
|
||||||
const inTable = isInTable();
|
const inTable = isInTable();
|
||||||
@@ -18,6 +19,7 @@
|
|||||||
if (inDocs) return 'text-paragraph-md mb-8';
|
if (inDocs) return 'text-paragraph-md mb-8';
|
||||||
if (inPolicy) return 'text-paragraph-md mb-4';
|
if (inPolicy) return 'text-paragraph-md mb-4';
|
||||||
if (inTable) return 'text-paragraph-md';
|
if (inTable) return 'text-paragraph-md';
|
||||||
|
if (inTutorialDocs) return 'text-paragraph-md mb-2';
|
||||||
if (inChangelog) return 'text-paragraph-lg mb-4 font-normal';
|
if (inChangelog) return 'text-paragraph-lg mb-4 font-normal';
|
||||||
return 'text-paragraph-lg mb-8';
|
return 'text-paragraph-lg mb-8';
|
||||||
})();
|
})();
|
||||||
|
|||||||
@@ -5,12 +5,6 @@
|
|||||||
export let url: string = PUBLIC_APPWRITE_DASHBOARD;
|
export let url: string = PUBLIC_APPWRITE_DASHBOARD;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="call-to-action">
|
<div class="py-12">
|
||||||
<a href={url} class="web-button">{label}</a>
|
<a href={url} class="web-button">{label}</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style lang="scss">
|
|
||||||
.call-to-action {
|
|
||||||
margin: 48px 0;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
@@ -1,9 +1,15 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { type Writable } from 'svelte/store';
|
||||||
import Heading from '../nodes/Heading.svelte';
|
import Heading from '../nodes/Heading.svelte';
|
||||||
|
import { getContext, hasContext } from 'svelte';
|
||||||
|
|
||||||
export let id: string;
|
export let id: string;
|
||||||
export let step: number;
|
export let step: number;
|
||||||
export let title: string;
|
export let title: string;
|
||||||
|
|
||||||
|
if (hasContext('articleHasNumericBadge')) {
|
||||||
|
getContext<Writable<boolean>>('articleHasNumericBadge').set(true);
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<section class="web-article-content-section is-with-line">
|
<section class="web-article-content-section is-with-line">
|
||||||
|
|||||||
@@ -7,6 +7,8 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import Select from '$lib/components/Select.svelte';
|
||||||
|
import { classNames } from '$lib/utils/classnames';
|
||||||
import { createTabs } from '@melt-ui/svelte';
|
import { createTabs } from '@melt-ui/svelte';
|
||||||
import { setContext } from 'svelte';
|
import { setContext } from 'svelte';
|
||||||
import { writable } from 'svelte/store';
|
import { writable } from 'svelte/store';
|
||||||
@@ -27,20 +29,82 @@
|
|||||||
|
|
||||||
<div class="web-card is-normal mt-4" {...$root} use:root>
|
<div class="web-card is-normal mt-4" {...$root} use:root>
|
||||||
<div
|
<div
|
||||||
class="tabs flex gap-4 overflow-scroll"
|
class="tabs flex items-center gap-4 overflow-scroll"
|
||||||
style="scrollbar-width: none; -ms-overflow-style: none;"
|
style="scrollbar-width: none; -ms-overflow-style: none;"
|
||||||
>
|
>
|
||||||
<ul class="tabs-list flex items-center gap-4" {...$list} use:list>
|
<ul class="tabs-list hidden items-center gap-4 sm:flex" {...$list} use:list>
|
||||||
{#each $ctx.triggers.entries() as [id, title]}
|
{#each Array.from($ctx.triggers.entries()).slice(0, 7) as [id, title]}
|
||||||
<li class="tabs-item rounded-t-[0.625rem] hover:bg-white/4">
|
<li
|
||||||
|
class="tabs-item rounded-t-[0.625rem] text-center hover:bg-white/4"
|
||||||
|
class:text-[var(--color-primary)]={$value === id}
|
||||||
|
>
|
||||||
<button
|
<button
|
||||||
class="tabs-button cursor-pointer bg-clip-padding py-[0.625rem] px-1 font-light outline-none"
|
class={classNames(
|
||||||
class:is-selected={$value === id}
|
'tabs-button relative cursor-pointer bg-clip-padding py-[0.625rem] px-1 font-light outline-none',
|
||||||
|
'after:relative after:top-1 after:bottom-0 after:block after:h-px after:transition-all',
|
||||||
|
{
|
||||||
|
'after:bg-[var(--color-primary)]': $value === id
|
||||||
|
}
|
||||||
|
)}
|
||||||
{...$trigger(id)}
|
{...$trigger(id)}
|
||||||
use:trigger>{title}</button
|
use:trigger>{title}</button
|
||||||
>
|
>
|
||||||
</li>
|
</li>
|
||||||
{/each}
|
{/each}
|
||||||
|
{#if Array.from($ctx.triggers.entries()).slice(7, Array.from($ctx.triggers.entries()).length - 1).length}
|
||||||
|
{@const entries = Array.from($ctx.triggers.entries())}
|
||||||
|
{@const desktopOptions = entries.slice(7, entries.length - 1)}
|
||||||
|
|
||||||
|
<li>
|
||||||
|
<Select
|
||||||
|
initialLabel="More"
|
||||||
|
options={desktopOptions.map(([value, label]) => {
|
||||||
|
return {
|
||||||
|
value,
|
||||||
|
label
|
||||||
|
};
|
||||||
|
})}
|
||||||
|
bind:value={$value}
|
||||||
|
/>
|
||||||
|
</li>
|
||||||
|
{/if}
|
||||||
|
</ul>
|
||||||
|
<ul class="tabs-list flex items-center gap-4 sm:hidden" {...$list} use:list>
|
||||||
|
{#each Array.from($ctx.triggers.entries()).slice(0, 3) as [id, title]}
|
||||||
|
<li
|
||||||
|
class="tabs-item rounded-t-[0.625rem] text-center hover:bg-white/4"
|
||||||
|
class:text-[var(--color-primary)]={$value === id}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class={classNames(
|
||||||
|
'tabs-button relative cursor-pointer bg-clip-padding py-[0.625rem] px-1 font-light outline-none',
|
||||||
|
'after:relative after:top-1 after:bottom-0 after:block after:h-px after:transition-all',
|
||||||
|
{
|
||||||
|
'after:bg-[var(--color-primary)]': $value === id
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
{...$trigger(id)}
|
||||||
|
use:trigger>{title}</button
|
||||||
|
>
|
||||||
|
</li>
|
||||||
|
{/each}
|
||||||
|
{#if Array.from($ctx.triggers.entries()).slice(3, Array.from($ctx.triggers.entries()).length - 1).length}
|
||||||
|
{@const entries = Array.from($ctx.triggers.entries())}
|
||||||
|
{@const desktopOptions = entries.slice(3, entries.length - 1)}
|
||||||
|
|
||||||
|
<li>
|
||||||
|
<Select
|
||||||
|
initialLabel="More"
|
||||||
|
options={desktopOptions.map(([value, label]) => {
|
||||||
|
return {
|
||||||
|
value,
|
||||||
|
label
|
||||||
|
};
|
||||||
|
})}
|
||||||
|
bind:value={$value}
|
||||||
|
/>
|
||||||
|
</li>
|
||||||
|
{/if}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<slot />
|
<slot />
|
||||||
|
|||||||
@@ -1,3 +1,31 @@
|
|||||||
|
# Persistence {% #persistence %}
|
||||||
|
|
||||||
|
Appwrite handles the persistence of the session in a consistent way across SDKs. After authenticating with an SDK, the SDK will persist the session so that the user will not need to log in again the next time they open the app. The mechanism for persistence depends on the SDK.
|
||||||
|
|
||||||
|
{% info title="Best Practice" %}
|
||||||
|
Only keep user sessions active as long as needed and maintain exactly **one** instance of the Client SDK in your app to avoid conflicting session data.
|
||||||
|
{% /info %}
|
||||||
|
|
||||||
|
| {% width=70 %} | Framework {% width=120 %} | Storage method |
|
||||||
|
| :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :-----------------------: | :--------------------------------------------------------------------------------------------------: |
|
||||||
|
| {% only_dark %}{% icon_image src="/images/platforms/dark/javascript.svg" alt="Javascript logo" size="m" /%}{% /only_dark %}{% only_light %}{% icon_image src="/images/platforms/javascript.svg" alt="Javascript logo" size="m" /%}{% /only_light %} | Web | Uses a secure session cookie and falls back to local storage when a session cookie is not available. |
|
||||||
|
| {% only_dark %}{% icon_image src="/images/platforms/dark/flutter.svg" alt="Javascript logo" size="m" /%}{% /only_dark %}{% only_light %}{% icon_image src="/images/platforms/flutter.svg" alt="Javascript logo" size="m" /%}{% /only_light %} | Flutter | Uses a session cookie stored in Application Documents through the **path_provider** package. |
|
||||||
|
| {% only_dark %}{% icon_image src="/images/platforms/dark/apple.svg" alt="Javascript logo" size="m" /%}{% /only_dark %}{% only_light %}{% icon_image src="/images/platforms/apple.svg" alt="Javascript logo" size="m" /%}{% /only_light %} | Apple | Uses a session cookie stored in **UserDefaults**. |
|
||||||
|
| {% only_dark %}{% icon_image src="/images/platforms/dark/android.svg" alt="Javascript logo" size="m" /%}{% /only_dark %}{% only_light %}{% icon_image src="/images/platforms/android.svg" alt="Javascript logo" size="m" /%}{% /only_light %} | Android | Uses a session cookie stored in **SharedPreferences**. |
|
||||||
|
|
||||||
|
# Session limits {% #session-limits %}
|
||||||
|
|
||||||
|
In Appwrite versions 1.2 and above, you can limit the number of active sessions created per user to prevent the accumulation of unused but active sessions. New sessions created by the same user past the session limit delete the oldest session.
|
||||||
|
|
||||||
|
You can change the session limit in the **Security** tab of the Auth Service in your Appwrite Console. The default session limit is 10 with a maximum configurable limit of 100.
|
||||||
|
|
||||||
|
# Permissions {% #permissions %}
|
||||||
|
|
||||||
|
Security is very important to protect users' data and privacy.
|
||||||
|
Appwrite uses a [permissions model](/docs/advanced/platform/permissions) coupled with user sessions to ensure users need correct permissions to access resources.
|
||||||
|
With all Appwrite services, including databases and storage, access is granted at the collection, bucket, document, or file level.
|
||||||
|
These permissions are enforced for client SDKs and server SDKs when using JWT, but are ignored when using a server SDK with an API key.
|
||||||
|
|
||||||
# Password history {% #password-history %}
|
# Password history {% #password-history %}
|
||||||
|
|
||||||
Password history prevents users from reusing recent passwords. This protects user accounts from security risks by enforcing a new password every time it's changed.
|
Password history prevents users from reusing recent passwords. This protects user accounts from security risks by enforcing a new password every time it's changed.
|
||||||
@@ -34,3 +62,19 @@ Enable email alerts for your users so that whenever another session is created f
|
|||||||
You won't receive notifications when logging in using [Magic URL](/docs/products/auth/magic-url), [Email OTP](/docs/products/auth/email-otp), or [OAuth2](/docs/products/auth/oauth2) since these authentication methods already verify user access to their systems, establishing the authentication's legitimacy.
|
You won't receive notifications when logging in using [Magic URL](/docs/products/auth/magic-url), [Email OTP](/docs/products/auth/email-otp), or [OAuth2](/docs/products/auth/oauth2) since these authentication methods already verify user access to their systems, establishing the authentication's legitimacy.
|
||||||
|
|
||||||
To toggle session alerts, navigate to **Auth** > **Security** > **Session alerts**.
|
To toggle session alerts, navigate to **Auth** > **Security** > **Session alerts**.
|
||||||
|
|
||||||
|
# Memberships privacy {% #memberships-privacy %}
|
||||||
|
|
||||||
|
In certain use cases, your app may not need to share members' personal information with others. You can safeguard privacy by marking specific membership details as private. To configure this setting, navigate to **Auth** > **Security** > **Memberships privacy**
|
||||||
|
|
||||||
|
These details can be made private:
|
||||||
|
|
||||||
|
- `userName` - The member's name
|
||||||
|
- `userEmail` - The member's email address
|
||||||
|
- `mfa` - Whether the member has enabled multi-factor authentication
|
||||||
|
|
||||||
|
# Mock phone numbers {% #mock-phone-numbers %}
|
||||||
|
|
||||||
|
Creating and using mock phone numbers allows users to test SMS authentication without needing an actual phone number. This can be useful for testing edge cases where a user doesn't have a phone number but needs to sign in to your application using SMS.
|
||||||
|
|
||||||
|
To create a mock phone number, navigate to **Auth** > **Security** > Mock Phone Numbers. After defining a mock phone number, you need to define a specific OTP code that will be used for SMS sign-in instead of the SMS secret code sent to a real phone number.
|
||||||
|
|||||||
18
src/partials/prohibited-activities.md
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
# Prohibited activities {% #prohibited-activities %}
|
||||||
|
|
||||||
|
The following actions are prohibited on the Appwrite platform and may lead to immediate suspension or termination:
|
||||||
|
|
||||||
|
- **Illegal and harmful content:** Sharing any content that is illegal, infringing (e.g., copyright infringement), harmful, threatening, defamatory, obscene, harassing, or otherwise objectionable. This includes distributing malware, viruses, or any malicious code.
|
||||||
|
- **Unauthorized access and disruption:** Accessing or attempting to access any system, data, or account without authorization. This includes:
|
||||||
|
- Hacking
|
||||||
|
- Penetration testing without approval
|
||||||
|
- Denial-of-Service (DoS) attacks
|
||||||
|
- Disrupting Appwrite Cloud's integrity or performance (e.g., excessive resource usage, unauthorized load testing)
|
||||||
|
- **Deceptive practices:** Engaging in any fraudulent or deceptive activity, such as:
|
||||||
|
- Phishing
|
||||||
|
- Misleading others
|
||||||
|
- Circumventing payment obligations
|
||||||
|
- **Unsolicited communications:** Sending spam, unauthorized advertising, or any form of improper solicitation.
|
||||||
|
- **Misuse of resources:**
|
||||||
|
- Using Appwrite Cloud for cryptocurrency mining without authorization
|
||||||
|
- Violating any applicable laws or regulations
|
||||||
@@ -674,5 +674,41 @@
|
|||||||
{
|
{
|
||||||
"link": "/cli/install.sh",
|
"link": "/cli/install.sh",
|
||||||
"redirect": "https://raw.githubusercontent.com/appwrite/sdk-for-cli/master/install.sh"
|
"redirect": "https://raw.githubusercontent.com/appwrite/sdk-for-cli/master/install.sh"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"link": "/case-studies",
|
||||||
|
"redirect": "/customer-stories"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"link": "/blog/category/case-studies",
|
||||||
|
"redirect": "/blog/category/customer-stories"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"link": "/blog/post/case-study-smartbee",
|
||||||
|
"redirect": "/blog/post/customer-stories-smartbee"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"link": "/blog/post/case-study-kcollect",
|
||||||
|
"redirect": "/blog/post/customer-stories-kcollect"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"link": "/blog/post/case-study-majik-kids",
|
||||||
|
"redirect": "/blog/post/customer-stories-majik-kids"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"link": "/blog/post/case-study-myshoefitter",
|
||||||
|
"redirect": "/blog/post/customer-stories-myshoefitter"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"link": "/blog/post/case-study-open-mind",
|
||||||
|
"redirect": "/blog/post/customer-stories-open-mind"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"link": "/blog/post/case-study-langx",
|
||||||
|
"redirect": "/blog/post/customer-stories-langx"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"link": "/blog/post/case-study-undo",
|
||||||
|
"redirect": "/blog/post/customer-stories-undo"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -74,6 +74,15 @@
|
|||||||
if (ref || referrer || utmSource || utmCampaign || utmMedium) {
|
if (ref || referrer || utmSource || utmCampaign || utmMedium) {
|
||||||
createSource(ref, referrer, utmSource, utmCampaign, utmMedium);
|
createSource(ref, referrer, utmSource, utmCampaign, utmMedium);
|
||||||
}
|
}
|
||||||
|
if (referrer || ref) {
|
||||||
|
sessionStorage.setItem('utmReferral', referrer ? referrer : (ref ?? ''));
|
||||||
|
}
|
||||||
|
if (utmSource) {
|
||||||
|
sessionStorage.setItem('utmSource', utmSource);
|
||||||
|
}
|
||||||
|
if (utmMedium) {
|
||||||
|
sessionStorage.setItem('utmMedium', utmMedium);
|
||||||
|
}
|
||||||
const initialTheme = $page.route.id?.startsWith('/docs') ? getPreferredTheme() : 'dark';
|
const initialTheme = $page.route.id?.startsWith('/docs') ? getPreferredTheme() : 'dark';
|
||||||
|
|
||||||
applyTheme(initialTheme);
|
applyTheme(initialTheme);
|
||||||
|
|||||||
@@ -11,11 +11,12 @@
|
|||||||
import MainFooter from '../lib/components/MainFooter.svelte';
|
import MainFooter from '../lib/components/MainFooter.svelte';
|
||||||
import DeveloperCard from './DeveloperCard.svelte';
|
import DeveloperCard from './DeveloperCard.svelte';
|
||||||
import { PUBLIC_APPWRITE_DASHBOARD } from '$env/static/public';
|
import { PUBLIC_APPWRITE_DASHBOARD } from '$env/static/public';
|
||||||
import CoverImage from './dashboard.png';
|
import CoverImage from './dashboard.webp';
|
||||||
import Hero from '$lib/components/ui/Hero.svelte';
|
import Hero from '$lib/components/ui/Hero.svelte';
|
||||||
import GradientText from '$lib/components/ui/GradientText.svelte';
|
import GradientText from '$lib/components/ui/GradientText.svelte';
|
||||||
import Badge from '$lib/components/ui/Badge.svelte';
|
import Badge from '$lib/components/ui/Badge.svelte';
|
||||||
import { trackEvent } from '$lib/actions/analytics';
|
import { trackEvent } from '$lib/actions/analytics';
|
||||||
|
import AppwriteIn100Seconds from '$lib/components/AppwriteIn100Seconds.svelte';
|
||||||
|
|
||||||
const title = 'Appwrite - Build like a team of hundreds';
|
const title = 'Appwrite - Build like a team of hundreds';
|
||||||
const description = DEFAULT_DESCRIPTION;
|
const description = DEFAULT_DESCRIPTION;
|
||||||
@@ -92,7 +93,7 @@
|
|||||||
<enhanced:img
|
<enhanced:img
|
||||||
style="width:1466px; height:804px; transform:rotate(150.348deg); opacity: 0.65; filter: blur(127.5px);
|
style="width:1466px; height:804px; transform:rotate(150.348deg); opacity: 0.65; filter: blur(127.5px);
|
||||||
max-block-size: unset; max-inline-size: unset;"
|
max-block-size: unset; max-inline-size: unset;"
|
||||||
src="./top-page-dark.png"
|
src="./top-page-dark.webp"
|
||||||
alt=""
|
alt=""
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -103,7 +104,7 @@
|
|||||||
style="top: 22rem; left: 54%; translate: calc(-50% - 900px); width: 75.9375rem;"
|
style="top: 22rem; left: 54%; translate: calc(-50% - 900px); width: 75.9375rem;"
|
||||||
class:web-u-hide-mobile={$isMobileNavOpen}
|
class:web-u-hide-mobile={$isMobileNavOpen}
|
||||||
>
|
>
|
||||||
<img src="/images/bgs/hero-lines-1.png" alt="" />
|
<img src="/images/bgs/hero-lines-1.webp" alt="" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
@@ -112,7 +113,7 @@
|
|||||||
class:web-u-hide-mobile={$isMobileNavOpen}
|
class:web-u-hide-mobile={$isMobileNavOpen}
|
||||||
>
|
>
|
||||||
<div style="left: 0;">
|
<div style="left: 0;">
|
||||||
<img src="/images/bgs/hero-lines-2.png" alt="" />
|
<img src="/images/bgs/hero-lines-2.webp" alt="" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -129,9 +130,7 @@
|
|||||||
<span class="web-icon-star shrink-0" aria-hidden="true" />
|
<span class="web-icon-star shrink-0" aria-hidden="true" />
|
||||||
<span class="text-caption shrink-0 font-medium">New</span>
|
<span class="text-caption shrink-0 font-medium">New</span>
|
||||||
<div class="web-hero-banner-button-sep" />
|
<div class="web-hero-banner-button-sep" />
|
||||||
<span class="text-caption web-u-trim-1"
|
<span class="text-caption web-u-trim-1">Introducing Database Backups</span>
|
||||||
>Introducing Database Backups</span
|
|
||||||
>
|
|
||||||
|
|
||||||
<span class="web-icon-arrow-right shrink-0" aria-hidden="true" />
|
<span class="web-icon-arrow-right shrink-0" aria-hidden="true" />
|
||||||
</a>
|
</a>
|
||||||
@@ -147,14 +146,17 @@
|
|||||||
Functions, Storage, and Messaging to your projects using the frameworks
|
Functions, Storage, and Messaging to your projects using the frameworks
|
||||||
and languages of your choice.
|
and languages of your choice.
|
||||||
</svelte:fragment>
|
</svelte:fragment>
|
||||||
<a
|
<div class="mt-8 flex flex-col gap-4 sm:flex-row" slot="cta">
|
||||||
href={PUBLIC_APPWRITE_DASHBOARD}
|
<a
|
||||||
class="web-button mt-8 w-full lg:w-fit"
|
href={PUBLIC_APPWRITE_DASHBOARD}
|
||||||
slot="cta"
|
class="web-button w-full lg:w-fit"
|
||||||
on:click={() => trackEvent('Get started in hero')}
|
on:click={() => trackEvent('Get started in hero')}
|
||||||
>
|
>
|
||||||
Get started
|
Get started
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
|
<AppwriteIn100Seconds />
|
||||||
|
</div>
|
||||||
</Hero>
|
</Hero>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -35,6 +35,8 @@
|
|||||||
selectedMap = selectedMap;
|
selectedMap = selectedMap;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let showToc = false;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
@@ -64,19 +66,11 @@
|
|||||||
<div class="web-grid-120-1fr-auto">
|
<div class="web-grid-120-1fr-auto">
|
||||||
<header class="web-grid-120-1fr-auto-header">
|
<header class="web-grid-120-1fr-auto-header">
|
||||||
<h1 class="text-display font-aeonik-pro text-primary">Brand assets</h1>
|
<h1 class="text-display font-aeonik-pro text-primary">Brand assets</h1>
|
||||||
<button
|
|
||||||
class="web-u-padding-block-20 text-primary web-is-only-mobile
|
|
||||||
web-u-margin-inline-32-negative web-u-sep-block
|
|
||||||
mt-6 flex w-full w-full"
|
|
||||||
>
|
|
||||||
<span class="container flex w-full items-center justify-between">
|
|
||||||
<span class="text-description">Table of contents</span>
|
|
||||||
<span class="icon-menu-alt-4" aria-hidden="true" />
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
</header>
|
</header>
|
||||||
<TocNav />
|
|
||||||
<main class="web-grid-120-1fr-auto-main /web-is-mobile-closed" id="main">
|
<TocNav bind:showToc />
|
||||||
|
|
||||||
|
<main class="web-grid-120-1fr-auto-main" id="main">
|
||||||
<div class="web-content">
|
<div class="web-content">
|
||||||
<section>
|
<section>
|
||||||
<p>
|
<p>
|
||||||
@@ -628,4 +622,21 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.container {
|
||||||
|
padding-left: 0;
|
||||||
|
padding-right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
header {
|
||||||
|
padding-block-end: unset;
|
||||||
|
}
|
||||||
|
|
||||||
|
header,
|
||||||
|
main {
|
||||||
|
padding-left: var(--spacing-5, 1.25rem);
|
||||||
|
padding-right: var(--spacing-5, 1.25rem);
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,6 +0,0 @@
|
|||||||
---
|
|
||||||
layout: category
|
|
||||||
name: Case Studies
|
|
||||||
description: Learn more about the product development and growth journeys of our best customers.
|
|
||||||
---
|
|
||||||
|
|
||||||
6
src/routes/blog/category/customer-stories/+page.markdoc
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
---
|
||||||
|
layout: category
|
||||||
|
name: Customer Stories
|
||||||
|
description: Learn more about how other developers have built successful applications while relying on Appwrite.
|
||||||
|
---
|
||||||
|
|
||||||
@@ -0,0 +1,197 @@
|
|||||||
|
---
|
||||||
|
layout: post
|
||||||
|
title: 10 new Git commands you should start using today
|
||||||
|
description: Learn these Git commands to make your workflow smoother, faster, and flexible.
|
||||||
|
date: 2024-12-12
|
||||||
|
cover: /images/blog/10-git-commands-you-should-start-using/cover.png
|
||||||
|
timeToRead: 10
|
||||||
|
author: ebenezer-don
|
||||||
|
category: tutorial
|
||||||
|
featured: false
|
||||||
|
callToAction: true
|
||||||
|
---
|
||||||
|
|
||||||
|
If you've worked with Git long enough, you've probably hit some common frustrations like operations getting slower as repositories grow, accidentally overwriting changes when switching branches, or struggling with massive monorepos.
|
||||||
|
|
||||||
|
Thankfully, just like every other tool, Git is constantly evolving and adding new features to make our lives easier. While some of these commands aren't particularly recent, they remain lesser-known gems that can significantly improve your workflow. If you're already familiar with some of the core tips and tricks—like those covered in [15 Git command line tips every developer should know](https://appwrite.io/blog/post/15-git-cli-tips?dofollow=true), this article will introduce you to ten additional commands that can take your Git skills to the next level.
|
||||||
|
|
||||||
|
# 1. git switch - A safer way to change branches
|
||||||
|
|
||||||
|
Before Git 2.23, `git checkout` was the main command for switching branches, but it did much more than that. You could use it to restore files, create branches, or check out specific commits. This made it powerful but potentially confusing - especially when you just wanted to switch branches without touching your files.
|
||||||
|
|
||||||
|
That's why Git 2.23 introduced `git switch` as a more focused alternative for branch operations. With `git switch`, you can focus solely on branch management:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Move to another branch
|
||||||
|
git switch feature-branch
|
||||||
|
|
||||||
|
# Create and switch to a new branch
|
||||||
|
git switch -c new-branch
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
This clarity reduces the risk of accidentally overwriting files or making unintended changes. If you've ever hesitated to use `git checkout` for fear of doing something wrong, `git switch` simplifies the process.
|
||||||
|
|
||||||
|
# 2. git restore - Safely undo changes
|
||||||
|
|
||||||
|
Undoing changes often involved using `git checkout` to revert files or `git reset` to move the branch HEAD. However, both commands had the potential to alter your branch state if used incorrectly: `git reset` could move your branch HEAD, while `git checkout` could switch branches or check out a different commit, disrupting the current branch.
|
||||||
|
|
||||||
|
Git 2.23 introduced `git restore` to focus solely on undoing changes to files. It provides a safer and more straightforward way to revert changes in your working directory or staging area, clearly separating file operations from branch management tasks:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Discard working directory changes
|
||||||
|
git restore main.js
|
||||||
|
|
||||||
|
# Unstage changes from the index
|
||||||
|
git restore --staged main.js
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
This is especially useful for beginners or in high-stakes situations where precision matters. You can undo changes without worrying about accidentally switching branches or resetting commits.
|
||||||
|
|
||||||
|
# 3. git maintenance - Automate repository health
|
||||||
|
|
||||||
|
As repositories grow, performance can degrade. Operations like `git fetch`, `git status`, or `git log` may slow down, and unused data can clutter your repository. Before Git 2.29, you'd have to manually run commands like `git gc` (garbage collection) or `git repack` to keep your repository optimized.
|
||||||
|
|
||||||
|
Git 2.29 introduced `git maintenance`, which automates these tasks for you:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Enable automatic maintenance
|
||||||
|
git maintenance start
|
||||||
|
|
||||||
|
# Run cleanup tasks immediately
|
||||||
|
git maintenance run
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
**What's happening behind the scenes?**
|
||||||
|
|
||||||
|
- **Garbage Collection:** Removes unreachable objects, such as commits discarded during rebases or branch deletions.
|
||||||
|
- **Repacking:** Consolidates fragmented packfiles for better storage efficiency.
|
||||||
|
- **Commit Graph Updates:** Optimizes commit history traversal, speeding up commands like `git log` and `git blame`.
|
||||||
|
|
||||||
|
Using `git maintenance` will help you keep your repository healthy without manual effort.
|
||||||
|
|
||||||
|
# 4. git sparse-checkout - Efficiently handle large repositories
|
||||||
|
|
||||||
|
Monorepos are great for managing multiple projects, but cloning an entire repository when you only need a specific directory can be inefficient. Git 2.25 introduced `git sparse-checkout` to solve this.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Enable sparse-checkout mode
|
||||||
|
git sparse-checkout init
|
||||||
|
|
||||||
|
# Fetch only specific directories
|
||||||
|
# You can specify multiple directories separated by spaces
|
||||||
|
git sparse-checkout set services/ docs/
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
With `git sparse-checkout`, you can include only the directories or files you need in your working directory, leaving the rest untouched. This is useful for large teams working on distinct parts of a monorepo, and will save you time and disk space.
|
||||||
|
|
||||||
|
# 5. git log --remerge-diff: Understand merges better
|
||||||
|
|
||||||
|
Merge commits often show which branches were merged, but they don't always explain the specific changes introduced, especially when conflicts were resolved during the merge.
|
||||||
|
|
||||||
|
Starting with Git 2.35, you can use:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git log --remerge-diff
|
||||||
|
```
|
||||||
|
|
||||||
|
This option reconstructs the merge commit by replaying the recorded merge strategy and showing the exact changes it introduced. It's useful for debugging merge conflicts or reviewing a complicated merge history.
|
||||||
|
|
||||||
|
# 6. git blame --ignore-rev - Ignore noisy commits
|
||||||
|
|
||||||
|
When your team makes a bulk formatting change, `git blame` can lose its utility, as every line ends up pointing to the formatting commit instead of the original author.
|
||||||
|
|
||||||
|
Introduced in Git 2.23, the `--ignore-rev` option allows you to exclude such commits:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git blame --ignore-rev commit-hash
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
To persist this exclusion, you can set up an ignore-revs file:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Add the commit hash to the ignore-revs fileecho commit-hash >> .git-blame-ignore-revs
|
||||||
|
|
||||||
|
# Tell Git to use the file
|
||||||
|
git config blame.ignoreRevsFile .git-blame-ignore-revs
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
This helps you focus on meaningful authorship and can be useful in codebases with frequent style updates.
|
||||||
|
|
||||||
|
# 7. git range-diff - Compare and track changes between commit ranges
|
||||||
|
|
||||||
|
Rewriting history, whether through rebasing, cherry-picking, or interactive editing, can be tricky. After a rebase, you might wonder how the rewritten commits differ from the originals. `git range-diff` helps by comparing two commit ranges, showing how one evolved into the other and highlighting changes to individual commits:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git range-diff
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
This command can be used to understand the evolution of a feature or bug fix across different branches.
|
||||||
|
|
||||||
|
# 8. git worktree - Work on multiple branches simultaneously
|
||||||
|
|
||||||
|
Switching branches in a single working directory can disrupt your workflow, especially when you need to work across multiple branches. With `git worktree`, you can create additional working directories tied to the same repository.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Add a new worktree for a specific branch
|
||||||
|
git worktree add ../feature-branch feature-branch
|
||||||
|
|
||||||
|
# Remove a worktree when you're done
|
||||||
|
git worktree remove ../feature-branch
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
`git worktree` allows you to work on different branches without switching or stashing. You can also create throwaway worktrees with detached HEADs for testing, or isolate builds and deployments in separate working directories.
|
||||||
|
|
||||||
|
# 9. git rebase --update-refs - Keep references in sync
|
||||||
|
|
||||||
|
Rebasing rewrites history by replacing old commits with new ones, but this often leaves branch pointers or tags referencing outdated commits. Git 2.38 introduces the `--update-refs` option to handle this automatically:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git rebase --update-refs
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
With this command, Git ensures that related branches and tags referencing rewritten commits are updated to match the new history. This eliminates the need for tedious manual updates and ensures consistency across your repository.
|
||||||
|
|
||||||
|
For even more control, you can configure git rebase to always update specific refs by setting:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git config rebase.updateRefs true
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
This is useful in collaborative workflows or when managing multiple refs tied to the same history.
|
||||||
|
|
||||||
|
# 10. git commit --fixup and git rebase --autosquash - Fixup commits
|
||||||
|
|
||||||
|
While not a new feature (introduced in Git 1.7.4, back in 2011), `git commit --fixup` is often overlooked despite being a useful tool for maintaining clean commit histories. When working on a feature, you might realize that you need to fix or improve a previous commit. Manually editing your commit history to include these changes can lead to errors. Git provides `git commit --fixup` and `git rebase --autosquash` to automate this process.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Create a fixup commit targeting a specific commit
|
||||||
|
git commit --fixup=<commit-hash>
|
||||||
|
|
||||||
|
# Later, during an interactive rebase, automatically squash fixup commits
|
||||||
|
git rebase -i --autosquash <base-branch>
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
The `--fixup` option creates a commit that's marked to be automatically squashed into the target commit during an interactive rebase with `--autosquash`. This streamlines the process of cleaning up your commit history before merging changes, and will ensure that related changes are grouped together without manual effort.
|
||||||
|
|
||||||
|
# Conclusion
|
||||||
|
|
||||||
|
The commands we've discussed in this article can help you solve real problems you might be facing every day as a Git user. Whether you're managing a monorepo, handling large histories, or trying to keep your repository clean, these practical solutions can make a difference. Start with one or two that fit your current workflow, and you might be surprised by the improvements in your productivity.
|
||||||
|
|
||||||
|
If you liked this article, you might also enjoy [15 Git command line tips every developer should know](https://appwrite.io/blog/post/15-git-cli-tips?dofollow=true).
|
||||||
|
|
||||||
|
# More resources
|
||||||
|
|
||||||
|
- [How to implement Sign in with GitHub](https://appwrite.io/blog/post/implement-sign-in-with-github)
|
||||||
|
- [SQL vs NoSQL: Choosing the right database for your project](https://appwrite.io/blog/post/sql-vs-nosql)
|
||||||
|
- [Deno 2 vs Bun: which JavaScript runtime is right for you?](https://appwrite.io/blog/post/deno-vs-bun-javascript-runtime)
|
||||||
@@ -8,11 +8,12 @@ timeToRead: 6
|
|||||||
author: ebenezer-don
|
author: ebenezer-don
|
||||||
category: tutorial
|
category: tutorial
|
||||||
featured: false
|
featured: false
|
||||||
|
callToAction: true
|
||||||
---
|
---
|
||||||
|
|
||||||
While the command line interface can seem intimidating on the surface, it's actually a very useful tool that gives you control over your code in ways that GUIs often don't. If you can get comfortable with even a few git commands, you'll find yourself being more productive.
|
While the command line interface can seem intimidating on the surface, it's actually a very useful tool that gives you control over your code in ways that GUIs often don't. If you can get comfortable with even a few git commands, you'll find yourself being more productive.
|
||||||
|
|
||||||
In this guide, we'll cover 15 Git command line tips that will help make your workflow smoother, faster, and flexible, whether you're working solo or with a team.
|
In this guide, we'll cover 15 essential git commands you should know as a developer, however, if you're looking for more advanced commands, you can check out these [10 new Git commands you should start using today](https://appwrite.io/blog/post/10-git-commands-you-should-start-using?dofollow=true).
|
||||||
|
|
||||||
|
|
||||||
# 1. git init - start a new repository
|
# 1. git init - start a new repository
|
||||||
@@ -214,12 +215,13 @@ If something goes wrong, `reflog` can provide a trail to recover lost changes, m
|
|||||||
|
|
||||||
# Conclusion
|
# Conclusion
|
||||||
|
|
||||||
These 15 Git command line tips lay a strong foundation for both solo projects and team-based workflows. Mastering these basics ensures that you'll work efficiently, minimize errors, and maintain a clean project history. Over time, as you get comfortable with each command, your confidence in managing code will grow.
|
These 15 Git command line tips lay a strong foundation for both solo projects and team-based workflows. Mastering these basics ensures that you'll work efficiently, minimize errors, and maintain a clean project history.
|
||||||
|
Over time, as you get comfortable with each command, your confidence in managing code will grow, and you'll be able to easily add these [10 new Git commands](https://appwrite.io/blog/post/10-git-commands-you-should-start-using?dofollow=true) to your workflow.
|
||||||
|
|
||||||
Git's power is in its flexibility, and learning the command line lets you tap into that fully. Practice them, apply them, and let them become second nature for a smoother development experience.
|
Git's power is in its flexibility, and learning the command line lets you tap into that fully. Practice them, apply them, and let them become second nature for a smoother development experience.
|
||||||
|
|
||||||
# More resources
|
# More resources
|
||||||
|
|
||||||
- [How to implement Sign in with GitHub](https://appwrite.io/blog/post/implement-sign-in-with-github)
|
- [How to implement Sign in with GitHub](https://appwrite.io/blog/post/implement-sign-in-with-github)
|
||||||
- [Building a currency converter API with Deno 2 and Appwrite](https://appwrite.io/blog/post/build-a-currency-converter-with-deno)
|
- [10 new Git commands you should start using today](https://appwrite.io/blog/post/10-git-commands-you-should-start-using?dofollow=true)
|
||||||
- [Local serverless function development with the new Appwrite CLI](https://appwrite.io/blog/post/functions-local-development-guide)
|
- [Building a currency converter API with Deno 2 and Appwrite](https://appwrite.io/blog/post/build-a-currency-converter-with-deno)
|
||||||
@@ -35,7 +35,7 @@ Pro gives you much more room and flexibility to build, scale, and maintain your
|
|||||||
- Unlimited projects (never paused)
|
- Unlimited projects (never paused)
|
||||||
- Unlimited projects (never paused)
|
- Unlimited projects (never paused)
|
||||||
---
|
---
|
||||||
- 10GB bandwidth
|
- 5GB bandwidth
|
||||||
- 300GB bandwidth
|
- 300GB bandwidth
|
||||||
---
|
---
|
||||||
- 2GB storage
|
- 2GB storage
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ timeToRead: 7
|
|||||||
author: aditya-oberai
|
author: aditya-oberai
|
||||||
category: product
|
category: product
|
||||||
unlisted: true
|
unlisted: true
|
||||||
|
callToAction: true
|
||||||
---
|
---
|
||||||
|
|
||||||
If you are looking to build a mobile app, website, tool, or any other application that needs a backend, then you also know the daunting tasks that await. This is probably what brought you to this blog in the first place: looking for a solution to take care of your backend. One of these solutions is a Backend-as-a-Service (BaaS). It provides pre-built backend infrastructure and services to simplify app development, handling server-side tasks like data storage, user management, APIs, server maintenance, security, database management, and more. Two of these solutions are Appwrite and Supabase, and although both are solid options for your BaaS, they’re somewhat different.
|
If you are looking to build a mobile app, website, tool, or any other application that needs a backend, then you also know the daunting tasks that await. This is probably what brought you to this blog in the first place: looking for a solution to take care of your backend. One of these solutions is a Backend-as-a-Service (BaaS). It provides pre-built backend infrastructure and services to simplify app development, handling server-side tasks like data storage, user management, APIs, server maintenance, security, database management, and more. Two of these solutions are Appwrite and Supabase, and although both are solid options for your BaaS, they’re somewhat different.
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ Cloudinary is a cloud service that focuses on managing media assets, especially
|
|||||||
| **FEATURE** | **STORAGE** | **CLOUDINARY** |
|
| **FEATURE** | **STORAGE** | **CLOUDINARY** |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| Deployment | Self-hosted or cloud-hosted | Cloud |
|
| Deployment | Self-hosted or cloud-hosted | Cloud |
|
||||||
| Free plan | 2GB storage, 10GB bandwidth | 25GB storage/bandwidth |
|
| Free plan | 2GB storage, 5GB bandwidth | 25GB storage/bandwidth |
|
||||||
| Paid plan | $15 per member/month for increased bandwidth, users and storage | $89 to $224 per month for increased users and credits |
|
| Paid plan | $15 per member/month for increased bandwidth, users and storage | $89 to $224 per month for increased users and credits |
|
||||||
| Open source | Yes ✅ | No ❌ |
|
| Open source | Yes ✅ | No ❌ |
|
||||||
| Support | Discord and email, dedicated channels for startups | Community and email, paid support options |
|
| Support | Discord and email, dedicated channels for startups | Community and email, paid support options |
|
||||||
|
|||||||
@@ -8,8 +8,9 @@ timeToRead: 8
|
|||||||
author: aditya-oberai
|
author: aditya-oberai
|
||||||
category: product
|
category: product
|
||||||
featured: false
|
featured: false
|
||||||
|
callToAction: true
|
||||||
---
|
---
|
||||||
Serverless functions are a powerful tool for developers designed to provide flexibility and simplify backend tasks. With serverless functions, you can focus more on writing code and less on managing infrastructure, making your work faster and more efficient.
|
Serverless functions are a powerful tool for developers designed to provide flexibility and simplify backend tasks. With serverless functions, you can focus more on writing code and less on managing infrastructure, making your work faster and more efficient.
|
||||||
|
|
||||||
In this comparison, we'll take a look at the serverless functions offered by three popular backend-as-a-service platforms: Firebase, Supabase, and Appwrite.
|
In this comparison, we'll take a look at the serverless functions offered by three popular backend-as-a-service platforms: Firebase, Supabase, and Appwrite.
|
||||||
|
|
||||||
@@ -38,7 +39,7 @@ Firebase Cloud Functions allow you to run backend code in response to events tri
|
|||||||

|

|
||||||
|
|
||||||
## Overview:
|
## Overview:
|
||||||
Supabase, an open-source Firebase alternative, is rapidly gaining popularity for its seamless integration with PostgreSQL. It provides developers with a powerful SQL database, real-time capabilities, and serverless functions.
|
Supabase, an open-source Firebase alternative, is rapidly gaining popularity for its seamless integration with PostgreSQL. It provides developers with a powerful SQL database, real-time capabilities, and serverless functions.
|
||||||
|
|
||||||
## Serverless functions:
|
## Serverless functions:
|
||||||
Supabase functions, also known as Edge Functions, are deployed at the edge, ensuring fast execution times with built-in observability. These functions are written in TypeScript and are designed to work closely with your PostgreSQL database, allowing you to execute SQL queries directly from your functions. Edge Functions run server-side logic geographically close to users, offering low latency and great performance.
|
Supabase functions, also known as Edge Functions, are deployed at the edge, ensuring fast execution times with built-in observability. These functions are written in TypeScript and are designed to work closely with your PostgreSQL database, allowing you to execute SQL queries directly from your functions. Edge Functions run server-side logic geographically close to users, offering low latency and great performance.
|
||||||
@@ -96,11 +97,11 @@ Appwrite Functions support multiple languages, including Node.js, Python, Ruby,
|
|||||||
|
|
||||||
# Conclusion
|
# Conclusion
|
||||||
|
|
||||||
Choosing the right serverless function provider depends on your specific needs and project requirements.
|
Choosing the right serverless function provider depends on your specific needs and project requirements.
|
||||||
|
|
||||||
Supabase offers excellent SQL integration and low latency with its edge functions, making it ideal for applications that require real-time data handling and minimal delays.
|
Supabase offers excellent SQL integration and low latency with its edge functions, making it ideal for applications that require real-time data handling and minimal delays.
|
||||||
|
|
||||||
Firebase stands out with its mature ecosystem and seamless integration with Google services, perfect for developers looking for a comprehensive and reliable platform.
|
Firebase stands out with its mature ecosystem and seamless integration with Google services, perfect for developers looking for a comprehensive and reliable platform.
|
||||||
|
|
||||||
Appwrite's flexibility in language support and modularity makes it a great choice for developers seeking a highly customizable backend solution.
|
Appwrite's flexibility in language support and modularity makes it a great choice for developers seeking a highly customizable backend solution.
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ cover: /images/blog/best-pagination-technique/cover.png
|
|||||||
timeToRead: 8
|
timeToRead: 8
|
||||||
author: matej-baco
|
author: matej-baco
|
||||||
category: tutorial
|
category: tutorial
|
||||||
|
callToAction: true
|
||||||
---
|
---
|
||||||
|
|
||||||
The Database is one of the cornerstones of every application. It's where you store everything your app needs to remember, compute later, or display to other users online. It's all smooth sailing until your database grows and your application starts lagging because it's trying to fetch and render 1,000 posts simultaneously. As a smart engineer, you quickly patch this with a `Show more` button. However, a few weeks later, you encounter a `Timeout` error. Turning to Stack Overflow, you find that copying and pasting solutions is no longer helping. With no other options, you start debugging and discover that the database returns over 50,000 posts each time a user opens your app. What do you do now?
|
The Database is one of the cornerstones of every application. It's where you store everything your app needs to remember, compute later, or display to other users online. It's all smooth sailing until your database grows and your application starts lagging because it's trying to fetch and render 1,000 posts simultaneously. As a smart engineer, you quickly patch this with a `Show more` button. However, a few weeks later, you encounter a `Timeout` error. Turning to Stack Overflow, you find that copying and pasting solutions is no longer helping. With no other options, you start debugging and discover that the database returns over 50,000 posts each time a user opens your app. What do you do now?
|
||||||
@@ -196,7 +197,7 @@ const config = JSON.parse(open("config.json"));
|
|||||||
export default function () {
|
export default function () {
|
||||||
const offset = Query.offset(__ENV.OFFSET);
|
const offset = Query.offset(__ENV.OFFSET);
|
||||||
const limit = 10;
|
const limit = 10;
|
||||||
|
|
||||||
http.get(`${config.endpoint}/databases/main/collections/posts/documents?queries[]=${offset}&queries[]=${limit}`, {
|
http.get(`${config.endpoint}/databases/main/collections/posts/documents?queries[]=${offset}&queries[]=${limit}`, {
|
||||||
headers: {
|
headers: {
|
||||||
'content-type': 'application/json',
|
'content-type': 'application/json',
|
||||||
@@ -264,7 +265,7 @@ const config = JSON.parse(open("config.json"));
|
|||||||
export default function () {
|
export default function () {
|
||||||
const cursor = Query.cursorAfter(__ENV.CURSOR);
|
const cursor = Query.cursorAfter(__ENV.CURSOR);
|
||||||
const limit = 10;
|
const limit = 10;
|
||||||
|
|
||||||
http.get(`${config.endpoint}/databases/main/collections/posts/documents?queries[]=${offset}&queries[]=${limit}`, {
|
http.get(`${config.endpoint}/databases/main/collections/posts/documents?queries[]=${offset}&queries[]=${limit}`, {
|
||||||
headers: {
|
headers: {
|
||||||
'content-type': 'application/json',
|
'content-type': 'application/json',
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ timeToRead: 3
|
|||||||
author: dennis-ivy
|
author: dennis-ivy
|
||||||
category: tutorial
|
category: tutorial
|
||||||
featured: false
|
featured: false
|
||||||
|
callToAction: true
|
||||||
---
|
---
|
||||||
|
|
||||||
I want to address an issue I've seen popping up on Stack Overflow and the Appwrite Discord server and address some of the reasons you may be getting this error, then walk you through some of the steps you can take to try and resolve it as well.
|
I want to address an issue I've seen popping up on Stack Overflow and the Appwrite Discord server and address some of the reasons you may be getting this error, then walk you through some of the steps you can take to try and resolve it as well.
|
||||||
@@ -18,23 +19,23 @@ The error message you'll see in your console when trying to make a request to an
|
|||||||
|
|
||||||
Before we start debugging this, let's talk about what a CORS error is.
|
Before we start debugging this, let's talk about what a CORS error is.
|
||||||
|
|
||||||
Without diving deep into the topic, CORS (Cross-Origin Resource Sharing ) is a mechanism that allows a server to specify which origins can access it. By origins, I mean URLs.
|
Without diving deep into the topic, CORS (Cross-Origin Resource Sharing ) is a mechanism that allows a server to specify which origins can access it. By origins, I mean URLs.
|
||||||
|
|
||||||
Site A, our server sitting at `myapi.com` won't allow request coming from site B, which is our client app sitting at `myfrontend.com`.
|
Site A, our server sitting at `myapi.com` won't allow request coming from site B, which is our client app sitting at `myfrontend.com`.
|
||||||
|
|
||||||
This happens because our server has not added site B, `myfrontend.com`, to its list of allowed origins.
|
This happens because our server has not added site B, `myfrontend.com`, to its list of allowed origins.
|
||||||
Any request coming from a URL that is not listed in our server's allowed origins will be rejected by our CORS policy.
|
Any request coming from a URL that is not listed in our server's allowed origins will be rejected by our CORS policy.
|
||||||
The solution in this case would be to simply add `myfrontend.com` to the list of allowed origins.
|
The solution in this case would be to simply add `myfrontend.com` to the list of allowed origins.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
CORS is crucial because it provides a secure way to make requests across different origins.
|
CORS is crucial because it provides a secure way to make requests across different origins.
|
||||||
Without CORS, any website would be able to make requests to our server, and this would lead to major problems.
|
Without CORS, any website would be able to make requests to our server, and this would lead to major problems.
|
||||||
Imagine a malicious third party making a website `myfr0nt3nd.com` that key logs your user name and password, before making requests to your backend to validate the combination.
|
Imagine a malicious third party making a website `myfr0nt3nd.com` that key logs your user name and password, before making requests to your backend to validate the combination.
|
||||||
|
|
||||||
# Why you are getting a CORS error
|
# Why you are getting a CORS error
|
||||||
|
|
||||||
Now let's try to figure this all out in the context of Appwrite and why you may be getting this error.
|
Now let's try to figure this all out in the context of Appwrite and why you may be getting this error.
|
||||||
I have listed three main reasons. If more arise, I will update the article to include them.
|
I have listed three main reasons. If more arise, I will update the article to include them.
|
||||||
|
|
||||||
1. Origin not set in Console
|
1. Origin not set in Console
|
||||||
@@ -43,7 +44,7 @@ I have listed three main reasons. If more arise, I will update the article to in
|
|||||||
|
|
||||||
## 1 - Origin not set in Console
|
## 1 - Origin not set in Console
|
||||||
|
|
||||||
First, you'll want to check your Appwrite Console to make sure you have added a hostname and are making a request from the correct hostname.
|
First, you'll want to check your Appwrite Console to make sure you have added a hostname and are making a request from the correct hostname.
|
||||||
Make sure you have added a platform in your Appwrite console by going to the **Overview** tab, select your platform or adding one if you have none, and then ensure you have added a hostname.
|
Make sure you have added a platform in your Appwrite console by going to the **Overview** tab, select your platform or adding one if you have none, and then ensure you have added a hostname.
|
||||||
|
|
||||||
A hostname is simply the domain you will be making the request from. In development, this will most likely be `localhost`. No need to add a port number or protocol here.
|
A hostname is simply the domain you will be making the request from. In development, this will most likely be `localhost`. No need to add a port number or protocol here.
|
||||||
@@ -60,16 +61,16 @@ So if you find this is why you were getting a CORS error, you have a few ways of
|
|||||||
|
|
||||||
## 3 - Incorrect ID on request
|
## 3 - Incorrect ID on request
|
||||||
|
|
||||||
This one happens because of an improperly configured request, such as a typo when specifying a project ID. For example, when using the `listDocuments` method,
|
This one happens because of an improperly configured request, such as a typo when specifying a project ID. For example, when using the `listDocuments` method,
|
||||||
if the project ID is set incorrectly when the client is initialized, you will receive a CORS error.
|
if the project ID is set incorrectly when the client is initialized, you will receive a CORS error.
|
||||||
|
|
||||||
Without diving into the details about how CORS works, the problem occurs when the browser tries to check if the origin is allowed.
|
Without diving into the details about how CORS works, the problem occurs when the browser tries to check if the origin is allowed.
|
||||||
The request returns a `40X` response, so the entire CORS check fails.
|
The request returns a `40X` response, so the entire CORS check fails.
|
||||||
|
|
||||||
## Other things to consider
|
## Other things to consider
|
||||||
|
|
||||||
In most cases, the issues people face have to do with one of the above reasons listed and can be solved with the given suggestions.
|
In most cases, the issues people face have to do with one of the above reasons listed and can be solved with the given suggestions.
|
||||||
However, if you are still running into issues, I’ll keep an ongoing list of other possibilities and things to check for.
|
However, if you are still running into issues, I’ll keep an ongoing list of other possibilities and things to check for.
|
||||||
|
|
||||||
- Disabled CORS in browser. (I’ve seen people have this issue with browser extensions)
|
- Disabled CORS in browser. (I’ve seen people have this issue with browser extensions)
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ date: 2023-12-02
|
|||||||
cover: /images/blog/kcollect.png
|
cover: /images/blog/kcollect.png
|
||||||
timeToRead: 5
|
timeToRead: 5
|
||||||
author: aditya-oberai
|
author: aditya-oberai
|
||||||
category: case-studies
|
category: customer-stories
|
||||||
---
|
---
|
||||||
|
|
||||||
In 2019, Ryan O’Connor was a mere university student when he started exploring the world of Korean popular music, or K-pop. One of the areas within the K-pop fan ecosystem that caught his interest was the concept of photo cards. For those new to the K-pop community, K-pop photo cards are artist-specific collectible cards that are possessed and traded similarly to sports or Pokemon trading cards. These photo cards are produced and distributed by K-pop record labels and have developed a substantial interest and following over the last few years.
|
In 2019, Ryan O’Connor was a mere university student when he started exploring the world of Korean popular music, or K-pop. One of the areas within the K-pop fan ecosystem that caught his interest was the concept of photo cards. For those new to the K-pop community, K-pop photo cards are artist-specific collectible cards that are possessed and traded similarly to sports or Pokemon trading cards. These photo cards are produced and distributed by K-pop record labels and have developed a substantial interest and following over the last few years.
|
||||||
@@ -6,7 +6,7 @@ date: 2024-05-23
|
|||||||
cover: /images/blog/case-study-langx/cover.png
|
cover: /images/blog/case-study-langx/cover.png
|
||||||
timeToRead: 5
|
timeToRead: 5
|
||||||
author: aditya-oberai
|
author: aditya-oberai
|
||||||
category: case-studies
|
category: customer-stories
|
||||||
---
|
---
|
||||||
|
|
||||||
Born in Istanbul, Turkey, Xue never needed to prioritize learning English as a language until he pursued further education at Boğaziçi University, where his Mechanical Engineering coursework was delivered in English. After graduating and working as an IT manager at a multinational import-export company, Xue moved to Canada and founded his own tech consulting firm. By now, he had started exploring various language exchange platforms such as Tandem as a learner; however, he soon realized there was no perfect platform for his needs.
|
Born in Istanbul, Turkey, Xue never needed to prioritize learning English as a language until he pursued further education at Boğaziçi University, where his Mechanical Engineering coursework was delivered in English. After graduating and working as an IT manager at a multinational import-export company, Xue moved to Canada and founded his own tech consulting firm. By now, he had started exploring various language exchange platforms such as Tandem as a learner; however, he soon realized there was no perfect platform for his needs.
|
||||||
@@ -6,7 +6,7 @@ date: 2024-01-19
|
|||||||
cover: /images/blog/majik-kids.png
|
cover: /images/blog/majik-kids.png
|
||||||
timeToRead: 7
|
timeToRead: 7
|
||||||
author: aditya-oberai
|
author: aditya-oberai
|
||||||
category: case-studies
|
category: customer-stories
|
||||||
---
|
---
|
||||||
|
|
||||||
# Ideating an alternative content platform for children
|
# Ideating an alternative content platform for children
|
||||||
@@ -73,4 +73,4 @@ So far, they have
|
|||||||
|
|
||||||
The team at Majik Kids appreciates how Appwrite Cloud **simplified their development process** and **accelerated productivity** by offering a scalable and reliable managed Backend-as-a-Service solution with significant cost savings.
|
The team at Majik Kids appreciates how Appwrite Cloud **simplified their development process** and **accelerated productivity** by offering a scalable and reliable managed Backend-as-a-Service solution with significant cost savings.
|
||||||
|
|
||||||
Learn more about Majik Kids by visiting their [website](https://majikkids.com/).
|
Learn more about Majik Kids by visiting their [website](https://majikkids.com/).
|
||||||
@@ -6,7 +6,7 @@ date: 2024-03-04
|
|||||||
cover: /images/blog/case-study-myshoefitter.png
|
cover: /images/blog/case-study-myshoefitter.png
|
||||||
timeToRead: 5
|
timeToRead: 5
|
||||||
author: aditya-oberai
|
author: aditya-oberai
|
||||||
category: case-studies
|
category: customer-stories
|
||||||
---
|
---
|
||||||
|
|
||||||
> “Appwrite has been a tremendous asset in implementing our IT infrastructure. Not only is the software an absolute game-changer, but the team is always there when you need them. The integrated user authentication and the ease of creating data structures have undoubtedly saved us several weeks' worth of time. For me, Appwrite is the perfect backend solution. All you have to do is sign up, and your backend is ready to go. I have never seen such an innovative and easy-to-understand backend solution before!” \
|
> “Appwrite has been a tremendous asset in implementing our IT infrastructure. Not only is the software an absolute game-changer, but the team is always there when you need them. The integrated user authentication and the ease of creating data structures have undoubtedly saved us several weeks' worth of time. For me, Appwrite is the perfect backend solution. All you have to do is sign up, and your backend is ready to go. I have never seen such an innovative and easy-to-understand backend solution before!” \
|
||||||
@@ -6,7 +6,7 @@ date: 2024-04-12
|
|||||||
cover: /images/blog/case-study-open-mind/cover.png
|
cover: /images/blog/case-study-open-mind/cover.png
|
||||||
timeToRead: 5
|
timeToRead: 5
|
||||||
author: aditya-oberai
|
author: aditya-oberai
|
||||||
category: case-studies
|
category: customer-stories
|
||||||
---
|
---
|
||||||
|
|
||||||
While still at school, David Forster noticed a substantial increase in the usage of narcotic substances by his peers. He saw that the consumption of narcotic substances led to a decline in the mental and physical health of these folks. However, at that time, the only educational forums on this topic that were accessible to people were Wiki pages with information that was too complex to understand. A lack of simple educational tools prevented David from helping his peers break out of a substance habit.
|
While still at school, David Forster noticed a substantial increase in the usage of narcotic substances by his peers. He saw that the consumption of narcotic substances led to a decline in the mental and physical health of these folks. However, at that time, the only educational forums on this topic that were accessible to people were Wiki pages with information that was too complex to understand. A lack of simple educational tools prevented David from helping his peers break out of a substance habit.
|
||||||
@@ -47,4 +47,4 @@ Although localized to regions within Europe, the Android app has amassed:
|
|||||||
|
|
||||||
In Testflight, the Apple app amassed over 1000 beta users; however, changes in the App Store policies prevented them from further updating and promoting the Apple app. In recent times, however, the team has taken a new direction and is converting the Open Mind mobile app into a web app using Flutter Web. The current version of the web application is available at [openmindapp.de](https://openmindapp.de/) and is quickly gaining traction and users.
|
In Testflight, the Apple app amassed over 1000 beta users; however, changes in the App Store policies prevented them from further updating and promoting the Apple app. In recent times, however, the team has taken a new direction and is converting the Open Mind mobile app into a web app using Flutter Web. The current version of the web application is available at [openmindapp.de](https://openmindapp.de/) and is quickly gaining traction and users.
|
||||||
|
|
||||||
As the Open Mind application grows, we wish David and the App Innovators team the best of luck and look forward to their future ventures. You can learn more about them by visiting their [website](https://app-innovators.de/).
|
As the Open Mind application grows, we wish David and the App Innovators team the best of luck and look forward to their future ventures. You can learn more about them by visiting their [website](https://app-innovators.de/).
|
||||||
@@ -6,7 +6,7 @@ date: 2023-12-02
|
|||||||
cover: /images/blog/smartbee.png
|
cover: /images/blog/smartbee.png
|
||||||
timeToRead: 5
|
timeToRead: 5
|
||||||
author: aditya-oberai
|
author: aditya-oberai
|
||||||
category: case-studies
|
category: customer-stories
|
||||||
---
|
---
|
||||||
|
|
||||||
In 2020, Sergio Ponguta and his brother started Smartbee, a company offering security and communications solutions for coal mining operations in Colombia. Both brothers, being formally educated in systems engineering, combined with a lack of fear of traversing down mines, felt comfortable launching this venture.
|
In 2020, Sergio Ponguta and his brother started Smartbee, a company offering security and communications solutions for coal mining operations in Colombia. Both brothers, being formally educated in systems engineering, combined with a lack of fear of traversing down mines, felt comfortable launching this venture.
|
||||||
@@ -68,4 +68,4 @@ In Sergio’s own words,
|
|||||||
|
|
||||||
> Just go for it, don’t think twice. Try Appwrite, and you will love it!
|
> Just go for it, don’t think twice. Try Appwrite, and you will love it!
|
||||||
|
|
||||||
Learn more about Smartbee by visiting their [website](https://smartbee.com.co/).
|
Learn more about Smartbee by visiting their [website](https://smartbee.com.co/).
|
||||||
@@ -6,7 +6,7 @@ date: 2024-07-09
|
|||||||
cover: /images/blog/case-study-undo/cover.png
|
cover: /images/blog/case-study-undo/cover.png
|
||||||
timeToRead: 6
|
timeToRead: 6
|
||||||
author: aditya-oberai
|
author: aditya-oberai
|
||||||
category: case-studies
|
category: customer-stories
|
||||||
---
|
---
|
||||||
|
|
||||||
Over the past decade, Jonas Janssen has seen the circular economy grow in Belgium, resulting in sustainable business models that focus on rental, resale, and recycling for consumer companies. At his previous job as a CTO, he interacted with several customers, often sustainability-focused small and medium-sized businesses (SMBs). However, many of these companies mentioned the lack of software solutions for managing logistics and supply chains in circular businesses. Major solutions providers like Microsoft and SAP would build software with extensive feature sets and large prices that these companies neither needed nor could afford. Simply put, there was no software solution in the market for circular businesses with a low barrier of entry and reasonable pricing.
|
Over the past decade, Jonas Janssen has seen the circular economy grow in Belgium, resulting in sustainable business models that focus on rental, resale, and recycling for consumer companies. At his previous job as a CTO, he interacted with several customers, often sustainability-focused small and medium-sized businesses (SMBs). However, many of these companies mentioned the lack of software solutions for managing logistics and supply chains in circular businesses. Major solutions providers like Microsoft and SAP would build software with extensive feature sets and large prices that these companies neither needed nor could afford. Simply put, there was no software solution in the market for circular businesses with a low barrier of entry and reasonable pricing.
|
||||||
@@ -64,4 +64,4 @@ Appwrite enabled UNDŌ to go from idea to first customer rapidly. In Jonas’s w
|
|||||||
|
|
||||||
> Thanks to Appwrite and advances in technology, we were able to get an MVP out in 2/3 months with 1 developer.
|
> Thanks to Appwrite and advances in technology, we were able to get an MVP out in 2/3 months with 1 developer.
|
||||||
|
|
||||||
We appreciate how UNDŌ is leveraging Appwrite to support businesses with eco-friendly, sustainable practices. We definitely look forward to their future endeavors. You can learn more about them by visiting their [website](https://undo.software/).
|
We appreciate how UNDŌ is leveraging Appwrite to support businesses with eco-friendly, sustainable practices. We definitely look forward to their future endeavors. You can learn more about them by visiting their [website](https://undo.software/).
|
||||||
@@ -7,6 +7,7 @@ cover: /images/blog/defying-the-laws-of-web-animations/cover.png
|
|||||||
timeToRead: 10
|
timeToRead: 10
|
||||||
author: thomas-g-lopes
|
author: thomas-g-lopes
|
||||||
category: website
|
category: website
|
||||||
|
callToAction: true
|
||||||
---
|
---
|
||||||
|
|
||||||
If you're a frontend developer, you know that one of the scariest tasks you can receive is coding a complex web animation. If you're not a frontend developer, I bet that sounds even harder.
|
If you're a frontend developer, you know that one of the scariest tasks you can receive is coding a complex web animation. If you're not a frontend developer, I bet that sounds even harder.
|
||||||
@@ -173,8 +174,6 @@ The video above showcases both Motion and Svelte transitions in action. The tabl
|
|||||||
{/if}
|
{/if}
|
||||||
```
|
```
|
||||||
|
|
||||||
{% call_to_action /%}
|
|
||||||
|
|
||||||
## Transitioning between sections
|
## Transitioning between sections
|
||||||
|
|
||||||
There's one other nifty feature of Motion that I didn't mention: It can seamlessly interrupt ongoing animations.
|
There's one other nifty feature of Motion that I didn't mention: It can seamlessly interrupt ongoing animations.
|
||||||
|
|||||||
@@ -0,0 +1,327 @@
|
|||||||
|
---
|
||||||
|
layout: post
|
||||||
|
title: "Deno 2 vs Bun: which JavaScript runtime is right for you?"
|
||||||
|
description: Deno vs Bun, how they fit in the Node.js ecosystem, and when you might want to use one over the other.
|
||||||
|
date: 2024-11-22
|
||||||
|
cover: /images/blog/deno-vs-bun-javascript-runtime/cover.png
|
||||||
|
timeToRead: 10
|
||||||
|
author: ebenezer-don
|
||||||
|
category: tutorial
|
||||||
|
---
|
||||||
|
|
||||||
|
JavaScript runtimes are evolving beyond Node.js, and this gives developers access to new tools designed for modern workflows, performance, and security. Two of the most talked-about options today are **Deno 2** and **Bun**.
|
||||||
|
Both aim to improve the developer experience in different ways, and they each bring unique strengths and trade-offs.
|
||||||
|
In this article, we'll take a practical look at what they offer, how they differ, and when you might want to use one over the other.
|
||||||
|
|
||||||
|
## Jump ahead:
|
||||||
|
|
||||||
|
- [Origins and background](#origins-and-background)
|
||||||
|
- [Performance comparison](#performance-comparison)
|
||||||
|
- [Language support and TypeScript](#language-support-and-typescript)
|
||||||
|
- [Package management approaches](#package-management-approaches)
|
||||||
|
- [Security features and models](#security-features-and-models)
|
||||||
|
- [Development tooling](#development-tooling)
|
||||||
|
- [Node.js compatibility layer](#nodejs-compatibility-layer)
|
||||||
|
- [Developer experience and workflow](#developer-experience-and-workflow)
|
||||||
|
- [Resource management](#resource-management)
|
||||||
|
- [Ideal use cases](#ideal-use-cases)
|
||||||
|
- [Community and ecosystem overview](#community-and-ecosystem-overview)
|
||||||
|
|
||||||
|
# Origins and background {% #origins-and-background %}
|
||||||
|
|
||||||
|
Deno was created by Ryan Dahl, who also developed Node.js. After years of Node.js dominating the JavaScript ecosystem, Dahl became vocal about its shortcomings around **security**, **dependency management**, and the lack of **TypeScript support**.
|
||||||
|
Deno was designed to address these issues.
|
||||||
|
|
||||||
|
Bun was created by Jarred Sumner, and it takes a different approach. Bun focuses on **performance and ease of use**. The runtime is built with speed as its primary goal, targeting developers who want a faster, smoother experience.
|
||||||
|
|
||||||
|
# Performance comparison {% #performance-comparison %}
|
||||||
|
|
||||||
|
## What makes Bun so fast? {% #what-makes-bun-so-fast %}
|
||||||
|
|
||||||
|
Bun is widely recognized for its speed. It's written in Zig, a low-level programming language designed for performance, and uses **JavaScriptCore**, the engine behind Safari.
|
||||||
|
This focus on efficiency is apparent in nearly every aspect of Bun's runtime:
|
||||||
|
|
||||||
|
- **Cold start times:** Bun starts faster than both Deno and Node.js, making it ideal for tasks that need quick initialization, like CLI tools.
|
||||||
|
- **HTTP servers:** Benchmarks show Bun serving HTTP requests with lower latency and higher throughput compared to Deno or Node.js.
|
||||||
|
- **Package installation:** Bun's built-in package manager is faster than npm or even pnpm, making dependency setup almost instant.
|
||||||
|
|
||||||
|
These performance gains come from Bun's tightly integrated ecosystem.
|
||||||
|
By designing its runtime, package manager, and bundler to work together, Bun eliminates much of the overhead seen in traditional setups.
|
||||||
|
|
||||||
|
## How Deno balances performance {% #how-deno-balances-performance %}
|
||||||
|
|
||||||
|
Deno isn't designed to compete with Bun in raw speed, but that doesn't mean it's slow.
|
||||||
|
Deno is built with Rust and uses **V8**, the same JavaScript engine as Node.js and Chrome. The runtime focuses on predictable, reliable performance. It's well-suited for:
|
||||||
|
|
||||||
|
- **Long-running processes:** Deno's resource management ensures consistent memory usage, making it ideal for servers or APIs that run for extended periods.
|
||||||
|
- **Real-world workloads:** While Deno may not win in benchmarks against Bun, Deno performs well under typical conditions like handling API requests or running TypeScript-heavy applications.
|
||||||
|
|
||||||
|
The difference between Bun's and Deno's performance matters most in edge cases.
|
||||||
|
If you're building tools or systems where startup speed is critical, Bun will feel faster. For more complex, long-term projects, Deno's balanced approach might be preferable.
|
||||||
|
|
||||||
|
# Language support and TypeScript {% #language-support-and-typescript %}
|
||||||
|
|
||||||
|
## Deno's native TypeScript support {% #deno-native-typescript-support %}
|
||||||
|
|
||||||
|
One of Deno's advantages is its native TypeScript support. Unlike Node.js or Bun, you can run .ts files directly in Deno without requiring separate tools like Babel or TypeScript CLI.
|
||||||
|
The runtime compiles TypeScript files into JavaScript during the first execution and caches the results, so developers can use TypeScript out of the box.
|
||||||
|
|
||||||
|
This integration:
|
||||||
|
|
||||||
|
- Simplifies workflows by removing the need for external build tools for most use cases.
|
||||||
|
- Treats TypeScript as a first-class citizen, deeply integrated into the runtime.
|
||||||
|
- Supports excellent error reporting, with optional full type-checking using the `--check` flag.
|
||||||
|
|
||||||
|
If you're heavily invested in TypeScript, Deno's built-in support will significantly reduce setup complexity.
|
||||||
|
This makes it an excellent choice for TypeScript-first projects.
|
||||||
|
|
||||||
|
## Bun's approach to TypeScript {% #bun-typescript-approach %}
|
||||||
|
|
||||||
|
Bun also supports TypeScript but takes a different approach to integration. Like Deno, Bun transpiles TypeScript to JavaScript using its own optimized implementation based on its JavaScript engine.
|
||||||
|
However, while Deno integrates TypeScript deeply into its runtime and offers features like type-checking with the `--check` flag, Bun focuses solely on fast transpilation and does not perform type-checking natively. For strict type-checking and debugging, external tools like the TypeScript compiler (`tsc`) are required.
|
||||||
|
|
||||||
|
For simpler TypeScript projects, Bun's lightweight approach works well. But for larger or more complex applications that demand deeper TypeScript integration, Deno provides a more comprehensive developer experience.
|
||||||
|
|
||||||
|
# Package management approaches {% #package-management-approaches %}
|
||||||
|
|
||||||
|
## How Deno manages dependencies {% #deno-dependency-management %}
|
||||||
|
|
||||||
|
Deno's approach to dependencies is one of its boldest departures from Node.js. Instead of relying on a centralized package manager like npm, Deno uses **URL-based imports**.
|
||||||
|
For example:
|
||||||
|
|
||||||
|
```jsx
|
||||||
|
import { serve } from '<https://deno.land/std@0.192.0/http/server.ts>'
|
||||||
|
```
|
||||||
|
|
||||||
|
This method:
|
||||||
|
|
||||||
|
- Removes the need for a `node_modules` directory.
|
||||||
|
- Ensures every dependency is explicitly defined, reducing the chance of unexpected changes in the supply chain.
|
||||||
|
|
||||||
|
In addition to URL-based imports, Deno now supports npm compatibility, enabling developers to use Node.js packages directly.
|
||||||
|
This feature is still relatively new, and while it's highly functional, some edge cases may remain. Bun, by comparison, was built with npm integration as a core feature and might provide a more polished experience for npm users at this time.
|
||||||
|
|
||||||
|
## Bun's npm-first model {% #bun-npm-first-model %}
|
||||||
|
|
||||||
|
Bun fully embraces the npm ecosystem. Its built-in package manager is designed to be faster than npm or pnpm, and it supports nearly all npm packages out of the box.
|
||||||
|
This makes Bun an excellent choice for projects that rely heavily on existing Node.js libraries or for developers transitioning from Node.js.
|
||||||
|
|
||||||
|
One key strength of Bun is how quickly it installs and resolves dependencies. For example, running `bun install` on a project is significantly faster than running `npm install`, thanks to Bun's optimized dependency resolver and storage system.
|
||||||
|
This makes Bun a compelling option for developers seeking to streamline their workflows without sacrificing access to the rich npm ecosystem.
|
||||||
|
|
||||||
|
## Trade-offs {% #trade-offs %}
|
||||||
|
|
||||||
|
- **Deno:** Its modular, web-like approach is forward-thinking and secure, but it requires developers to adapt. The npm integration is improving, but it's not yet as polished as Bun's.
|
||||||
|
- **Bun:** Its tight npm integration and fast installs make Bun practical for Node.js projects. While focused on compatibility, it excels in performance and developer-friendly features like hot reloading and fast builds.
|
||||||
|
|
||||||
|
# Security features and models {% #security-features-and-models %}
|
||||||
|
|
||||||
|
## Deno's permissions model {% #deno-permissions-model %}
|
||||||
|
|
||||||
|
One of Deno's defining features is its **strict permissions model**. By default, Deno doesn't allow your code to access the file system, make network requests, or read environment variables unless you explicitly grant these permissions. For example:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
deno run --allow-net --allow-read app.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
This approach minimizes risks by ensuring that code can only interact with the system in ways that you, the developer, have approved. This is especially important in production environments or multi-tenant systems, where limiting what an application can do reduces the impact of potential vulnerabilities.
|
||||||
|
|
||||||
|
From a practical standpoint, the permissions model encourages developers to think carefully about the resources their code needs. However, if you're used to Node.js, you might find Deno's permissions model more restrictive at first.
|
||||||
|
|
||||||
|
## Bun's permissive approach {% #bun-permissive-approach %}
|
||||||
|
|
||||||
|
Bun takes a more traditional approach, similar to Node.js. By default, it doesn't restrict access to system resources, making it easier to get started but leaving security entirely in the hands of the developer. While this design choice aligns with Bun's philosophy of prioritizing speed and convenience, it can lead to unintended behaviors if you're not careful.
|
||||||
|
|
||||||
|
For example, a dependency or library you install might access files or make network requests without your knowledge. This isn't a dealbreaker for many developers, but it's a trade-off worth considering if security is a top priority.
|
||||||
|
|
||||||
|
## The trade-off {% #the-trade-off %}
|
||||||
|
|
||||||
|
The difference in philosophy here is significant:
|
||||||
|
|
||||||
|
- **Deno**: A secure-by-default model that requires more upfront configuration but provides peace of mind.
|
||||||
|
- **Bun**: A developer-friendly model that sacrifices some safety for ease of use and speed.
|
||||||
|
|
||||||
|
For production systems or applications handling sensitive data, Deno's model is preferable. For rapid prototyping or local development, Bun's permissiveness is less of a concern.
|
||||||
|
|
||||||
|
# Development tooling {% #development-tooling %}
|
||||||
|
|
||||||
|
## What Deno provides {% #deno-built-in-features %}
|
||||||
|
|
||||||
|
Deno aims to be a comprehensive toolkit for developers, offering several built-in features that eliminate the need for external dependencies. Some highlights include:
|
||||||
|
|
||||||
|
- **Test runner**: Deno includes a built-in test runner that supports synchronous and asynchronous tests, assertions, and isolation.
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
Deno.test('example test', () => {
|
||||||
|
console.log('This is a test')
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Bundler**: Previously included a simple module bundler, but this feature is now deprecated in favor of tools like esbuild.
|
||||||
|
- **Formatter and linter**: Deno's formatter and linter enforce consistent code style and best practices, with easy integration into CI/CD workflows.
|
||||||
|
- **Task runner (`deno task`)**: A lightweight alternative to npm scripts, allowing you to define and execute tasks in your project's `deno.json` file.
|
||||||
|
|
||||||
|
These tools are well-integrated and follow Deno's overall philosophy of minimalism and standards compliance. They may not always be as feature-rich as standalone alternatives, but for many use cases, they get the job done.
|
||||||
|
|
||||||
|
## What Bun provides {% #bun-built-in-features %}
|
||||||
|
|
||||||
|
Bun also provides built-in tools but focuses on performance and speed:
|
||||||
|
|
||||||
|
- **Test runner**: Like Deno, Bun includes a test runner, but it emphasizes faster execution for repetitive test workflows.
|
||||||
|
- **Bundler**: Bun's bundler is optimized for web development, supporting features like tree-shaking and minification for high-performance builds.
|
||||||
|
- **Package manager**: Bun's integrated package manager delivers exceptional installation speeds while maintaining strong npm compatibility.
|
||||||
|
|
||||||
|
Bun's tools excel in speed, making it an attractive option for tasks like testing, bundling, and dependency installation. However, unlike Deno, Bun prioritizes performance and practicality over strict adherence to web standards, which may be a trade-off for some developers.
|
||||||
|
|
||||||
|
## Developer workflow comparison {% #developer-workflow-comparison %}
|
||||||
|
|
||||||
|
- **Deno**: Offers a cohesive, standards-compliant toolkit that integrates well with modern development practices but may require additional configuration for advanced use cases.
|
||||||
|
- **Bun**: Prioritizes speed and convenience, making it ideal for iterative development and performance-critical tasks.
|
||||||
|
|
||||||
|
For day-to-day development, the choice depends on your priorities. Deno's tools emphasize stability and consistency, while Bun is designed to optimize speed and productivity.
|
||||||
|
|
||||||
|
# Node.js compatibility layer {% #nodejs-compatibility-layer %}
|
||||||
|
|
||||||
|
## Deno's growing compatibility {% #deno-nodejs-compatibility %}
|
||||||
|
|
||||||
|
Node.js compatibility has been one of Deno's biggest challenges. While Deno initially distanced itself from Node.js conventions (e.g., by avoiding the `require` syntax), it has gradually introduced features to support Node.js projects. Recent versions of Deno include:
|
||||||
|
|
||||||
|
- **npm support**: Deno can now use npm packages, which bridges the gap for developers relying on Node.js libraries.
|
||||||
|
- **Node.js API support**: Deno has implemented key Node.js APIs like `fs` and `events`, although not all features are fully supported yet.
|
||||||
|
|
||||||
|
Despite these advancements, there are still edge cases where Node.js compatibility isn't perfect. Libraries that rely heavily on native Node.js modules or older patterns may require additional effort to work in Deno.
|
||||||
|
|
||||||
|
## Bun as a drop-in replacement {% #bun-nodejs-compatibility %}
|
||||||
|
|
||||||
|
Bun, by contrast, aims to be a near-drop-in replacement for Node.js. Its compatibility with npm is excellent, and it supports almost all Node.js APIs out of the box. This makes it an attractive option for developers looking to migrate existing projects without rewriting significant portions of code.
|
||||||
|
|
||||||
|
However, even Bun isn't flawless. Certain edge cases, especially with less-used or experimental Node.js features, might still cause issues. But for the majority of projects, Bun's compatibility is seamless.
|
||||||
|
|
||||||
|
## Migrating existing projects {% #migrating-existing-projects %}
|
||||||
|
|
||||||
|
If you're transitioning from Node.js:
|
||||||
|
|
||||||
|
- **Bun** offers the path of least resistance, especially for npm-heavy projects.
|
||||||
|
- **Deno** requires more adaptation, especially for projects that rely on older Node.js conventions. However, its npm integration is improving rapidly and may soon close the gap.
|
||||||
|
|
||||||
|
# Developer experience and workflow {% #developer-experience-and-workflow %}
|
||||||
|
|
||||||
|
## Ease of use {% #ease-of-use %}
|
||||||
|
|
||||||
|
Both Deno and Bun strive to improve the developer experience, but their approaches differ:
|
||||||
|
|
||||||
|
- **Deno**: Focuses on clarity and standards. Its permission model and URL-based imports enforce best practices, but they can feel restrictive if you're coming from Node.js.
|
||||||
|
- **Bun**: Prioritizes familiarity and speed. It feels more like a performance-enhanced version of Node.js, which makes it easier to pick up for most developers.
|
||||||
|
|
||||||
|
## Documentation and resources {% #documentation-and-resources %}
|
||||||
|
|
||||||
|
- **Deno**: Has extensive official documentation, a growing ecosystem of tutorials, and strong community support. Its focus on standards also makes it easier to learn for developers already familiar with browser-based JavaScript.
|
||||||
|
- **Bun**: While newer and with a smaller community, Bun has clear documentation and is gaining traction quickly. Its npm compatibility makes it easy to leverage existing Node.js resources.
|
||||||
|
|
||||||
|
## Ecosystem maturity {% #ecosystem-maturity %}
|
||||||
|
|
||||||
|
Deno has been around longer and has a more mature ecosystem, with good TypeScript support and a steadily growing collection of modules on `deno.land`. Bun, while newer, is gaining momentum quickly, especially among developers looking for a high-performance alternative to Node.js.
|
||||||
|
|
||||||
|
# Resource management {% #resource-management %}
|
||||||
|
|
||||||
|
## Deno's resource management {% #deno-resource-management %}
|
||||||
|
|
||||||
|
Deno's architecture emphasizes memory safety and predictable resource usage, thanks to its foundation in Rust and V8. Rust enforces strict memory management, which ensures that Deno applications avoid common issues like memory leaks or unsafe pointer usage. Additionally, Deno provides a transparent way to manage resources:
|
||||||
|
|
||||||
|
- **Garbage Collection:** Like Node.js, Deno relies on V8 for garbage collection, ensuring that unused objects are cleaned up efficiently.
|
||||||
|
- **Concurrency:** Deno's async programming model and event loop enable efficient handling of high-concurrency workloads, such as managing multiple API requests or real-time applications.
|
||||||
|
|
||||||
|
This predictable resource usage makes Deno a strong choice for long-running processes or cloud-based applications where memory efficiency directly impacts operational costs.
|
||||||
|
|
||||||
|
## Bun's resource optimization {% #bun-resource-optimization %}
|
||||||
|
|
||||||
|
Bun, built with Zig and JavaScriptCore, prioritizes raw speed and lightweight resource usage. Zig allows developers to write highly optimized, low-level code, giving Bun an edge in tasks that require minimal overhead. For example:
|
||||||
|
|
||||||
|
- **Startup memory usage:** Bun often uses less memory at startup compared to Deno or Node.js.
|
||||||
|
- **CPU usage:** Bun's tight integration with JavaScriptCore results in lower CPU usage for tasks like serving static files or running small scripts.
|
||||||
|
|
||||||
|
However, Bun's strong focus on speed sometimes comes at the cost of detailed visibility into resource consumption. For production environments, you might need to monitor closely to avoid potential inefficiencies in complex use cases.
|
||||||
|
|
||||||
|
## When resource usage matters {% #when-resource-usage-matters %}
|
||||||
|
|
||||||
|
- If your application needs to run on constrained environments (e.g., serverless functions or edge devices), both Deno and Bun offer advantages over Node.js, but their strengths differ:
|
||||||
|
- **Deno** provides consistent and predictable resource management, making it well-suited for long-running processes or workloads where stability is critical.
|
||||||
|
- **Bun** performs better in lightweight or ephemeral tasks but may require more careful monitoring for resource-heavy workloads.
|
||||||
|
|
||||||
|
# Ideal use cases {% #ideal-use-cases %}
|
||||||
|
|
||||||
|
## When to choose Deno {% #when-to-choose-deno %}
|
||||||
|
|
||||||
|
Deno's design philosophy makes it ideal for scenarios where security, maintainability, and standards compliance are priorities:
|
||||||
|
|
||||||
|
- **TypeScript-first projects:** With native TypeScript support, Deno removes the friction of configuring a separate build pipeline.
|
||||||
|
- **Secure applications:** Deno's permissions model is a clear win for projects handling sensitive data or operating in multi-tenant environments.
|
||||||
|
- **Modern API development:** Its web-like module system and built-in tooling make it well-suited for REST or GraphQL APIs.
|
||||||
|
- **CLI tools:** Deno's simplicity and predictability make it a strong contender for command-line utilities, especially those using TypeScript.
|
||||||
|
|
||||||
|
## When to choose Bun {% #when-to-choose-bun %}
|
||||||
|
|
||||||
|
Bun's strengths lie in its speed and npm compatibility, making it a better fit for projects where performance and integration with existing tools are key:
|
||||||
|
|
||||||
|
- **Performance-critical applications:** If startup time or execution speed is a top priority, Bun is the faster option.
|
||||||
|
- **Node.js migrations:** For developers transitioning from Node.js, Bun offers seamless npm compatibility with little adjustment needed.
|
||||||
|
- **Prototyping and development:** Its speed and built-in tools help iterate quickly on small to medium-sized projects.
|
||||||
|
- **Web applications:** Bun's bundler and npm integration make it appealing for modern web development workflows.
|
||||||
|
|
||||||
|
## Hybrid use cases {% #hybrid-use-cases %}
|
||||||
|
|
||||||
|
There are situations where using both runtimes in a single project might make sense. For instance:
|
||||||
|
|
||||||
|
- Use **Deno** for a secure backend API and **Bun** for a fast, lightweight front-end build tool.
|
||||||
|
- Use **Bun** for rapid prototyping and continue with it or transition to **Deno** for production if security and standards compliance become higher priorities.
|
||||||
|
|
||||||
|
While these setups are possible, combining runtimes may add complexity to your development workflow.
|
||||||
|
|
||||||
|
# Community and ecosystem overview {% #community-and-ecosystem-overview %}
|
||||||
|
|
||||||
|
## Deno's ecosystem {% #deno-ecosystem %}
|
||||||
|
|
||||||
|
Deno has been around longer and benefits from a growing, mature ecosystem. Its official module registry, `deno.land`, includes a curated collection of high-quality libraries and tools, many of which are standards-based. Additionally, its compatibility with npm packages is improving, broadening the range of tools available to developers.
|
||||||
|
|
||||||
|
The Deno community is active, with strong contributions from developers and detailed documentation that makes onboarding relatively smooth. However, it still faces challenges in convincing Node.js developers to adapt to its unique features, like URL-based imports and strict permissions.
|
||||||
|
|
||||||
|
## Bun's ecosystem {% #bun-ecosystem %}
|
||||||
|
|
||||||
|
Bun's ecosystem is smaller but growing rapidly. Its focus on npm compatibility ensures that developers have access to the large Node.js library ecosystem from day one. This makes Bun feel less like a standalone runtime and more like a high-performance layer on top of Node.js.
|
||||||
|
|
||||||
|
The Bun community, while smaller, is enthusiastic and growing fast. Documentation is clear and concise, but as a newer tool, it may lack the breadth of resources that Deno offers.
|
||||||
|
|
||||||
|
## Maturity vs. momentum {% #maturity-vs-momentum %}
|
||||||
|
|
||||||
|
- **Deno** benefits from a more mature and standards-driven ecosystem.
|
||||||
|
- **Bun** capitalizes on its compatibility with npm and Node.js, giving it momentum as a fast alternative.
|
||||||
|
|
||||||
|
## Deno 2 vs. Bun comparison table {% #deno-2-vs-bun-comparison-table %}
|
||||||
|
|
||||||
|
| **Feature** | **Deno** | **Bun** |
|
||||||
|
| ---------------------- | ------------------------------------------------- | ------------------------------------------------ |
|
||||||
|
| **Philosophy** | Secure-by-default, standards-compliant | Performance-focused, npm-centric |
|
||||||
|
| **Primary Language** | Rust, V8 JavaScript engine | Zig, JavaScriptCore engine |
|
||||||
|
| **TypeScript Support** | Native support; no configuration needed | Transpiles TypeScript; external type-checking |
|
||||||
|
| **Module System** | URL-based imports; supports npm packages | Integrated with npm; uses `node_modules` |
|
||||||
|
| **Performance** | Balanced performance in various tasks | Optimized for speed, especially in server tasks |
|
||||||
|
| **Security** | Explicit permissions model | No default restrictions |
|
||||||
|
| **Node.js Compatibility** | Growing support with npm integration | High compatibility; near drop-in replacement |
|
||||||
|
| **Tooling and Packages** | Runtime, bundler, test runner, formatter, linter | Runtime, bundler, test runner, package manager |
|
||||||
|
| **Community and Maturity** | Established, standards-driven | Emerging, rapidly growing |
|
||||||
|
|
||||||
|
# Conclusion {% #conclusion %}
|
||||||
|
|
||||||
|
Deno 2 and Bun are both impressive JavaScript runtimes, but they serve different purposes and cater to different priorities. Ultimately, the right runtime depends on your use case. If you're building a secure backend API or a TypeScript-heavy CLI tool, **Deno 2** is hard to beat. If you're looking for raw speed or want to stay close to the Node.js ecosystem, **Bun** is an excellent option.
|
||||||
|
|
||||||
|
JavaScript is evolving, and both Deno 2 and Bun reflect the direction it's heading. While they take different paths, they both push the boundaries of what developers can expect from modern runtimes.
|
||||||
|
|
||||||
|
If you enjoyed this article, you might also enjoy [Building apps with Bun and Appwrite](https://appwrite.io/blog/post/building-apps-with-bun-and-appwrite) or reading about [what Deno 2 means for Appwrite Functions](https://appwrite.io/blog/post/deno-2-appwrite-functions).
|
||||||
|
|
||||||
|
You can reach out to us on [Discord](https://appwrite.io/discord) if you have any questions or send me a message on [LinkedIn](https://www.linkedin.com/in/ebenezerdon/) if you have any feedback.
|
||||||
|
|
||||||
|
# More resources {% #more-resources %}
|
||||||
|
|
||||||
|
- [Join Appwrite's Discord server](https://appwrite.io/discord)
|
||||||
|
- [Build a currency converter with Deno 2 and Appwrite](https://www.appwrite.io/blog/post/build-a-currency-converter-with-deno2)
|
||||||
|
- [Why you need to try the new Bun function runtime](https://appwrite.io/blog/post/why-you-need-to-try-the-new-bun-runtime)
|
||||||