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_DB_MAIN_ID=
|
||||
PUBLIC_APPWRITE_FN_TLDR_ID=
|
||||
PUBLIC_APPWRITE_DASHBOARD=
|
||||
PUBLIC_APPWRITE_ENDPOINT=
|
||||
PUBLIC_APPWRITE_PROJECT_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_DOMAIN=${{ secrets.PRD_APP_DOMAIN }}" >> .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
|
||||
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_DOMAIN=${{ secrets.STG_APP_DOMAIN }}" >> .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
|
||||
docker-compose -f ${{ env.STACK_FILE }} config
|
||||
|
||||
1
.gitignore
vendored
@@ -19,3 +19,4 @@ package-lock.json
|
||||
terraform/**/.t*
|
||||
terraform/**/.env
|
||||
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
|
||||
|
||||
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 %}
|
||||
|
||||
@@ -15,7 +15,7 @@ The Appwrite Website has been built with the following frameworks:
|
||||
|
||||
## Development
|
||||
|
||||
_If this is your first time setting up the repository, please run `pnpm install` inside the repo's directory._
|
||||
_If this is your first time setting up the repository, please run `pnpm install` inside the repo's directory and create a `.env` file based on `.env.example`._
|
||||
|
||||
To get the repo up and running in your local environment, use the following command:
|
||||
|
||||
|
||||
@@ -119,23 +119,23 @@ services:
|
||||
- TIME_BETWEEN_RUNS=3600
|
||||
- UNUSED_TIME=6h
|
||||
|
||||
sematext-agent:
|
||||
image: sematext/agent:latest
|
||||
environment:
|
||||
REGION: EU
|
||||
INFRA_TOKEN: $SEMATEXT_TOKEN
|
||||
deploy:
|
||||
mode: global
|
||||
restart_policy:
|
||||
condition: any
|
||||
resource-monitor:
|
||||
image: ghcr.io/appwrite/monitoring:0.1.0
|
||||
entrypoint: monitoring
|
||||
command:
|
||||
- '--url=${_APP_BETTER_STACK_INCIDENT_URL}'
|
||||
- '--interval=60'
|
||||
- '--cpu-limit=85'
|
||||
- '--memory-limit=80'
|
||||
- '--disk-limit=85'
|
||||
hostname: '{{.Node.Hostname}}'
|
||||
<<: *x-logging
|
||||
volumes:
|
||||
- /:/hostfs:ro
|
||||
- /etc/passwd:/etc/passwd:ro
|
||||
- /etc/group:/etc/group:ro
|
||||
- /sys:/host/sys:ro
|
||||
- /dev:/hostfs/dev:ro
|
||||
- /var/run:/var/run
|
||||
- /sys/kernel/debug:/sys/kernel/debug
|
||||
- /mnt:/mnt:ro
|
||||
deploy:
|
||||
<<: *x-update-config
|
||||
endpoint_mode: dnsrr
|
||||
mode: global
|
||||
|
||||
networks:
|
||||
cloud:
|
||||
|
||||
@@ -114,23 +114,23 @@ services:
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
|
||||
sematext-agent:
|
||||
image: sematext/agent:latest
|
||||
environment:
|
||||
REGION: EU
|
||||
INFRA_TOKEN: $SEMATEXT_TOKEN
|
||||
deploy:
|
||||
mode: global
|
||||
restart_policy:
|
||||
condition: any
|
||||
resource-monitor:
|
||||
image: ghcr.io/appwrite/monitoring:0.1.0
|
||||
entrypoint: monitoring
|
||||
command:
|
||||
- '--url=${_APP_BETTER_STACK_INCIDENT_URL}'
|
||||
- '--interval=60'
|
||||
- '--cpu-limit=85'
|
||||
- '--memory-limit=80'
|
||||
- '--disk-limit=85'
|
||||
hostname: '{{.Node.Hostname}}'
|
||||
<<: *x-logging
|
||||
volumes:
|
||||
- /:/hostfs:ro
|
||||
- /etc/passwd:/etc/passwd:ro
|
||||
- /etc/group:/etc/group:ro
|
||||
- /sys:/host/sys:ro
|
||||
- /dev:/hostfs/dev:ro
|
||||
- /var/run:/var/run
|
||||
- /sys/kernel/debug:/sys/kernel/debug
|
||||
- /mnt:/mnt:ro
|
||||
deploy:
|
||||
<<: *x-update-config
|
||||
endpoint_mode: dnsrr
|
||||
mode: global
|
||||
|
||||
networks:
|
||||
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",
|
||||
"property": "default",
|
||||
"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:integration": "playwright test",
|
||||
"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",
|
||||
"dependencies": {
|
||||
"@sentry/sveltekit": "^8.12.0",
|
||||
"h3": "^1.12.0",
|
||||
"sharp": "^0.33.4"
|
||||
"@sentry/sveltekit": "^8.38.0",
|
||||
"h3": "^1.13.0",
|
||||
"sharp": "^0.33.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@appwrite.io/console": "^0.6.2",
|
||||
"@appwrite.io/console": "^0.6.4",
|
||||
"@appwrite.io/pink": "~0.26.0",
|
||||
"@appwrite.io/pink-icons": "~0.26.0",
|
||||
"@appwrite.io/repo": "github:appwrite/appwrite#feat-multi-region-docs",
|
||||
"@internationalized/date": "3.5.0",
|
||||
"@melt-ui/pp": "^0.3.2",
|
||||
"@melt-ui/svelte": "^0.74.4",
|
||||
"@playwright/test": "^1.44.1",
|
||||
"@melt-ui/svelte": "^0.86.0",
|
||||
"@playwright/test": "^1.49.0",
|
||||
"@sveltejs/adapter-node": "^4.0.1",
|
||||
"@sveltejs/enhanced-img": "^0.1.9",
|
||||
"@sveltejs/kit": "^2.5.17",
|
||||
"@sveltejs/vite-plugin-svelte": "^3.1.1",
|
||||
"@sveltejs/kit": "^2.8.1",
|
||||
"@sveltejs/vite-plugin-svelte": "^3.1.2",
|
||||
"@tailwindcss/postcss": "4.0.0-alpha.17",
|
||||
"@types/compression": "^1.7.5",
|
||||
"@types/glob": "^8.1.0",
|
||||
"@types/markdown-it": "^13.0.8",
|
||||
"@types/markdown-it": "^13.0.9",
|
||||
"@types/morgan": "^1.9.9",
|
||||
"@typescript-eslint/eslint-plugin": "^7.13.1",
|
||||
"@typescript-eslint/parser": "^7.13.1",
|
||||
"analytics": "^0.8.14",
|
||||
"clsx": "^2.1.1",
|
||||
"cva": "npm:class-variance-authority@^0.7.0",
|
||||
"date-fns": "^3.6.0",
|
||||
"dequal": "^2.0.3",
|
||||
"embla-carousel": "^8.1.5",
|
||||
"embla-carousel-svelte": "^8.1.5",
|
||||
"embla-carousel": "^8.4.0",
|
||||
"embla-carousel-auto-scroll": "^8.5.1",
|
||||
"embla-carousel-svelte": "^8.4.0",
|
||||
"embla-carousel-wheel-gestures": "^8.0.1",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-config-prettier": "^8.10.0",
|
||||
"eslint-plugin-svelte": "^2.40.0",
|
||||
"eslint": "^9.15.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-plugin-svelte": "^2.46.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",
|
||||
"meilisearch": "^0.37.0",
|
||||
"motion": "^10.18.0",
|
||||
@@ -67,24 +68,26 @@
|
||||
"openapi-types": "^12.1.3",
|
||||
"oslllo-svg-fixer": "^3.0.0",
|
||||
"plausible-tracker": "^0.3.9",
|
||||
"postcss": "^8.4.39",
|
||||
"postcss": "^8.4.49",
|
||||
"prettier": "^3.3.3",
|
||||
"prettier-plugin-svelte": "^3.2.5",
|
||||
"prettier-plugin-tailwindcss": "^0.6.6",
|
||||
"remeda": "^2.10.0",
|
||||
"sass": "^1.77.6",
|
||||
"svelte": "^4.2.18",
|
||||
"svelte-check": "^3.8.1",
|
||||
"svelte-markdoc-preprocess": "^2.0.0",
|
||||
"prettier-plugin-svelte": "^3.2.8",
|
||||
"prettier-plugin-tailwindcss": "^0.6.8",
|
||||
"remeda": "^2.17.3",
|
||||
"sass": "^1.81.0",
|
||||
"svelte": "^4.2.19",
|
||||
"svelte-check": "^3.8.6",
|
||||
"svelte-markdoc-preprocess": "^2.1.0",
|
||||
"svelte-markdown": "^0.4.1",
|
||||
"svgtofont": "^4.2.1",
|
||||
"tailwind-merge": "^2.5.2",
|
||||
"svgtofont": "^4.2.3",
|
||||
"tailwind-merge": "^2.5.4",
|
||||
"tailwindcss": "4.0.0-alpha.17",
|
||||
"tslib": "^2.6.3",
|
||||
"typescript": "^5.5.2",
|
||||
"vite": "^5.3.1",
|
||||
"vite-plugin-dynamic-import": "^1.5.0",
|
||||
"tslib": "^2.8.1",
|
||||
"typescript": "^5.6.3",
|
||||
"typescript-eslint": "^8.15.0",
|
||||
"vite": "^5.4.11",
|
||||
"vite-plugin-dynamic-import": "^1.6.0",
|
||||
"vite-plugin-image-optimizer": "^1.1.8",
|
||||
"vite-plugin-manifest-sri": "^0.2.0",
|
||||
"vitest": "^1.6.0"
|
||||
}
|
||||
}
|
||||
|
||||
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-secondary: hsl(var(--color-secondary));
|
||||
--color-accent: var(--color-secondary);
|
||||
--color-smooth: var(--color-smooth);
|
||||
|
||||
/* pink */
|
||||
--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-700: calc(hsl(var(--color-blue-hue) - 1) 42% 42%);
|
||||
|
||||
/* green */
|
||||
--color-green-700: #0a714f;
|
||||
|
||||
/* secondary */
|
||||
--color-secondary-100: hsl(var(--color-secondary-hue) 99% 66%);
|
||||
--color-accent-200: hsl(var(--color-secondary-hue), 78%, 60%, 0.32);
|
||||
@@ -53,7 +57,7 @@
|
||||
--color-white: hsl(0 0% 100%);
|
||||
--color-black: hsl(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-50: hsl(var(--color-greyscale-hue) 11% 94%);
|
||||
--color-greyscale-100: hsl(var(--color-greyscale-hue) 6% 90%);
|
||||
@@ -77,8 +81,12 @@
|
||||
|
||||
/* Animations */
|
||||
--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;
|
||||
--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 */
|
||||
--transition: 0.2s;
|
||||
@@ -93,6 +101,18 @@
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes caret-blink {
|
||||
0%,
|
||||
70%,
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
20%,
|
||||
50% {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes blur {
|
||||
0% {
|
||||
filter: blur(5px);
|
||||
@@ -120,6 +140,21 @@
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes scroll {
|
||||
0% {
|
||||
transform: translateX(0);
|
||||
}
|
||||
100% {
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes marquee {
|
||||
to {
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
}
|
||||
|
||||
/* Fonts */
|
||||
--font-family-sans: 'Inter', arial, sans-serif;
|
||||
--font-family-mono: 'Fira Code', monospace;
|
||||
@@ -128,9 +163,12 @@
|
||||
--font-family-archia: 'Archia', arial, sans-serif;
|
||||
|
||||
/* Font sizes */
|
||||
--font-size-x-micro: 0.625rem;
|
||||
--font-size-x-micro--line-height: 0.875rem;
|
||||
--font-size-x-micro--letter-spacing: var(--letter-spacing-tighter);
|
||||
--font-size-micro: 0.75rem;
|
||||
--font-size-micro--line-height: 1rem;
|
||||
--font-size-micro--letter-spacing: var(--letter-spacing-loose);
|
||||
--font-size-micro--letter-spacing: var(--letter-spacing-tighter);
|
||||
--font-size-caption: 0.875rem;
|
||||
--font-size-caption--line-height: 1.375rem;
|
||||
--font-size-caption--letter-spacing: var(--letter-spacing-tight);
|
||||
@@ -190,6 +228,7 @@
|
||||
--color-accent: var(--color-pink-600);
|
||||
--color-badge-bg: var(--color-badge-bg-light);
|
||||
--color-badge-border: var(--color-badge-border-light);
|
||||
--color-smooth: hsl(var(--color-greyscale-hue) 6%, 10%, 0.04);
|
||||
}
|
||||
|
||||
/* dark theme */
|
||||
@@ -198,6 +237,7 @@
|
||||
--color-secondary: var(--color-greyscale-300);
|
||||
--color-badge-bg: var(--color-badge-bg-dark);
|
||||
--color-badge-border: var(--color-badge-border-dark);
|
||||
--color-smooth: hsl(0 0%, 100%, 0.06);
|
||||
}
|
||||
|
||||
/* Container */
|
||||
|
||||
@@ -28,6 +28,51 @@ const redirecter: Handle = async ({ event, resolve }) => {
|
||||
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 response = await resolve(event, {
|
||||
transformPageChunk: ({ html }) => html.replace('%aw_banner_key%', BANNER_KEY)
|
||||
@@ -35,5 +80,5 @@ const bannerRewriter: Handle = async ({ event, resolve }) => {
|
||||
return response;
|
||||
};
|
||||
|
||||
export const handle = sequence(Sentry.sentryHandle(), redirecter, bannerRewriter);
|
||||
export const handle = sequence(Sentry.sentryHandle(), redirecter, bannerRewriter, securityheaders);
|
||||
export const handleError = Sentry.handleErrorWithSentry();
|
||||
|
||||
@@ -69,17 +69,4 @@ export const trackEvent = async (name: string, data: object = {}) => {
|
||||
}
|
||||
};
|
||||
|
||||
export function isTrackingAllowed() {
|
||||
if (ENV.TEST) {
|
||||
return;
|
||||
}
|
||||
if (window.navigator?.doNotTrack) {
|
||||
if (navigator.doNotTrack === '1' || navigator.doNotTrack === 'yes') {
|
||||
return false;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
export const isTrackingAllowed = () => !ENV.TEST;
|
||||
|
||||
|
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 Post from './post/post.svelte';
|
||||
import { anyify } from '$lib/utils/anyify';
|
||||
import Badge from '$lib/components/ui/Badge.svelte';
|
||||
|
||||
/* Basic Animation setup */
|
||||
let scrollInfo = {
|
||||
@@ -508,10 +507,6 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
|
||||
.web-label {
|
||||
margin-block-start: 0.25rem;
|
||||
}
|
||||
}
|
||||
|
||||
h4 {
|
||||
|
||||
@@ -108,10 +108,6 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
|
||||
.web-label {
|
||||
margin-block-start: 0.25rem;
|
||||
}
|
||||
}
|
||||
|
||||
h4 {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { flip } from '$lib/utils/flip';
|
||||
import { crossfade, scale, slide } from 'svelte/transition';
|
||||
import { scale, slide } from 'svelte/transition';
|
||||
import { functionsController } from '.';
|
||||
|
||||
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 { animate } from 'motion';
|
||||
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) {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(resolve, duration);
|
||||
|
||||
@@ -8,12 +8,12 @@
|
||||
<summary
|
||||
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">
|
||||
<span class="icon-cheveron-down" aria-hidden="true" />
|
||||
</div>
|
||||
</summary>
|
||||
<div class="collapsible-content flex flex-col">
|
||||
<div class="collapsible-content text-secondary text-sub-body flex flex-col">
|
||||
<slot />
|
||||
</div>
|
||||
</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';
|
||||
</script>
|
||||
|
||||
<div class="call-to-action">
|
||||
<div class="details">
|
||||
<div
|
||||
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>
|
||||
<a href={url} class="web-button">{label}</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.call-to-action {
|
||||
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;
|
||||
|
||||
.bg {
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
@@ -36,22 +26,6 @@
|
||||
rgba(253, 54, 110, 0.09),
|
||||
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>
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
<script lang="ts">
|
||||
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;
|
||||
let showFeedback = false;
|
||||
@@ -13,7 +16,10 @@
|
||||
async function handleSubmit() {
|
||||
submitting = true;
|
||||
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',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
@@ -22,7 +28,10 @@
|
||||
email,
|
||||
type: feedbackType,
|
||||
route: $page.route.id,
|
||||
comment
|
||||
comment,
|
||||
metaFields: {
|
||||
userId
|
||||
}
|
||||
})
|
||||
});
|
||||
submitting = false;
|
||||
@@ -32,6 +41,7 @@
|
||||
}
|
||||
comment = email = '';
|
||||
submitted = true;
|
||||
setTimeout(() => (showFeedback = false), 500);
|
||||
}
|
||||
|
||||
function reset() {
|
||||
@@ -44,6 +54,10 @@
|
||||
$: if (!showFeedback) {
|
||||
reset();
|
||||
}
|
||||
|
||||
$: if (showFeedback && loggedIn && $user?.email) {
|
||||
email = $user?.email;
|
||||
}
|
||||
</script>
|
||||
|
||||
<section class="web-content-footer">
|
||||
@@ -59,7 +73,7 @@
|
||||
class="web-radio-button"
|
||||
aria-label="helpful"
|
||||
on:click={() => {
|
||||
showFeedback = feedbackType === 'positive' ? false : true;
|
||||
showFeedback = feedbackType !== 'positive';
|
||||
feedbackType = 'positive';
|
||||
}}
|
||||
>
|
||||
@@ -69,7 +83,7 @@
|
||||
class="web-radio-button"
|
||||
aria-label="unhelpful"
|
||||
on:click={() => {
|
||||
showFeedback = feedbackType === 'negative' ? false : true;
|
||||
showFeedback = feedbackType !== 'negative';
|
||||
feedbackType = 'negative';
|
||||
}}
|
||||
>
|
||||
@@ -103,6 +117,7 @@
|
||||
on:submit|preventDefault={handleSubmit}
|
||||
class="web-card is-normal"
|
||||
style="--card-padding:1rem"
|
||||
out:fade={{ duration: 450 }}
|
||||
>
|
||||
<div class="flex flex-col gap-2">
|
||||
<label for="message">
|
||||
|
||||
@@ -32,11 +32,11 @@
|
||||
{ label: 'Solid', href: '/docs/quick-starts/solid' }
|
||||
],
|
||||
Products: [
|
||||
{ label: 'Auth', href: '/docs/products/auth' },
|
||||
{ label: 'Auth', href: '/products/auth' },
|
||||
{ label: 'Databases', href: '/docs/products/databases' },
|
||||
{ label: 'Functions', href: '/docs/products/functions' },
|
||||
{ label: 'Functions', href: '/products/functions' },
|
||||
{ label: 'Messaging', href: '/products/messaging' },
|
||||
{ label: 'Storage', href: '/docs/products/storage' },
|
||||
{ label: 'Storage', href: '/products/storage' },
|
||||
{ label: 'Realtime', href: '/docs/apis/realtime' }
|
||||
],
|
||||
Learn: [
|
||||
|
||||
@@ -1,16 +1,26 @@
|
||||
<script lang="ts">
|
||||
import Button from './ui/Button.svelte';
|
||||
import { PUBLIC_APPWRITE_DASHBOARD } from '$env/static/public';
|
||||
import { classNames } from '$lib/utils/classnames';
|
||||
import { trackEvent } from '$lib/actions/analytics';
|
||||
import { browser } from '$app/environment';
|
||||
|
||||
export let classes = '';
|
||||
|
||||
function getTrackingEventName() {
|
||||
return browser
|
||||
? 'loggedIn' in document.body.dataset
|
||||
? 'Go to console'
|
||||
: 'Get started'
|
||||
: 'Get started';
|
||||
}
|
||||
</script>
|
||||
|
||||
<a
|
||||
class={classNames('web-button web-u-inline-width-100-percent-mobile', classes)}
|
||||
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="block group-[&[data-logged-in]]/body:hidden">Get started</span>
|
||||
|
||||
@@ -87,7 +87,7 @@
|
||||
<ul
|
||||
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">
|
||||
<img {src} {alt} {width} {height} />
|
||||
</li>
|
||||
|
||||
@@ -92,6 +92,13 @@
|
||||
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 {
|
||||
display: flex;
|
||||
@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 { IsLoggedIn } from '$lib/components';
|
||||
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 links: NavLink[];
|
||||
@@ -16,7 +16,7 @@
|
||||
|
||||
<nav class="web-side-nav web-is-not-desktop" class:hidden={!open}>
|
||||
<div class="web-side-nav-wrapper ps-4 pe-4">
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="flex items-center gap-2 px-4">
|
||||
<a href="https://cloud.appwrite.io/register" class="web-button is-secondary flex-1">
|
||||
Sign up
|
||||
</a>
|
||||
@@ -25,11 +25,15 @@
|
||||
<div class="web-side-nav-scroll">
|
||||
<section>
|
||||
<ul>
|
||||
{#each links as { href, label }}
|
||||
{#each links as { href, label, mobileSubmenu }}
|
||||
<li>
|
||||
{#if mobileSubmenu}
|
||||
<svelte:component this={mobileSubmenu} {label} />
|
||||
{:else}
|
||||
<a class="web-side-nav-button" {href}>
|
||||
<span class="text-caption">{label}</span>
|
||||
</a>
|
||||
{/if}
|
||||
</li>
|
||||
{/each}
|
||||
</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">
|
||||
import { PUBLIC_GROWTH_ENDPOINT } from '$env/static/public';
|
||||
|
||||
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',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
name,
|
||||
email,
|
||||
cloud: true /* not optional on the growth endpoint. */
|
||||
email
|
||||
})
|
||||
});
|
||||
return response;
|
||||
|
||||
@@ -3,7 +3,13 @@
|
||||
import { trackEvent } from '$lib/actions/analytics';
|
||||
</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">
|
||||
<section class="web-hero flex items-center justify-center gap-y-8">
|
||||
@@ -152,5 +158,6 @@
|
||||
height: auto;
|
||||
max-inline-size: unset;
|
||||
max-block-size: unset;
|
||||
filter: blur(100px);
|
||||
}
|
||||
</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;
|
||||
}>();
|
||||
|
||||
export let initialLabel: string = 'Select an option';
|
||||
|
||||
const {
|
||||
elements: { trigger, menu, option: optionEl, group: groupEl, groupLabel },
|
||||
states: { open, selected, selectedLabel }
|
||||
@@ -80,6 +82,8 @@
|
||||
duration: 150,
|
||||
y: placement === 'top' ? 4 : -4
|
||||
} as FlyParams;
|
||||
|
||||
console.log({ initialLabel, $selectedLabel });
|
||||
</script>
|
||||
|
||||
<button
|
||||
@@ -93,7 +97,7 @@
|
||||
{#if selectedOption?.icon}
|
||||
<span class={selectedOption.icon} aria-hidden="true" />
|
||||
{/if}
|
||||
<span>{$selectedLabel}</span>
|
||||
<span>{$selectedLabel || initialLabel}</span>
|
||||
</div>
|
||||
<span class="icon-cheveron-{$open ? 'up' : 'down'}" aria-hidden="true" />
|
||||
</button>
|
||||
@@ -115,7 +119,7 @@
|
||||
{#if option.icon}
|
||||
<span class={option.icon} aria-hidden="true" />
|
||||
{/if}
|
||||
<span style:text-transform="capitalize">{option.label}</span>
|
||||
<span>{option.label}</span>
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
<script lang="ts">
|
||||
import { getTocCtx } from './TocRoot.svelte';
|
||||
import TocTree from './TocTree.svelte';
|
||||
import { cubicOut } from 'svelte/easing';
|
||||
import { getTocCtx } from './TocRoot.svelte';
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
export let showToc = true;
|
||||
|
||||
@@ -12,9 +14,63 @@
|
||||
} = getTocCtx();
|
||||
|
||||
$: 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>
|
||||
|
||||
<aside class="web-grid-120-1fr-auto-side" class:web-is-mobile-closed={!showToc}>
|
||||
<svelte:window on:resize={handleResizeForTocTree} />
|
||||
|
||||
<section class="web-mobile-header">
|
||||
<div class="web-is-only-mobile">
|
||||
<button
|
||||
on:click={() => (showToc = !showToc)}
|
||||
class="flex w-full items-center justify-between"
|
||||
>
|
||||
<span class="flex w-full items-center justify-between">
|
||||
<span class="text-description">Table of contents</span>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
class="toggle-icon {showToc ? 'web-icon-close' : 'icon-menu-alt-4'}"
|
||||
></span>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{#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"
|
||||
@@ -100,6 +156,72 @@
|
||||
</svg>
|
||||
</span>
|
||||
</div>
|
||||
<TocTree tree={$headingsTree} activeHeadingIdxs={$activeHeadingIdxs} {item} />
|
||||
|
||||
<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">
|
||||
import { type TableOfContentsItem, type TableOfContentsElements, melt } from '@melt-ui/svelte';
|
||||
import { getTocCtx } from './TocRoot.svelte';
|
||||
import { browser } from '$app/environment';
|
||||
|
||||
export let tree: TableOfContentsItem[] = [];
|
||||
export let activeHeadingIdxs: number[];
|
||||
export let item: TableOfContentsElements['item'];
|
||||
export let level = 1;
|
||||
|
||||
export let showToc = true;
|
||||
|
||||
const {
|
||||
toc: {
|
||||
helpers: { isActive }
|
||||
}
|
||||
} = getTocCtx();
|
||||
|
||||
function onItemClick() {
|
||||
const isDesktop = browser ? window.innerWidth >= 1024 : false;
|
||||
if (!isDesktop) showToc = !showToc;
|
||||
}
|
||||
</script>
|
||||
|
||||
<ul class="web-page-steps-list text-sub-body font-medium">
|
||||
@@ -22,6 +30,7 @@
|
||||
class:is-selected={$isActive(heading.id)}
|
||||
href="#{heading.id}"
|
||||
use:melt={$item(heading.id)}
|
||||
on:click|preventDefault={onItemClick}
|
||||
>
|
||||
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
|
||||
{@html heading.node.innerHTML}
|
||||
|
||||
@@ -7,10 +7,16 @@
|
||||
type EmblaPluginType
|
||||
} from 'embla-carousel';
|
||||
import { WheelGesturesPlugin } from 'embla-carousel-wheel-gestures';
|
||||
import AutoScrollPlugin, { type AutoScrollOptionsType } from 'embla-carousel-auto-scroll';
|
||||
|
||||
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',
|
||||
skipSnaps: true,
|
||||
loop: true
|
||||
@@ -27,7 +33,7 @@
|
||||
else hasNext = false;
|
||||
};
|
||||
|
||||
let plugins: EmblaPluginType[] = [WheelGesturesPlugin()];
|
||||
let plugins: EmblaPluginType[] = [WheelGesturesPlugin(), AutoScrollPlugin(autoScrollOptions)];
|
||||
|
||||
let selectedScrollIndex = 0;
|
||||
const onSelect = (index: number) => {
|
||||
@@ -123,6 +129,7 @@
|
||||
</script>
|
||||
|
||||
<div class="embla web-carousel relative overflow-hidden">
|
||||
{#if showArrows}
|
||||
{#if hasPrev}
|
||||
<button class="web-carousel-button web-carousel-button-start" on:click={onPrev}>
|
||||
<span class="web-icon-arrow-left" aria-hidden="true"></span>
|
||||
@@ -133,14 +140,17 @@
|
||||
<span class="web-icon-arrow-right" aria-hidden="true"></span>
|
||||
</button>
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
<div class="embla__viewport" use:embla={{ options, plugins }} on:emblaInit={onEmblaInit}>
|
||||
<ul class="embla__container flex">
|
||||
<slot />
|
||||
</ul>
|
||||
</div>
|
||||
<div class="shadow" />
|
||||
</div>
|
||||
|
||||
{#if showBullets}
|
||||
<div class="web-carousel-bullets">
|
||||
<ul class="web-carousel-bullets-list">
|
||||
{#each Array.from({ length: emblaApi?.scrollSnapList().length }) as _, i}
|
||||
@@ -155,3 +165,27 @@
|
||||
{/each}
|
||||
</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">
|
||||
<slot />
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<style lang="scss">
|
||||
@use '$scss/abstract' as *;
|
||||
@use '$scss/abstract/functions' as f;
|
||||
|
||||
.slide {
|
||||
cursor: grab;
|
||||
|
||||
&:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
flex: 0 0 50%;
|
||||
min-width: 0;
|
||||
margin-right: f.pxToRem(16);
|
||||
|
||||
@media (max-width: 768px) {
|
||||
flex: 0 0 100%;
|
||||
}
|
||||
|
||||
flex: 0 0 50%;
|
||||
min-width: 0;
|
||||
margin-right: pxToRem(16);
|
||||
&:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
}
|
||||
</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 />
|
||||
</span>
|
||||
|
||||
@@ -34,7 +34,9 @@
|
||||
}
|
||||
|
||||
const CTX_KEY = Symbol('docs');
|
||||
const TUT_CTX_KEY = Symbol('tut-docs');
|
||||
export const isInDocs = () => getContext<boolean>(CTX_KEY) ?? false;
|
||||
export const isInTutorialDocs = () => getContext<boolean>(TUT_CTX_KEY) ?? false;
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
@@ -43,6 +45,7 @@
|
||||
import { getContext, setContext } from 'svelte';
|
||||
import { GITHUB_REPO_LINK, GITHUB_STARS } from '$lib/constants';
|
||||
import { PUBLIC_APPWRITE_DASHBOARD } from '$env/static/public';
|
||||
import { page } from '$app/stores';
|
||||
|
||||
export let variant: DocsLayoutVariant = 'default';
|
||||
export let isReferences = false;
|
||||
@@ -63,7 +66,9 @@
|
||||
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) => {
|
||||
if (e.key === 'Escape' && ($layoutState.showReferences || $layoutState.showSidenav)) {
|
||||
|
||||
@@ -13,14 +13,17 @@
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { scrollToTop } from '$lib/actions/scrollToTop';
|
||||
|
||||
import { setContext } from 'svelte';
|
||||
import { writable } from 'svelte/store';
|
||||
import { Feedback } from '$lib/components';
|
||||
import { scrollToTop } from '$lib/actions/scrollToTop';
|
||||
|
||||
export let title: string;
|
||||
export let toc: Array<TocItem>;
|
||||
export let back: string | undefined = undefined;
|
||||
export let date: string | undefined = undefined;
|
||||
|
||||
const reducedArticleSize = setContext('articleHasNumericBadge', writable(false));
|
||||
</script>
|
||||
|
||||
<main class="contents" id="main">
|
||||
@@ -59,8 +62,9 @@
|
||||
</div>
|
||||
<div class="web-article-header-end" />
|
||||
</header>
|
||||
<div class="web-article-content">
|
||||
<div class="web-article-content" class:web-reduced-article-size={$reducedArticleSize}>
|
||||
<slot />
|
||||
|
||||
<Feedback {date} />
|
||||
</div>
|
||||
<aside class="web-references-menu ps-6">
|
||||
@@ -110,3 +114,12 @@
|
||||
</aside>
|
||||
</article>
|
||||
</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 { TocItem } from './DocsArticle.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 back: string;
|
||||
@@ -31,13 +32,66 @@
|
||||
|
||||
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(() => {
|
||||
if (!slotContent) return;
|
||||
|
||||
// dynamically modify all `label` headers to `body`.
|
||||
slotContent.querySelectorAll<HTMLHeadingElement>('h2.web-label').forEach((header) => {
|
||||
header.classList.replace('web-label', 'web-main-body-500');
|
||||
slotContent.querySelectorAll<HTMLHeadingElement>('h2.text-label').forEach((header) => {
|
||||
header.classList.replace('text-label', 'web-main-body-500');
|
||||
});
|
||||
|
||||
preSelectItemOnInit();
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -77,7 +131,9 @@
|
||||
/>
|
||||
</a>
|
||||
{/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 class="web-article-header-end" />
|
||||
@@ -87,16 +143,18 @@
|
||||
<section class="web-article-content-sub-section">
|
||||
<header class="web-article-content-header">
|
||||
<span class="web-numeric-badge">{currentStep}</span>
|
||||
<div class="tutorial-heading">
|
||||
<Heading level={1} id={currentStepItem.href} step={currentStep}>
|
||||
{getCorrectTitle(currentStepItem, 1)}
|
||||
</Heading>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="u-padding-block-start-32" bind:this={slotContent}>
|
||||
<div class="web-u-padding-block-start-32" bind:this={slotContent}>
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
<div class="flex justify-between">
|
||||
<div class="web-u-padding-block-start-32 flex justify-between">
|
||||
{#if prevStep}
|
||||
<a href={prevStep.href} class="web-button is-text previous-step-anchor">
|
||||
<span class="icon-cheveron-left" aria-hidden="true" />
|
||||
@@ -135,6 +193,7 @@
|
||||
<ol class="web-references-menu-list">
|
||||
{#each tutorials as tutorial, index}
|
||||
{@const isCurrentStep = currentStep === tutorial.step}
|
||||
{@const absoluteToc = toc.slice(1)}
|
||||
<li class="web-references-menu-item">
|
||||
<a
|
||||
href={tutorial.href}
|
||||
@@ -148,14 +207,16 @@
|
||||
>{index === 0 ? 'Introduction' : tutorial.title}</span
|
||||
>
|
||||
</a>
|
||||
{#if isCurrentStep && toc.length}
|
||||
{#if isCurrentStep && absoluteToc.length}
|
||||
<ol
|
||||
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">
|
||||
<a
|
||||
href={parent.href}
|
||||
on:click|preventDefault={() =>
|
||||
scrollToItem(parent, innerIndex)}
|
||||
class="web-references-menu-link is-inner"
|
||||
class:tutorial-scroll-indicator={parent.selected}
|
||||
class:is-selected={parent.selected}
|
||||
@@ -208,4 +269,49 @@
|
||||
background: 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>
|
||||
|
||||
@@ -1,11 +1,6 @@
|
||||
<script lang="ts" context="module">
|
||||
import { writable } from 'svelte/store';
|
||||
|
||||
export type NavLink = {
|
||||
label: string;
|
||||
href: string;
|
||||
showBadge?: boolean;
|
||||
};
|
||||
export const isHeaderHidden = writable(false);
|
||||
export const isMobileNavOpen = writable(false);
|
||||
const initialized = writable(false);
|
||||
@@ -21,12 +16,13 @@
|
||||
import { addEventListener } from '@melt-ui/svelte/internal/helpers';
|
||||
import { onMount } from 'svelte';
|
||||
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 AnnouncementBanner from '$lib/components/AnnouncementBanner.svelte';
|
||||
import InitBanner from '$lib/components/InitBanner.svelte';
|
||||
import Button from '$lib/components/ui/Button.svelte';
|
||||
import { trackEvent } from '$lib/actions/analytics';
|
||||
import MainNav, { type NavLink } from '$lib/components/MainNav.svelte';
|
||||
|
||||
export let omitMainId = false;
|
||||
let theme: 'light' | 'dark' | null = 'dark';
|
||||
@@ -102,6 +98,11 @@
|
||||
});
|
||||
|
||||
let navLinks: NavLink[] = [
|
||||
{
|
||||
label: 'Products',
|
||||
submenu: ProductsSubmenu,
|
||||
mobileSubmenu: ProductsMobileSubmenu
|
||||
},
|
||||
{
|
||||
label: 'Docs',
|
||||
href: '/docs'
|
||||
@@ -240,23 +241,7 @@
|
||||
width="130"
|
||||
/>
|
||||
</a>
|
||||
<nav class="web-main-header-nav" aria-label="Main">
|
||||
<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>
|
||||
<MainNav initialized={$initialized} links={navLinks} />
|
||||
</div>
|
||||
<div class="web-main-header-end">
|
||||
<a
|
||||
@@ -303,25 +288,4 @@
|
||||
.is-special-padding {
|
||||
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>
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
</script>
|
||||
|
||||
<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}
|
||||
href={groupItem.href}
|
||||
target={groupItem.openInNewTab ? '_blank' : '_self'}
|
||||
|
||||
@@ -57,6 +57,15 @@ export enum Platform {
|
||||
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> = {
|
||||
[Platform.ClientApple]: 'Apple',
|
||||
[Platform.ClientFlutter]: 'Flutter',
|
||||
|
||||
@@ -199,28 +199,19 @@ export function getSchema(id: string, api: OpenAPIV3.Document): OpenAPIV3.Schema
|
||||
}
|
||||
|
||||
const specs = import.meta.glob(
|
||||
'$appwrite/app/config/specs/open-api3*-(client|server|console).json',
|
||||
{
|
||||
query: '?raw',
|
||||
import: 'default'
|
||||
}
|
||||
'$appwrite/app/config/specs/open-api3*-(client|server|console).json'
|
||||
);
|
||||
async function getSpec(version: string, platform: string) {
|
||||
|
||||
export async function getApi(version: string, platform: string): Promise<OpenAPIV3.Document> {
|
||||
const isClient = platform.startsWith('client-');
|
||||
const isServer = platform.startsWith('server-');
|
||||
const target = `/node_modules/@appwrite.io/repo/app/config/specs/open-api3-${version}-${
|
||||
isServer ? 'server' : isClient ? 'client' : 'console'
|
||||
}.json`;
|
||||
|
||||
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(
|
||||
'/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');
|
||||
}
|
||||
|
||||
const description = descriptions[target]();
|
||||
|
||||
return description;
|
||||
return descriptions[target]();
|
||||
}
|
||||
|
||||
export async function getService(
|
||||
@@ -327,9 +316,11 @@ export async function getService(
|
||||
continue;
|
||||
}
|
||||
|
||||
const demo = await examples[path]();
|
||||
|
||||
data.methods.push({
|
||||
id: operation['x-appwrite'].method,
|
||||
demo: await examples[path](),
|
||||
demo: demo ?? '',
|
||||
title: operation.summary ?? '',
|
||||
description: operation.description ?? '',
|
||||
parameters: parameters ?? [],
|
||||
@@ -370,9 +361,9 @@ export function resolveReference(
|
||||
|
||||
export const generateExample = (
|
||||
schema: OpenAPIV3.SchemaObject,
|
||||
api: OpenAPIV3.Document<{}>,
|
||||
api: OpenAPIV3.Document<object>,
|
||||
modelType: ModelType = ModelType.REST
|
||||
): Object => {
|
||||
): object => {
|
||||
const properties = Object.keys(schema.properties ?? {}).map((key) => {
|
||||
const name = 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 }) {
|
||||
let isFound = false;
|
||||
let difficulty, readtime;
|
||||
let difficulty: string | undefined, readtime: string | undefined;
|
||||
|
||||
return Object.entries(data.tutorials)
|
||||
.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>
|
||||
</a>
|
||||
<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">
|
||||
{name}
|
||||
</h1>
|
||||
<p class="web-category-header-description text-description">
|
||||
<p class="text-secondary text-description">
|
||||
{description}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
import { Root, Slide } from '$lib/components/carousel';
|
||||
import FooterNav from '$lib/components/FooterNav.svelte';
|
||||
import MainFooter from '$lib/components/MainFooter.svelte';
|
||||
import ProductsGrid from '$lib/components/ProductsGrid.svelte';
|
||||
import { Main } from '$lib/layouts';
|
||||
import { DEFAULT_HOST } from '$lib/utils/metadata';
|
||||
import type { Integration } from '$routes/integrations/+page';
|
||||
@@ -44,7 +43,7 @@
|
||||
class="web-u-sep-block-end pb-0"
|
||||
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-carousel-wrapper">
|
||||
<a href="/integrations" class="web-button is-text mb-12">
|
||||
@@ -181,13 +180,8 @@
|
||||
</Main>
|
||||
|
||||
<style lang="scss">
|
||||
@use '$scss/abstract' as *;
|
||||
|
||||
.cta {
|
||||
min-height: pxToRem(560);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
@use '$scss/abstract/functions' as f;
|
||||
@use '$scss/abstract/variables/devices';
|
||||
|
||||
.web-pre-footer-bg {
|
||||
position: absolute;
|
||||
@@ -199,24 +193,15 @@
|
||||
max-inline-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 {
|
||||
@media #{$break1} {
|
||||
@media #{devices.$break1} {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
@media #{$break2open} {
|
||||
@media #{devices.$break2open} {
|
||||
display: grid;
|
||||
gap: pxToRem(64);
|
||||
gap: f.pxToRem(64);
|
||||
grid-template-columns: repeat(12, 1fr);
|
||||
}
|
||||
|
||||
|
||||
@@ -72,23 +72,14 @@
|
||||
<header class="web-grid-120-1fr-auto-header">
|
||||
<h1 class="text-title font-aeonik-pro text-primary">{title}</h1>
|
||||
</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
|
||||
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 />
|
||||
|
||||
<TocNav bind:showToc />
|
||||
|
||||
<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 -->
|
||||
<h2 aria-hidden="true">Introduction</h2>
|
||||
<slot />
|
||||
@@ -107,7 +98,20 @@
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.toc-btn {
|
||||
transition: translate 0.3s ease;
|
||||
@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>
|
||||
|
||||
@@ -236,7 +236,7 @@
|
||||
</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="container">
|
||||
<h3 class="text-label text-primary">Read next</h3>
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
import { copy } from '$lib/utils/copy';
|
||||
import type { CodeContext } from '../tags/MultiCode.svelte';
|
||||
import { melt } from '@melt-ui/svelte';
|
||||
import { isInTutorialDocs } from '$lib/layouts/Docs.svelte';
|
||||
|
||||
export let content: string;
|
||||
export let toCopy: string | undefined = undefined;
|
||||
@@ -15,6 +16,7 @@
|
||||
export let withLineNumbers = true;
|
||||
export let badge: string | null = null;
|
||||
|
||||
const inTutorialDocs = isInTutorialDocs();
|
||||
const insideMultiCode = hasContext('multi-code');
|
||||
const selected = insideMultiCode ? getContext<CodeContext>('multi-code').selected : null;
|
||||
|
||||
@@ -61,7 +63,11 @@
|
||||
{@html result}
|
||||
{/if}
|
||||
{: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">
|
||||
<div class="web-code-snippet-header-start">
|
||||
{#if badgeValue}
|
||||
@@ -98,3 +104,9 @@
|
||||
</div>
|
||||
</section>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.no-top-margin {
|
||||
margin-top: unset !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -6,7 +6,13 @@
|
||||
|
||||
const isExternal = ['http://', 'https://'].some((prefix) => href.startsWith(prefix));
|
||||
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();
|
||||
|
||||
|
||||
@@ -3,10 +3,11 @@
|
||||
import { isInPolicy } from '$markdoc/layouts/Policy.svelte';
|
||||
import { getContext, hasContext } from '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 inDocs = isInDocs();
|
||||
const inTutorialDocs = isInTutorialDocs();
|
||||
const inPolicy = isInPolicy();
|
||||
const inChangelog = isInChangelog();
|
||||
const inTable = isInTable();
|
||||
@@ -18,6 +19,7 @@
|
||||
if (inDocs) return 'text-paragraph-md mb-8';
|
||||
if (inPolicy) return 'text-paragraph-md mb-4';
|
||||
if (inTable) return 'text-paragraph-md';
|
||||
if (inTutorialDocs) return 'text-paragraph-md mb-2';
|
||||
if (inChangelog) return 'text-paragraph-lg mb-4 font-normal';
|
||||
return 'text-paragraph-lg mb-8';
|
||||
})();
|
||||
|
||||
@@ -5,12 +5,6 @@
|
||||
export let url: string = PUBLIC_APPWRITE_DASHBOARD;
|
||||
</script>
|
||||
|
||||
<div class="call-to-action">
|
||||
<div class="py-12">
|
||||
<a href={url} class="web-button">{label}</a>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.call-to-action {
|
||||
margin: 48px 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,9 +1,15 @@
|
||||
<script lang="ts">
|
||||
import { type Writable } from 'svelte/store';
|
||||
import Heading from '../nodes/Heading.svelte';
|
||||
import { getContext, hasContext } from 'svelte';
|
||||
|
||||
export let id: string;
|
||||
export let step: number;
|
||||
export let title: string;
|
||||
|
||||
if (hasContext('articleHasNumericBadge')) {
|
||||
getContext<Writable<boolean>>('articleHasNumericBadge').set(true);
|
||||
}
|
||||
</script>
|
||||
|
||||
<section class="web-article-content-section is-with-line">
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import Select from '$lib/components/Select.svelte';
|
||||
import { classNames } from '$lib/utils/classnames';
|
||||
import { createTabs } from '@melt-ui/svelte';
|
||||
import { setContext } from 'svelte';
|
||||
import { writable } from 'svelte/store';
|
||||
@@ -27,20 +29,82 @@
|
||||
|
||||
<div class="web-card is-normal mt-4" {...$root} use:root>
|
||||
<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;"
|
||||
>
|
||||
<ul class="tabs-list flex items-center gap-4" {...$list} use:list>
|
||||
{#each $ctx.triggers.entries() as [id, title]}
|
||||
<li class="tabs-item rounded-t-[0.625rem] hover:bg-white/4">
|
||||
<ul class="tabs-list hidden items-center gap-4 sm:flex" {...$list} use:list>
|
||||
{#each Array.from($ctx.triggers.entries()).slice(0, 7) 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="tabs-button cursor-pointer bg-clip-padding py-[0.625rem] px-1 font-light outline-none"
|
||||
class:is-selected={$value === id}
|
||||
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(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>
|
||||
</div>
|
||||
<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 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.
|
||||
|
||||
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",
|
||||
"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) {
|
||||
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';
|
||||
|
||||
applyTheme(initialTheme);
|
||||
|
||||
@@ -11,11 +11,12 @@
|
||||
import MainFooter from '../lib/components/MainFooter.svelte';
|
||||
import DeveloperCard from './DeveloperCard.svelte';
|
||||
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 GradientText from '$lib/components/ui/GradientText.svelte';
|
||||
import Badge from '$lib/components/ui/Badge.svelte';
|
||||
import { trackEvent } from '$lib/actions/analytics';
|
||||
import AppwriteIn100Seconds from '$lib/components/AppwriteIn100Seconds.svelte';
|
||||
|
||||
const title = 'Appwrite - Build like a team of hundreds';
|
||||
const description = DEFAULT_DESCRIPTION;
|
||||
@@ -92,7 +93,7 @@
|
||||
<enhanced:img
|
||||
style="width:1466px; height:804px; transform:rotate(150.348deg); opacity: 0.65; filter: blur(127.5px);
|
||||
max-block-size: unset; max-inline-size: unset;"
|
||||
src="./top-page-dark.png"
|
||||
src="./top-page-dark.webp"
|
||||
alt=""
|
||||
/>
|
||||
</div>
|
||||
@@ -103,7 +104,7 @@
|
||||
style="top: 22rem; left: 54%; translate: calc(-50% - 900px); width: 75.9375rem;"
|
||||
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
|
||||
@@ -112,7 +113,7 @@
|
||||
class:web-u-hide-mobile={$isMobileNavOpen}
|
||||
>
|
||||
<div style="left: 0;">
|
||||
<img src="/images/bgs/hero-lines-2.png" alt="" />
|
||||
<img src="/images/bgs/hero-lines-2.webp" alt="" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -129,9 +130,7 @@
|
||||
<span class="web-icon-star shrink-0" aria-hidden="true" />
|
||||
<span class="text-caption shrink-0 font-medium">New</span>
|
||||
<div class="web-hero-banner-button-sep" />
|
||||
<span class="text-caption web-u-trim-1"
|
||||
>Introducing Database Backups</span
|
||||
>
|
||||
<span class="text-caption web-u-trim-1">Introducing Database Backups</span>
|
||||
|
||||
<span class="web-icon-arrow-right shrink-0" aria-hidden="true" />
|
||||
</a>
|
||||
@@ -147,14 +146,17 @@
|
||||
Functions, Storage, and Messaging to your projects using the frameworks
|
||||
and languages of your choice.
|
||||
</svelte:fragment>
|
||||
<div class="mt-8 flex flex-col gap-4 sm:flex-row" slot="cta">
|
||||
<a
|
||||
href={PUBLIC_APPWRITE_DASHBOARD}
|
||||
class="web-button mt-8 w-full lg:w-fit"
|
||||
slot="cta"
|
||||
class="web-button w-full lg:w-fit"
|
||||
on:click={() => trackEvent('Get started in hero')}
|
||||
>
|
||||
Get started
|
||||
</a>
|
||||
|
||||
<AppwriteIn100Seconds />
|
||||
</div>
|
||||
</Hero>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
@@ -35,6 +35,8 @@
|
||||
selectedMap = selectedMap;
|
||||
};
|
||||
};
|
||||
|
||||
let showToc = false;
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
@@ -64,19 +66,11 @@
|
||||
<div class="web-grid-120-1fr-auto">
|
||||
<header class="web-grid-120-1fr-auto-header">
|
||||
<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>
|
||||
<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">
|
||||
<section>
|
||||
<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>
|
||||
|
||||
@@ -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
|
||||
category: tutorial
|
||||
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.
|
||||
|
||||
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
|
||||
@@ -214,12 +215,13 @@ If something goes wrong, `reflog` can provide a trail to recover lost changes, m
|
||||
|
||||
# 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.
|
||||
|
||||
# More resources
|
||||
|
||||
- [How to implement Sign in with GitHub](https://appwrite.io/blog/post/implement-sign-in-with-github)
|
||||
- [10 new Git commands you should start using today](https://appwrite.io/blog/post/10-git-commands-you-should-start-using?dofollow=true)
|
||||
- [Building a currency converter API with Deno 2 and Appwrite](https://appwrite.io/blog/post/build-a-currency-converter-with-deno)
|
||||
- [Local serverless function development with the new Appwrite CLI](https://appwrite.io/blog/post/functions-local-development-guide)
|
||||
@@ -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)
|
||||
---
|
||||
- 10GB bandwidth
|
||||
- 5GB bandwidth
|
||||
- 300GB bandwidth
|
||||
---
|
||||
- 2GB storage
|
||||
|
||||
@@ -8,6 +8,7 @@ timeToRead: 7
|
||||
author: aditya-oberai
|
||||
category: product
|
||||
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.
|
||||
|
||||
@@ -65,7 +65,7 @@ Cloudinary is a cloud service that focuses on managing media assets, especially
|
||||
| **FEATURE** | **STORAGE** | **CLOUDINARY** |
|
||||
| --- | --- | --- |
|
||||
| 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 |
|
||||
| Open source | Yes ✅ | No ❌ |
|
||||
| Support | Discord and email, dedicated channels for startups | Community and email, paid support options |
|
||||
|
||||
@@ -8,6 +8,7 @@ timeToRead: 8
|
||||
author: aditya-oberai
|
||||
category: product
|
||||
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.
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ cover: /images/blog/best-pagination-technique/cover.png
|
||||
timeToRead: 8
|
||||
author: matej-baco
|
||||
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?
|
||||
|
||||
@@ -8,6 +8,7 @@ timeToRead: 3
|
||||
author: dennis-ivy
|
||||
category: tutorial
|
||||
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.
|
||||
|
||||
@@ -6,7 +6,7 @@ date: 2023-12-02
|
||||
cover: /images/blog/kcollect.png
|
||||
timeToRead: 5
|
||||
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.
|
||||
@@ -6,7 +6,7 @@ date: 2024-05-23
|
||||
cover: /images/blog/case-study-langx/cover.png
|
||||
timeToRead: 5
|
||||
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.
|
||||
@@ -6,7 +6,7 @@ date: 2024-01-19
|
||||
cover: /images/blog/majik-kids.png
|
||||
timeToRead: 7
|
||||
author: aditya-oberai
|
||||
category: case-studies
|
||||
category: customer-stories
|
||||
---
|
||||
|
||||
# Ideating an alternative content platform for children
|
||||
@@ -6,7 +6,7 @@ date: 2024-03-04
|
||||
cover: /images/blog/case-study-myshoefitter.png
|
||||
timeToRead: 5
|
||||
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!” \
|
||||
@@ -6,7 +6,7 @@ date: 2024-04-12
|
||||
cover: /images/blog/case-study-open-mind/cover.png
|
||||
timeToRead: 5
|
||||
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.
|
||||
@@ -6,7 +6,7 @@ date: 2023-12-02
|
||||
cover: /images/blog/smartbee.png
|
||||
timeToRead: 5
|
||||
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.
|
||||
@@ -6,7 +6,7 @@ date: 2024-07-09
|
||||
cover: /images/blog/case-study-undo/cover.png
|
||||
timeToRead: 6
|
||||
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.
|
||||
@@ -7,6 +7,7 @@ cover: /images/blog/defying-the-laws-of-web-animations/cover.png
|
||||
timeToRead: 10
|
||||
author: thomas-g-lopes
|
||||
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.
|
||||
@@ -173,8 +174,6 @@ The video above showcases both Motion and Svelte transitions in action. The tabl
|
||||
{/if}
|
||||
```
|
||||
|
||||
{% call_to_action /%}
|
||||
|
||||
## Transitioning between sections
|
||||
|
||||
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)
|
||||