merge main

This commit is contained in:
Jesse Winton
2024-08-09 15:42:20 -04:00
574 changed files with 10635 additions and 5064 deletions

View File

@@ -31,6 +31,8 @@ jobs:
push: true push: true
tags: ghcr.io/appwrite/website:${{ env.TAG }} tags: ghcr.io/appwrite/website:${{ env.TAG }}
build-args: | build-args: |
"PUBLIC_APPWRITE_ENDPOINT=${{ secrets.PUBLIC_APPWRITE_ENDPOINT }}"
"PUBLIC_APPWRITE_DASHBOARD=${{ secrets.PUBLIC_APPWRITE_DASHBOARD }}"
"PUBLIC_APPWRITE_PROJECT_ID=${{ vars.PUBLIC_APPWRITE_PROJECT_ID }}" "PUBLIC_APPWRITE_PROJECT_ID=${{ vars.PUBLIC_APPWRITE_PROJECT_ID }}"
"PUBLIC_APPWRITE_DB_MAIN_ID=${{ vars.PUBLIC_APPWRITE_DB_MAIN_ID }}" "PUBLIC_APPWRITE_DB_MAIN_ID=${{ vars.PUBLIC_APPWRITE_DB_MAIN_ID }}"
"PUBLIC_APPWRITE_COL_THREADS_ID=${{ vars.PUBLIC_APPWRITE_COL_THREADS_ID }}" "PUBLIC_APPWRITE_COL_THREADS_ID=${{ vars.PUBLIC_APPWRITE_COL_THREADS_ID }}"

View File

@@ -33,6 +33,8 @@ jobs:
push: true push: true
tags: ghcr.io/appwrite/website:${{ env.TAG }} tags: ghcr.io/appwrite/website:${{ env.TAG }}
build-args: | build-args: |
"PUBLIC_APPWRITE_ENDPOINT=${{ secrets.PUBLIC_APPWRITE_ENDPOINT }}"
"PUBLIC_APPWRITE_DASHBOARD=${{ secrets.PUBLIC_APPWRITE_DASHBOARD }}"
"PUBLIC_APPWRITE_PROJECT_ID=${{ vars.PUBLIC_APPWRITE_PROJECT_ID }}" "PUBLIC_APPWRITE_PROJECT_ID=${{ vars.PUBLIC_APPWRITE_PROJECT_ID }}"
"PUBLIC_APPWRITE_DB_MAIN_ID=${{ vars.PUBLIC_APPWRITE_DB_MAIN_ID }}" "PUBLIC_APPWRITE_DB_MAIN_ID=${{ vars.PUBLIC_APPWRITE_DB_MAIN_ID }}"
"PUBLIC_APPWRITE_COL_THREADS_ID=${{ vars.PUBLIC_APPWRITE_COL_THREADS_ID }}" "PUBLIC_APPWRITE_COL_THREADS_ID=${{ vars.PUBLIC_APPWRITE_COL_THREADS_ID }}"

View File

@@ -1,5 +1,11 @@
FROM node:20-bullseye as base FROM node:20-bullseye as base
ARG PUBLIC_APPWRITE_ENDPOINT
ENV PUBLIC_APPWRITE_ENDPOINT ${PUBLIC_APPWRITE_ENDPOINT}
ARG PUBLIC_APPWRITE_DASHBOARD
ENV PUBLIC_APPWRITE_DASHBOARD ${PUBLIC_APPWRITE_DASHBOARD}
ARG PUBLIC_APPWRITE_COL_MESSAGES_ID ARG PUBLIC_APPWRITE_COL_MESSAGES_ID
ENV PUBLIC_APPWRITE_COL_MESSAGES_ID ${PUBLIC_APPWRITE_COL_MESSAGES_ID} ENV PUBLIC_APPWRITE_COL_MESSAGES_ID ${PUBLIC_APPWRITE_COL_MESSAGES_ID}

View File

@@ -1,58 +1,60 @@
services: services:
traefik: traefik:
image: traefik:2.9 image: traefik:2.9
command: command:
- --log.level=DEBUG - --log.level=DEBUG
- --api.insecure=true - --api.insecure=true
- --providers.docker=true - --providers.docker=true
- --providers.docker.exposedByDefault=false - --providers.docker.exposedByDefault=false
- --entrypoints.web.address=:80 - --entrypoints.web.address=:80
- --entrypoints.websecure.address=:443 - --entrypoints.websecure.address=:443
- --providers.docker.constraints=Label(`traefik.constraint-label-stack`,`homepage`) - --providers.docker.constraints=Label(`traefik.constraint-label-stack`,`homepage`)
- --accesslog=true - --accesslog=true
labels: labels:
- traefik.http.routers.traefik.middlewares=traefik-compress - traefik.http.routers.traefik.middlewares=traefik-compress
- traefik.http.middlewares.traefik-compress.compress=true - traefik.http.middlewares.traefik-compress.compress=true
ports: ports:
- 80:80 - 80:80
- 8080:8080 - 8080:8080
volumes: volumes:
# - /letsencrypt:/letsencrypt # - /letsencrypt:/letsencrypt
- /var/run/docker.sock:/var/run/docker.sock - /var/run/docker.sock:/var/run/docker.sock
networks: networks:
- homepage - homepage
homepage: homepage:
image: homepage-dev image: homepage-dev
build: build:
context: . context: .
args: args:
- PUBLIC_APPWRITE_PROJECT_INIT_ID=$PUBLIC_APPWRITE_PROJECT_INIT_ID - PUBLIC_APPWRITE_ENDPOINT=$PUBLIC_APPWRITE_ENDPOINT
- PUBLIC_APPWRITE_PROJECT_ID=$PUBLIC_APPWRITE_PROJECT_ID - PUBLIC_APPWRITE_DASHBOARD=$PUBLIC_APPWRITE_DASHBOARD
- PUBLIC_APPWRITE_DB_MAIN_ID=$PUBLIC_APPWRITE_DB_MAIN_ID - PUBLIC_APPWRITE_PROJECT_INIT_ID=$PUBLIC_APPWRITE_PROJECT_INIT_ID
- PUBLIC_APPWRITE_COL_THREADS_ID=$PUBLIC_APPWRITE_COL_THREADS_ID - PUBLIC_APPWRITE_PROJECT_ID=$PUBLIC_APPWRITE_PROJECT_ID
- PUBLIC_APPWRITE_COL_MESSAGES_ID=$PUBLIC_APPWRITE_COL_MESSAGES_ID - PUBLIC_APPWRITE_DB_MAIN_ID=$PUBLIC_APPWRITE_DB_MAIN_ID
- PUBLIC_APPWRITE_FN_TLDR_ID=$PUBLIC_APPWRITE_FN_TLDR_ID - PUBLIC_APPWRITE_COL_THREADS_ID=$PUBLIC_APPWRITE_COL_THREADS_ID
restart: always - PUBLIC_APPWRITE_COL_MESSAGES_ID=$PUBLIC_APPWRITE_COL_MESSAGES_ID
networks: - PUBLIC_APPWRITE_FN_TLDR_ID=$PUBLIC_APPWRITE_FN_TLDR_ID
- homepage restart: always
labels: networks:
- traefik.enable=true - homepage
- traefik.constraint-label-stack=homepage labels:
- traefik.docker.network=appwrite - traefik.enable=true
- traefik.http.middlewares.appwrite_middlewares.compress=true - traefik.constraint-label-stack=homepage
- traefik.http.services.appwrite_service.loadbalancer.server.port=3000 - traefik.docker.network=appwrite
#http - traefik.http.middlewares.appwrite_middlewares.compress=true
- traefik.http.routers.appwrite.entrypoints=web - traefik.http.services.appwrite_service.loadbalancer.server.port=3000
- traefik.http.routers.appwrite.rule=PathPrefix(`/`) #http
- traefik.http.routers.appwrite.service=appwrite_service - traefik.http.routers.appwrite.entrypoints=web
- traefik.http.routers.appwrite.middlewares=appwrite_middlewares - traefik.http.routers.appwrite.rule=PathPrefix(`/`)
# https - traefik.http.routers.appwrite.service=appwrite_service
- traefik.http.routers.appwrite_secure.entrypoints=websecure - traefik.http.routers.appwrite.middlewares=appwrite_middlewares
- traefik.http.routers.appwrite_secure.rule=PathPrefix(`/`) # https
- traefik.http.routers.appwrite_secure.service=appwrite_service - traefik.http.routers.appwrite_secure.entrypoints=websecure
- traefik.http.routers.appwrite_secure.tls=true - traefik.http.routers.appwrite_secure.rule=PathPrefix(`/`)
- traefik.http.routers.appwrite_secure.middlewares=appwrite_middlewares - traefik.http.routers.appwrite_secure.service=appwrite_service
- traefik.http.routers.appwrite_secure.tls=true
- traefik.http.routers.appwrite_secure.middlewares=appwrite_middlewares
networks: networks:
homepage: homepage:

View File

@@ -25,6 +25,7 @@
"@sentry/sveltekit": "^8.12.0", "@sentry/sveltekit": "^8.12.0",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"h3": "^1.12.0", "h3": "^1.12.0",
"sharp": "^0.33.4",
"tailwind-merge": "^2.4.0" "tailwind-merge": "^2.4.0"
}, },
"prettier": { "prettier": {
@@ -38,7 +39,7 @@
"@appwrite.io/pink": "~0.16.0", "@appwrite.io/pink": "~0.16.0",
"@appwrite.io/pink-icons": "~0.16.0", "@appwrite.io/pink-icons": "~0.16.0",
"@appwrite.io/repo": "github:appwrite/appwrite#main", "@appwrite.io/repo": "github:appwrite/appwrite#main",
"@internationalized/date": "3.5.0", "@internationalized/date": "^3.5.0",
"@melt-ui/pp": "^0.3.2", "@melt-ui/pp": "^0.3.2",
"@melt-ui/svelte": "^0.74.4", "@melt-ui/svelte": "^0.74.4",
"@playwright/test": "^1.44.1", "@playwright/test": "^1.44.1",
@@ -54,6 +55,9 @@
"@typescript-eslint/eslint-plugin": "^7.13.1", "@typescript-eslint/eslint-plugin": "^7.13.1",
"@typescript-eslint/parser": "^7.13.1", "@typescript-eslint/parser": "^7.13.1",
"dequal": "^2.0.3", "dequal": "^2.0.3",
"embla-carousel": "^8.1.5",
"embla-carousel-svelte": "^8.1.5",
"embla-carousel-wheel-gestures": "^8.0.1",
"eslint": "^8.57.0", "eslint": "^8.57.0",
"eslint-config-prettier": "^8.10.0", "eslint-config-prettier": "^8.10.0",
"eslint-plugin-svelte": "^2.40.0", "eslint-plugin-svelte": "^2.40.0",
@@ -68,8 +72,8 @@
"prettier": "^3.3.3", "prettier": "^3.3.3",
"prettier-plugin-svelte": "^3.2.5", "prettier-plugin-svelte": "^3.2.5",
"prettier-plugin-tailwindcss": "^0.6.5", "prettier-plugin-tailwindcss": "^0.6.5",
"remeda": "^2.10.0",
"sass": "^1.77.6", "sass": "^1.77.6",
"sharp": "^0.33.4",
"svelte": "^4.2.18", "svelte": "^4.2.18",
"svelte-check": "^3.8.1", "svelte-check": "^3.8.1",
"svelte-markdoc-preprocess": "^2.0.0", "svelte-markdoc-preprocess": "^2.0.0",

3257
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M3.000 12.000 L 3.000 21.000 12.000 21.000 L 21.000 21.000 21.000 12.000 L 21.000 3.000 12.000 3.000 L 3.000 3.000 3.000 12.000 M9.813 7.550 C 9.928 7.832,11.602 11.179,11.718 11.360 C 11.782 11.459,11.835 11.567,11.837 11.600 C 11.839 11.633,11.886 11.758,11.942 11.877 C 12.043 12.093,12.043 12.094,12.107 12.003 C 12.142 11.952,12.228 11.774,12.299 11.606 C 12.369 11.438,12.479 11.216,12.543 11.114 C 12.607 11.012,13.047 10.135,13.520 9.165 L 14.380 7.402 15.051 7.401 L 15.722 7.400 15.666 7.490 C 15.636 7.540,14.933 8.859,14.105 10.422 L 12.600 13.265 12.600 15.132 L 12.600 17.000 12.000 17.000 L 11.400 17.000 11.400 15.102 L 11.400 13.204 9.844 10.302 L 8.288 7.400 9.020 7.400 L 9.751 7.400 9.813 7.550 " fill="#373B4D" stroke="none" fill-rule="evenodd"></path></svg>

After

Width:  |  Height:  |  Size: 883 B

View File

@@ -14,37 +14,39 @@ $web-icon-chevron-up: "\ea0d";
$web-icon-close: "\ea0e"; $web-icon-close: "\ea0e";
$web-icon-command: "\ea0f"; $web-icon-command: "\ea0f";
$web-icon-copy: "\ea10"; $web-icon-copy: "\ea10";
$web-icon-dark: "\ea11"; $web-icon-daily-dev: "\ea11";
$web-icon-discord: "\ea12"; $web-icon-dark: "\ea12";
$web-icon-divider-vertical: "\ea13"; $web-icon-discord: "\ea13";
$web-icon-download: "\ea14"; $web-icon-divider-vertical: "\ea14";
$web-icon-ext-link: "\ea15"; $web-icon-download: "\ea15";
$web-icon-firebase: "\ea16"; $web-icon-ext-link: "\ea16";
$web-icon-github: "\ea17"; $web-icon-firebase: "\ea17";
$web-icon-google: "\ea18"; $web-icon-github: "\ea18";
$web-icon-hamburger-menu: "\ea19"; $web-icon-google: "\ea19";
$web-icon-light: "\ea1a"; $web-icon-hamburger-menu: "\ea1a";
$web-icon-linkedin: "\ea1b"; $web-icon-light: "\ea1b";
$web-icon-location: "\ea1c"; $web-icon-linkedin: "\ea1c";
$web-icon-logout-left: "\ea1d"; $web-icon-location: "\ea1d";
$web-icon-logout-right: "\ea1e"; $web-icon-logout-left: "\ea1e";
$web-icon-mailgun: "\ea1f"; $web-icon-logout-right: "\ea1f";
$web-icon-message: "\ea20"; $web-icon-mailgun: "\ea20";
$web-icon-microsoft: "\ea21"; $web-icon-message: "\ea21";
$web-icon-minus: "\ea22"; $web-icon-microsoft: "\ea22";
$web-icon-nuxt: "\ea23"; $web-icon-minus: "\ea23";
$web-icon-platform: "\ea24"; $web-icon-nuxt: "\ea24";
$web-icon-play: "\ea25"; $web-icon-platform: "\ea25";
$web-icon-plus: "\ea26"; $web-icon-play: "\ea26";
$web-icon-product-hunt: "\ea27"; $web-icon-plus: "\ea27";
$web-icon-refine: "\ea28"; $web-icon-product-hunt: "\ea28";
$web-icon-rest: "\ea29"; $web-icon-refine: "\ea29";
$web-icon-search: "\ea2a"; $web-icon-rest: "\ea2a";
$web-icon-sendgrid: "\ea2b"; $web-icon-search: "\ea2b";
$web-icon-star: "\ea2c"; $web-icon-sendgrid: "\ea2c";
$web-icon-system: "\ea2d"; $web-icon-star: "\ea2d";
$web-icon-textmagic: "\ea2e"; $web-icon-system: "\ea2e";
$web-icon-twitter: "\ea2f"; $web-icon-textmagic: "\ea2f";
$web-icon-vue: "\ea30"; $web-icon-twitter: "\ea30";
$web-icon-x: "\ea31"; $web-icon-vue: "\ea31";
$web-icon-youtube: "\ea32"; $web-icon-x: "\ea32";
$web-icon-ycombinator: "\ea33";
$web-icon-youtube: "\ea34";

View File

@@ -1,302 +1,314 @@
{ {
"apple": { "apple": {
"encodedCode": "\\ea01", "encodedCode": "\\ea01",
"prefix": "web-icon", "prefix": "web-icon",
"className": "web-icon-apple", "className": "web-icon-apple",
"unicode": "&#59905;" "unicode": "&#59905;"
}, },
"appwrite": { "appwrite": {
"encodedCode": "\\ea02", "encodedCode": "\\ea02",
"prefix": "web-icon", "prefix": "web-icon",
"className": "web-icon-appwrite", "className": "web-icon-appwrite",
"unicode": "&#59906;" "unicode": "&#59906;"
}, },
"arrow-down": { "arrow-down": {
"encodedCode": "\\ea03", "encodedCode": "\\ea03",
"prefix": "web-icon", "prefix": "web-icon",
"className": "web-icon-arrow-down", "className": "web-icon-arrow-down",
"unicode": "&#59907;" "unicode": "&#59907;"
}, },
"arrow-ext-link": { "arrow-ext-link": {
"encodedCode": "\\ea04", "encodedCode": "\\ea04",
"prefix": "web-icon", "prefix": "web-icon",
"className": "web-icon-arrow-ext-link", "className": "web-icon-arrow-ext-link",
"unicode": "&#59908;" "unicode": "&#59908;"
}, },
"arrow-left": { "arrow-left": {
"encodedCode": "\\ea05", "encodedCode": "\\ea05",
"prefix": "web-icon", "prefix": "web-icon",
"className": "web-icon-arrow-left", "className": "web-icon-arrow-left",
"unicode": "&#59909;" "unicode": "&#59909;"
}, },
"arrow-right": { "arrow-right": {
"encodedCode": "\\ea06", "encodedCode": "\\ea06",
"prefix": "web-icon", "prefix": "web-icon",
"className": "web-icon-arrow-right", "className": "web-icon-arrow-right",
"unicode": "&#59910;" "unicode": "&#59910;"
}, },
"arrow-up": { "arrow-up": {
"encodedCode": "\\ea07", "encodedCode": "\\ea07",
"prefix": "web-icon", "prefix": "web-icon",
"className": "web-icon-arrow-up", "className": "web-icon-arrow-up",
"unicode": "&#59911;" "unicode": "&#59911;"
}, },
"calendar": { "calendar": {
"encodedCode": "\\ea08", "encodedCode": "\\ea08",
"prefix": "web-icon", "prefix": "web-icon",
"className": "web-icon-calendar", "className": "web-icon-calendar",
"unicode": "&#59912;" "unicode": "&#59912;"
}, },
"check": { "check": {
"encodedCode": "\\ea09", "encodedCode": "\\ea09",
"prefix": "web-icon", "prefix": "web-icon",
"className": "web-icon-check", "className": "web-icon-check",
"unicode": "&#59913;" "unicode": "&#59913;"
}, },
"chevron-down": { "chevron-down": {
"encodedCode": "\\ea0a", "encodedCode": "\\ea0a",
"prefix": "web-icon", "prefix": "web-icon",
"className": "web-icon-chevron-down", "className": "web-icon-chevron-down",
"unicode": "&#59914;" "unicode": "&#59914;"
}, },
"chevron-left": { "chevron-left": {
"encodedCode": "\\ea0b", "encodedCode": "\\ea0b",
"prefix": "web-icon", "prefix": "web-icon",
"className": "web-icon-chevron-left", "className": "web-icon-chevron-left",
"unicode": "&#59915;" "unicode": "&#59915;"
}, },
"chevron-right": { "chevron-right": {
"encodedCode": "\\ea0c", "encodedCode": "\\ea0c",
"prefix": "web-icon", "prefix": "web-icon",
"className": "web-icon-chevron-right", "className": "web-icon-chevron-right",
"unicode": "&#59916;" "unicode": "&#59916;"
}, },
"chevron-up": { "chevron-up": {
"encodedCode": "\\ea0d", "encodedCode": "\\ea0d",
"prefix": "web-icon", "prefix": "web-icon",
"className": "web-icon-chevron-up", "className": "web-icon-chevron-up",
"unicode": "&#59917;" "unicode": "&#59917;"
}, },
"close": { "close": {
"encodedCode": "\\ea0e", "encodedCode": "\\ea0e",
"prefix": "web-icon", "prefix": "web-icon",
"className": "web-icon-close", "className": "web-icon-close",
"unicode": "&#59918;" "unicode": "&#59918;"
}, },
"command": { "command": {
"encodedCode": "\\ea0f", "encodedCode": "\\ea0f",
"prefix": "web-icon", "prefix": "web-icon",
"className": "web-icon-command", "className": "web-icon-command",
"unicode": "&#59919;" "unicode": "&#59919;"
}, },
"copy": { "copy": {
"encodedCode": "\\ea10", "encodedCode": "\\ea10",
"prefix": "web-icon", "prefix": "web-icon",
"className": "web-icon-copy", "className": "web-icon-copy",
"unicode": "&#59920;" "unicode": "&#59920;"
}, },
"dark": { "daily-dev": {
"encodedCode": "\\ea11", "encodedCode": "\\ea11",
"prefix": "web-icon", "prefix": "web-icon",
"className": "web-icon-dark", "className": "web-icon-daily-dev",
"unicode": "&#59921;" "unicode": "&#59921;"
}, },
"discord": { "dark": {
"encodedCode": "\\ea12", "encodedCode": "\\ea12",
"prefix": "web-icon", "prefix": "web-icon",
"className": "web-icon-discord", "className": "web-icon-dark",
"unicode": "&#59922;" "unicode": "&#59922;"
}, },
"divider-vertical": { "discord": {
"encodedCode": "\\ea13", "encodedCode": "\\ea13",
"prefix": "web-icon", "prefix": "web-icon",
"className": "web-icon-divider-vertical", "className": "web-icon-discord",
"unicode": "&#59923;" "unicode": "&#59923;"
}, },
"download": { "divider-vertical": {
"encodedCode": "\\ea14", "encodedCode": "\\ea14",
"prefix": "web-icon", "prefix": "web-icon",
"className": "web-icon-download", "className": "web-icon-divider-vertical",
"unicode": "&#59924;" "unicode": "&#59924;"
}, },
"ext-link": { "download": {
"encodedCode": "\\ea15", "encodedCode": "\\ea15",
"prefix": "web-icon", "prefix": "web-icon",
"className": "web-icon-ext-link", "className": "web-icon-download",
"unicode": "&#59925;" "unicode": "&#59925;"
}, },
"firebase": { "ext-link": {
"encodedCode": "\\ea16", "encodedCode": "\\ea16",
"prefix": "web-icon", "prefix": "web-icon",
"className": "web-icon-firebase", "className": "web-icon-ext-link",
"unicode": "&#59926;" "unicode": "&#59926;"
}, },
"github": { "firebase": {
"encodedCode": "\\ea17", "encodedCode": "\\ea17",
"prefix": "web-icon", "prefix": "web-icon",
"className": "web-icon-github", "className": "web-icon-firebase",
"unicode": "&#59927;" "unicode": "&#59927;"
}, },
"google": { "github": {
"encodedCode": "\\ea18", "encodedCode": "\\ea18",
"prefix": "web-icon", "prefix": "web-icon",
"className": "web-icon-google", "className": "web-icon-github",
"unicode": "&#59928;" "unicode": "&#59928;"
}, },
"hamburger-menu": { "google": {
"encodedCode": "\\ea19", "encodedCode": "\\ea19",
"prefix": "web-icon", "prefix": "web-icon",
"className": "web-icon-hamburger-menu", "className": "web-icon-google",
"unicode": "&#59929;" "unicode": "&#59929;"
}, },
"light": { "hamburger-menu": {
"encodedCode": "\\ea1a", "encodedCode": "\\ea1a",
"prefix": "web-icon", "prefix": "web-icon",
"className": "web-icon-light", "className": "web-icon-hamburger-menu",
"unicode": "&#59930;" "unicode": "&#59930;"
}, },
"linkedin": { "light": {
"encodedCode": "\\ea1b", "encodedCode": "\\ea1b",
"prefix": "web-icon", "prefix": "web-icon",
"className": "web-icon-linkedin", "className": "web-icon-light",
"unicode": "&#59931;" "unicode": "&#59931;"
}, },
"location": { "linkedin": {
"encodedCode": "\\ea1c", "encodedCode": "\\ea1c",
"prefix": "web-icon", "prefix": "web-icon",
"className": "web-icon-location", "className": "web-icon-linkedin",
"unicode": "&#59932;" "unicode": "&#59932;"
}, },
"logout-left": { "location": {
"encodedCode": "\\ea1d", "encodedCode": "\\ea1d",
"prefix": "web-icon", "prefix": "web-icon",
"className": "web-icon-logout-left", "className": "web-icon-location",
"unicode": "&#59933;" "unicode": "&#59933;"
}, },
"logout-right": { "logout-left": {
"encodedCode": "\\ea1e", "encodedCode": "\\ea1e",
"prefix": "web-icon", "prefix": "web-icon",
"className": "web-icon-logout-right", "className": "web-icon-logout-left",
"unicode": "&#59934;" "unicode": "&#59934;"
}, },
"mailgun": { "logout-right": {
"encodedCode": "\\ea1f", "encodedCode": "\\ea1f",
"prefix": "web-icon", "prefix": "web-icon",
"className": "web-icon-mailgun", "className": "web-icon-logout-right",
"unicode": "&#59935;" "unicode": "&#59935;"
}, },
"message": { "mailgun": {
"encodedCode": "\\ea20", "encodedCode": "\\ea20",
"prefix": "web-icon", "prefix": "web-icon",
"className": "web-icon-message", "className": "web-icon-mailgun",
"unicode": "&#59936;" "unicode": "&#59936;"
}, },
"microsoft": { "message": {
"encodedCode": "\\ea21", "encodedCode": "\\ea21",
"prefix": "web-icon", "prefix": "web-icon",
"className": "web-icon-microsoft", "className": "web-icon-message",
"unicode": "&#59937;" "unicode": "&#59937;"
}, },
"minus": { "microsoft": {
"encodedCode": "\\ea22", "encodedCode": "\\ea22",
"prefix": "web-icon", "prefix": "web-icon",
"className": "web-icon-minus", "className": "web-icon-microsoft",
"unicode": "&#59938;" "unicode": "&#59938;"
}, },
"nuxt": { "minus": {
"encodedCode": "\\ea23", "encodedCode": "\\ea23",
"prefix": "web-icon", "prefix": "web-icon",
"className": "web-icon-nuxt", "className": "web-icon-minus",
"unicode": "&#59939;" "unicode": "&#59939;"
}, },
"platform": { "nuxt": {
"encodedCode": "\\ea24", "encodedCode": "\\ea24",
"prefix": "web-icon", "prefix": "web-icon",
"className": "web-icon-platform", "className": "web-icon-nuxt",
"unicode": "&#59940;" "unicode": "&#59940;"
}, },
"play": { "platform": {
"encodedCode": "\\ea25", "encodedCode": "\\ea25",
"prefix": "web-icon", "prefix": "web-icon",
"className": "web-icon-play", "className": "web-icon-platform",
"unicode": "&#59941;" "unicode": "&#59941;"
}, },
"plus": { "play": {
"encodedCode": "\\ea26", "encodedCode": "\\ea26",
"prefix": "web-icon", "prefix": "web-icon",
"className": "web-icon-plus", "className": "web-icon-play",
"unicode": "&#59942;" "unicode": "&#59942;"
}, },
"product-hunt": { "plus": {
"encodedCode": "\\ea27", "encodedCode": "\\ea27",
"prefix": "web-icon", "prefix": "web-icon",
"className": "web-icon-product-hunt", "className": "web-icon-plus",
"unicode": "&#59943;" "unicode": "&#59943;"
}, },
"refine": { "product-hunt": {
"encodedCode": "\\ea28", "encodedCode": "\\ea28",
"prefix": "web-icon", "prefix": "web-icon",
"className": "web-icon-refine", "className": "web-icon-product-hunt",
"unicode": "&#59944;" "unicode": "&#59944;"
}, },
"rest": { "refine": {
"encodedCode": "\\ea29", "encodedCode": "\\ea29",
"prefix": "web-icon", "prefix": "web-icon",
"className": "web-icon-rest", "className": "web-icon-refine",
"unicode": "&#59945;" "unicode": "&#59945;"
}, },
"search": { "rest": {
"encodedCode": "\\ea2a", "encodedCode": "\\ea2a",
"prefix": "web-icon", "prefix": "web-icon",
"className": "web-icon-search", "className": "web-icon-rest",
"unicode": "&#59946;" "unicode": "&#59946;"
}, },
"sendgrid": { "search": {
"encodedCode": "\\ea2b", "encodedCode": "\\ea2b",
"prefix": "web-icon", "prefix": "web-icon",
"className": "web-icon-sendgrid", "className": "web-icon-search",
"unicode": "&#59947;" "unicode": "&#59947;"
}, },
"star": { "sendgrid": {
"encodedCode": "\\ea2c", "encodedCode": "\\ea2c",
"prefix": "web-icon", "prefix": "web-icon",
"className": "web-icon-star", "className": "web-icon-sendgrid",
"unicode": "&#59948;" "unicode": "&#59948;"
}, },
"system": { "star": {
"encodedCode": "\\ea2d", "encodedCode": "\\ea2d",
"prefix": "web-icon", "prefix": "web-icon",
"className": "web-icon-system", "className": "web-icon-star",
"unicode": "&#59949;" "unicode": "&#59949;"
}, },
"textmagic": { "system": {
"encodedCode": "\\ea2e", "encodedCode": "\\ea2e",
"prefix": "web-icon", "prefix": "web-icon",
"className": "web-icon-textmagic", "className": "web-icon-system",
"unicode": "&#59950;" "unicode": "&#59950;"
}, },
"twitter": { "textmagic": {
"encodedCode": "\\ea2f", "encodedCode": "\\ea2f",
"prefix": "web-icon", "prefix": "web-icon",
"className": "web-icon-twitter", "className": "web-icon-textmagic",
"unicode": "&#59951;" "unicode": "&#59951;"
}, },
"vue": { "twitter": {
"encodedCode": "\\ea30", "encodedCode": "\\ea30",
"prefix": "web-icon", "prefix": "web-icon",
"className": "web-icon-vue", "className": "web-icon-twitter",
"unicode": "&#59952;" "unicode": "&#59952;"
}, },
"x": { "vue": {
"encodedCode": "\\ea31", "encodedCode": "\\ea31",
"prefix": "web-icon", "prefix": "web-icon",
"className": "web-icon-x", "className": "web-icon-vue",
"unicode": "&#59953;" "unicode": "&#59953;"
}, },
"youtube": { "x": {
"encodedCode": "\\ea32", "encodedCode": "\\ea32",
"prefix": "web-icon", "prefix": "web-icon",
"className": "web-icon-youtube", "className": "web-icon-x",
"unicode": "&#59954;" "unicode": "&#59954;"
} },
"ycombinator": {
"encodedCode": "\\ea33",
"prefix": "web-icon",
"className": "web-icon-ycombinator",
"unicode": "&#59955;"
},
"youtube": {
"encodedCode": "\\ea34",
"prefix": "web-icon",
"className": "web-icon-youtube",
"unicode": "&#59956;"
}
} }

View File

@@ -1,18 +1,15 @@
@font-face { @font-face {
font-family: 'web-icon'; font-family: "web-icon";
font-display: swap; font-display: swap;
src: url('web-icon.eot'); /* IE9*/ src: url('web-icon.eot'); /* IE9*/
src: src: url('web-icon.eot#iefix') format('embedded-opentype'), /* IE6-IE8 */
url('web-icon.eot#iefix') format('embedded-opentype'), url("web-icon.woff2") format("woff2"),
/* IE6-IE8 */ url('web-icon.woff2') format('woff2'), url("web-icon.woff") format("woff"),
url('web-icon.woff') format('woff'), url('web-icon.ttf') format('truetype'), /* chrome, firefox, opera, Safari, Android, iOS 4.2+*/
url('web-icon.ttf') format('truetype'), url('web-icon.svg#web-icon') format('svg'); /* iOS 4.1- */
/* chrome, firefox, opera, Safari, Android, iOS 4.2+*/ url('web-icon.svg#web-icon')
format('svg'); /* iOS 4.1- */
} }
[class^='web-icon-'], [class^="web-icon-"], [class*=" web-icon-"] {
[class*=' web-icon-'] {
font-family: 'web-icon' !important; font-family: 'web-icon' !important;
font-size: 20px; font-size: 20px;
font-style: normal; font-style: normal;
@@ -20,153 +17,55 @@
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
} }
.web-icon-apple:before { .web-icon-apple:before { content: "\ea01"; }
content: '\ea01'; .web-icon-appwrite:before { content: "\ea02"; }
} .web-icon-arrow-down:before { content: "\ea03"; }
.web-icon-appwrite:before { .web-icon-arrow-ext-link:before { content: "\ea04"; }
content: '\ea02'; .web-icon-arrow-left:before { content: "\ea05"; }
} .web-icon-arrow-right:before { content: "\ea06"; }
.web-icon-arrow-down:before { .web-icon-arrow-up:before { content: "\ea07"; }
content: '\ea03'; .web-icon-calendar:before { content: "\ea08"; }
} .web-icon-check:before { content: "\ea09"; }
.web-icon-arrow-ext-link:before { .web-icon-chevron-down:before { content: "\ea0a"; }
content: '\ea04'; .web-icon-chevron-left:before { content: "\ea0b"; }
} .web-icon-chevron-right:before { content: "\ea0c"; }
.web-icon-arrow-left:before { .web-icon-chevron-up:before { content: "\ea0d"; }
content: '\ea05'; .web-icon-close:before { content: "\ea0e"; }
} .web-icon-command:before { content: "\ea0f"; }
.web-icon-arrow-right:before { .web-icon-copy:before { content: "\ea10"; }
content: '\ea06'; .web-icon-daily-dev:before { content: "\ea11"; }
} .web-icon-dark:before { content: "\ea12"; }
.web-icon-arrow-up:before { .web-icon-discord:before { content: "\ea13"; }
content: '\ea07'; .web-icon-divider-vertical:before { content: "\ea14"; }
} .web-icon-download:before { content: "\ea15"; }
.web-icon-calendar:before { .web-icon-ext-link:before { content: "\ea16"; }
content: '\ea08'; .web-icon-firebase:before { content: "\ea17"; }
} .web-icon-github:before { content: "\ea18"; }
.web-icon-check:before { .web-icon-google:before { content: "\ea19"; }
content: '\ea09'; .web-icon-hamburger-menu:before { content: "\ea1a"; }
} .web-icon-light:before { content: "\ea1b"; }
.web-icon-chevron-down:before { .web-icon-linkedin:before { content: "\ea1c"; }
content: '\ea0a'; .web-icon-location:before { content: "\ea1d"; }
} .web-icon-logout-left:before { content: "\ea1e"; }
.web-icon-chevron-left:before { .web-icon-logout-right:before { content: "\ea1f"; }
content: '\ea0b'; .web-icon-mailgun:before { content: "\ea20"; }
} .web-icon-message:before { content: "\ea21"; }
.web-icon-chevron-right:before { .web-icon-microsoft:before { content: "\ea22"; }
content: '\ea0c'; .web-icon-minus:before { content: "\ea23"; }
} .web-icon-nuxt:before { content: "\ea24"; }
.web-icon-chevron-up:before { .web-icon-platform:before { content: "\ea25"; }
content: '\ea0d'; .web-icon-play:before { content: "\ea26"; }
} .web-icon-plus:before { content: "\ea27"; }
.web-icon-close:before { .web-icon-product-hunt:before { content: "\ea28"; }
content: '\ea0e'; .web-icon-refine:before { content: "\ea29"; }
} .web-icon-rest:before { content: "\ea2a"; }
.web-icon-command:before { .web-icon-search:before { content: "\ea2b"; }
content: '\ea0f'; .web-icon-sendgrid:before { content: "\ea2c"; }
} .web-icon-star:before { content: "\ea2d"; }
.web-icon-copy:before { .web-icon-system:before { content: "\ea2e"; }
content: '\ea10'; .web-icon-textmagic:before { content: "\ea2f"; }
} .web-icon-twitter:before { content: "\ea30"; }
.web-icon-dark:before { .web-icon-vue:before { content: "\ea31"; }
content: '\ea11'; .web-icon-x:before { content: "\ea32"; }
} .web-icon-ycombinator:before { content: "\ea33"; }
.web-icon-discord:before { .web-icon-youtube:before { content: "\ea34"; }
content: '\ea12';
}
.web-icon-divider-vertical:before {
content: '\ea13';
}
.web-icon-download:before {
content: '\ea14';
}
.web-icon-ext-link:before {
content: '\ea15';
}
.web-icon-firebase:before {
content: '\ea16';
}
.web-icon-github:before {
content: '\ea17';
}
.web-icon-google:before {
content: '\ea18';
}
.web-icon-hamburger-menu:before {
content: '\ea19';
}
.web-icon-light:before {
content: '\ea1a';
}
.web-icon-linkedin:before {
content: '\ea1b';
}
.web-icon-location:before {
content: '\ea1c';
}
.web-icon-logout-left:before {
content: '\ea1d';
}
.web-icon-logout-right:before {
content: '\ea1e';
}
.web-icon-mailgun:before {
content: '\ea1f';
}
.web-icon-message:before {
content: '\ea20';
}
.web-icon-microsoft:before {
content: '\ea21';
}
.web-icon-minus:before {
content: '\ea22';
}
.web-icon-nuxt:before {
content: '\ea23';
}
.web-icon-platform:before {
content: '\ea24';
}
.web-icon-play:before {
content: '\ea25';
}
.web-icon-plus:before {
content: '\ea26';
}
.web-icon-product-hunt:before {
content: '\ea27';
}
.web-icon-refine:before {
content: '\ea28';
}
.web-icon-rest:before {
content: '\ea29';
}
.web-icon-search:before {
content: '\ea2a';
}
.web-icon-sendgrid:before {
content: '\ea2b';
}
.web-icon-star:before {
content: '\ea2c';
}
.web-icon-system:before {
content: '\ea2d';
}
.web-icon-textmagic:before {
content: '\ea2e';
}
.web-icon-twitter:before {
content: '\ea2f';
}
.web-icon-vue:before {
content: '\ea30';
}
.web-icon-x:before {
content: '\ea31';
}
.web-icon-youtube:before {
content: '\ea32';
}

Binary file not shown.

View File

@@ -32,40 +32,42 @@
.web-icon-close:before { content: "\ea0e"; } .web-icon-close:before { content: "\ea0e"; }
.web-icon-command:before { content: "\ea0f"; } .web-icon-command:before { content: "\ea0f"; }
.web-icon-copy:before { content: "\ea10"; } .web-icon-copy:before { content: "\ea10"; }
.web-icon-dark:before { content: "\ea11"; } .web-icon-daily-dev:before { content: "\ea11"; }
.web-icon-discord:before { content: "\ea12"; } .web-icon-dark:before { content: "\ea12"; }
.web-icon-divider-vertical:before { content: "\ea13"; } .web-icon-discord:before { content: "\ea13"; }
.web-icon-download:before { content: "\ea14"; } .web-icon-divider-vertical:before { content: "\ea14"; }
.web-icon-ext-link:before { content: "\ea15"; } .web-icon-download:before { content: "\ea15"; }
.web-icon-firebase:before { content: "\ea16"; } .web-icon-ext-link:before { content: "\ea16"; }
.web-icon-github:before { content: "\ea17"; } .web-icon-firebase:before { content: "\ea17"; }
.web-icon-google:before { content: "\ea18"; } .web-icon-github:before { content: "\ea18"; }
.web-icon-hamburger-menu:before { content: "\ea19"; } .web-icon-google:before { content: "\ea19"; }
.web-icon-light:before { content: "\ea1a"; } .web-icon-hamburger-menu:before { content: "\ea1a"; }
.web-icon-linkedin:before { content: "\ea1b"; } .web-icon-light:before { content: "\ea1b"; }
.web-icon-location:before { content: "\ea1c"; } .web-icon-linkedin:before { content: "\ea1c"; }
.web-icon-logout-left:before { content: "\ea1d"; } .web-icon-location:before { content: "\ea1d"; }
.web-icon-logout-right:before { content: "\ea1e"; } .web-icon-logout-left:before { content: "\ea1e"; }
.web-icon-mailgun:before { content: "\ea1f"; } .web-icon-logout-right:before { content: "\ea1f"; }
.web-icon-message:before { content: "\ea20"; } .web-icon-mailgun:before { content: "\ea20"; }
.web-icon-microsoft:before { content: "\ea21"; } .web-icon-message:before { content: "\ea21"; }
.web-icon-minus:before { content: "\ea22"; } .web-icon-microsoft:before { content: "\ea22"; }
.web-icon-nuxt:before { content: "\ea23"; } .web-icon-minus:before { content: "\ea23"; }
.web-icon-platform:before { content: "\ea24"; } .web-icon-nuxt:before { content: "\ea24"; }
.web-icon-play:before { content: "\ea25"; } .web-icon-platform:before { content: "\ea25"; }
.web-icon-plus:before { content: "\ea26"; } .web-icon-play:before { content: "\ea26"; }
.web-icon-product-hunt:before { content: "\ea27"; } .web-icon-plus:before { content: "\ea27"; }
.web-icon-refine:before { content: "\ea28"; } .web-icon-product-hunt:before { content: "\ea28"; }
.web-icon-rest:before { content: "\ea29"; } .web-icon-refine:before { content: "\ea29"; }
.web-icon-search:before { content: "\ea2a"; } .web-icon-rest:before { content: "\ea2a"; }
.web-icon-sendgrid:before { content: "\ea2b"; } .web-icon-search:before { content: "\ea2b"; }
.web-icon-star:before { content: "\ea2c"; } .web-icon-sendgrid:before { content: "\ea2c"; }
.web-icon-system:before { content: "\ea2d"; } .web-icon-star:before { content: "\ea2d"; }
.web-icon-textmagic:before { content: "\ea2e"; } .web-icon-system:before { content: "\ea2e"; }
.web-icon-twitter:before { content: "\ea2f"; } .web-icon-textmagic:before { content: "\ea2f"; }
.web-icon-vue:before { content: "\ea30"; } .web-icon-twitter:before { content: "\ea30"; }
.web-icon-x:before { content: "\ea31"; } .web-icon-vue:before { content: "\ea31"; }
.web-icon-youtube:before { content: "\ea32"; } .web-icon-x:before { content: "\ea32"; }
.web-icon-ycombinator:before { content: "\ea33"; }
.web-icon-youtube:before { content: "\ea34"; }
$web-icon-apple: "\ea01"; $web-icon-apple: "\ea01";
$web-icon-appwrite: "\ea02"; $web-icon-appwrite: "\ea02";
@@ -83,37 +85,39 @@ $web-icon-chevron-up: "\ea0d";
$web-icon-close: "\ea0e"; $web-icon-close: "\ea0e";
$web-icon-command: "\ea0f"; $web-icon-command: "\ea0f";
$web-icon-copy: "\ea10"; $web-icon-copy: "\ea10";
$web-icon-dark: "\ea11"; $web-icon-daily-dev: "\ea11";
$web-icon-discord: "\ea12"; $web-icon-dark: "\ea12";
$web-icon-divider-vertical: "\ea13"; $web-icon-discord: "\ea13";
$web-icon-download: "\ea14"; $web-icon-divider-vertical: "\ea14";
$web-icon-ext-link: "\ea15"; $web-icon-download: "\ea15";
$web-icon-firebase: "\ea16"; $web-icon-ext-link: "\ea16";
$web-icon-github: "\ea17"; $web-icon-firebase: "\ea17";
$web-icon-google: "\ea18"; $web-icon-github: "\ea18";
$web-icon-hamburger-menu: "\ea19"; $web-icon-google: "\ea19";
$web-icon-light: "\ea1a"; $web-icon-hamburger-menu: "\ea1a";
$web-icon-linkedin: "\ea1b"; $web-icon-light: "\ea1b";
$web-icon-location: "\ea1c"; $web-icon-linkedin: "\ea1c";
$web-icon-logout-left: "\ea1d"; $web-icon-location: "\ea1d";
$web-icon-logout-right: "\ea1e"; $web-icon-logout-left: "\ea1e";
$web-icon-mailgun: "\ea1f"; $web-icon-logout-right: "\ea1f";
$web-icon-message: "\ea20"; $web-icon-mailgun: "\ea20";
$web-icon-microsoft: "\ea21"; $web-icon-message: "\ea21";
$web-icon-minus: "\ea22"; $web-icon-microsoft: "\ea22";
$web-icon-nuxt: "\ea23"; $web-icon-minus: "\ea23";
$web-icon-platform: "\ea24"; $web-icon-nuxt: "\ea24";
$web-icon-play: "\ea25"; $web-icon-platform: "\ea25";
$web-icon-plus: "\ea26"; $web-icon-play: "\ea26";
$web-icon-product-hunt: "\ea27"; $web-icon-plus: "\ea27";
$web-icon-refine: "\ea28"; $web-icon-product-hunt: "\ea28";
$web-icon-rest: "\ea29"; $web-icon-refine: "\ea29";
$web-icon-search: "\ea2a"; $web-icon-rest: "\ea2a";
$web-icon-sendgrid: "\ea2b"; $web-icon-search: "\ea2b";
$web-icon-star: "\ea2c"; $web-icon-sendgrid: "\ea2c";
$web-icon-system: "\ea2d"; $web-icon-star: "\ea2d";
$web-icon-textmagic: "\ea2e"; $web-icon-system: "\ea2e";
$web-icon-twitter: "\ea2f"; $web-icon-textmagic: "\ea2f";
$web-icon-vue: "\ea30"; $web-icon-twitter: "\ea30";
$web-icon-x: "\ea31"; $web-icon-vue: "\ea31";
$web-icon-youtube: "\ea32"; $web-icon-x: "\ea32";
$web-icon-ycombinator: "\ea33";
$web-icon-youtube: "\ea34";

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 162 KiB

After

Width:  |  Height:  |  Size: 166 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 68 KiB

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,4 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M21 3H3V21H21V3Z" fill="#373B4D"/>
<path d="M11.4 13.2L8.28003 7.38H9.72003L11.52 11.04C11.52 11.1 11.58 11.16 11.64 11.22C11.7 11.28 11.7 11.34 11.76 11.46L11.82 11.52V11.58C11.88 11.7 11.88 11.76 11.94 11.88C12 11.94 12 12.06 12.06 12.12C12.12 11.94 12.24 11.82 12.3 11.58C12.36 11.4 12.48 11.22 12.6 11.04L14.4 7.38H15.72L12.6 13.26V16.98H11.4V13.2Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 481 B

View File

@@ -234,7 +234,7 @@
aria-label="YouTube" aria-label="YouTube"
/> />
</div> </div>
<div class="web-title u-margin-block-start-auto">4k+ Youtube Subscribers</div> <div class="web-title u-margin-block-start-auto">7k+ Youtube Subscribers</div>
</a> </a>
<a <a
@@ -249,7 +249,7 @@
aria-label="GitHub" aria-label="GitHub"
/> />
</div> </div>
<div class="web-title u-margin-block-start-auto">18k+ Code Commits</div> <div class="web-title u-margin-block-start-auto">21k+ Code Commits</div>
</a> </a>
</div> </div>
</div> </div>

View File

@@ -1,9 +1,9 @@
import { PUBLIC_APPWRITE_PROJECT_ID } from '$env/static/public'; import { PUBLIC_APPWRITE_ENDPOINT, PUBLIC_APPWRITE_PROJECT_ID } from '$env/static/public';
import { Client, Databases, Functions } from '@appwrite.io/console'; import { Client, Databases, Functions } from '@appwrite.io/console';
export const client = new Client(); export const client = new Client();
client.setEndpoint('https://cloud.appwrite.io/v1').setProject(PUBLIC_APPWRITE_PROJECT_ID); client.setEndpoint(PUBLIC_APPWRITE_ENDPOINT).setProject(PUBLIC_APPWRITE_PROJECT_ID);
export const databases = new Databases(client); export const databases = new Databases(client);
export const functions = new Functions(client); export const functions = new Functions(client);

View File

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

View File

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

View File

@@ -0,0 +1,24 @@
<script lang="ts">
import { browser } from '$app/environment';
import { BANNER_KEY } from '$lib/constants';
const hideTopBanner = () => {
document.body.dataset.bannerHidden = '';
localStorage.setItem(BANNER_KEY, 'true');
};
</script>
<div class="web-top-banner">
<div class="web-top-banner-content web-u-color-text-primary">
<slot />
{#if browser}
<button
class="web-top-banner-button"
aria-label="close discord message"
on:click={hideTopBanner}
>
<span class="web-icon-close" aria-hidden="true" />
</button>
{/if}
</div>
</div>

View File

@@ -65,7 +65,7 @@
rel: 'noopener noreferrer' rel: 'noopener noreferrer'
} }
], ],
Program: [ Programs: [
{ label: 'Heroes', href: '/heroes' }, { label: 'Heroes', href: '/heroes' },
{ label: 'Startups', href: '/startups' } { label: 'Startups', href: '/startups' }
], ],

View File

@@ -0,0 +1,254 @@
<script lang="ts">
import { page } from '$app/stores';
import { onMount } from 'svelte';
let mounted = false;
onMount(() => {
mounted = true;
});
let groups = [1, 2, 1, 3, 1, 2, 1, 3, 3, 2];
const getRandomXValue = () => {
return Math.floor(Math.random() * 120);
};
const getRandomWidth = (index: number) => {
const minWidth = 80;
const maxRandomWidth = 125 - minWidth;
const seed = index;
const random = Math.sin(seed) * 10000;
const randomNumber = random - Math.floor(random);
return Math.floor(randomNumber * maxRandomWidth) + minWidth;
};
const randomDelay = () => Math.floor(Math.random() * 750);
</script>
<div class="banner" class:hidden={$page.url.pathname.includes('init')}>
<div class="content web-u-color-text-primary">
<div class="headings">
<span style:font-weight="500"
><span
style:font-size="20px"
style:font-family="var(--web-font-family-aeonik-pro)"
style:font-weight="600">init</span
> is coming
</span>
<span class="web-u-color-text-secondary">The start of something new</span>
<div class="shadow" />
</div>
<a href="/init/tickets" rel="noopener noreferrer" class="action">
<span class="web-caption-500">Register now</span>
<span class="web-icon-arrow-right" aria-hidden="true" />
<div class="shadow" />
</a>
</div>
<div class="shine" />
<div class="border" />
<div class="lines">
{#if mounted}
{#each Array.from({ length: groups.length }) as _, i}
<div style:position="relative" class="group">
{#each Array.from({ length: groups[i] }) as _, index}
<div
class="line"
style={`--width:${getRandomWidth(index)}px;--initial-delay:${randomDelay()}ms;left:${getRandomXValue()}px;`}
/>
{/each}
</div>
{/each}
{/if}
</div>
</div>
<style lang="scss">
.banner {
--shine: rgba(255, 255, 255, 0.04);
position: relative;
min-height: 76px;
display: flex;
align-items: center;
max-width: 100vw;
overflow: hidden;
border-bottom: 1px solid hsl(var(--web-color-border));
&.hidden {
display: none;
}
.content {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
position: relative;
z-index: 10;
padding-inline: clamp(1.25rem, 4vw, 120rem);
.headings {
z-index: 10;
position: relative;
display: flex;
flex-direction: column;
span {
position: relative;
z-index: 10;
}
.shadow {
width: 250px;
height: 76px;
position: absolute;
top: 50%;
left: 50%;
transform: translateX(-50%) translateY(-50%);
z-index: 0;
backdrop-filter: blur(6px);
background-color: hsl(var(--web-color-background) / 50%);
mask-composite: intersect;
mask-image: linear-gradient(
to top,
transparent,
rgba(0, 0, 0, 1) 25%,
rgba(0, 0, 0, 1) 75%,
transparent
),
linear-gradient(
to right,
transparent,
rgba(0, 0, 0, 1) 25%,
rgba(0, 0, 0, 1) 75%,
transparent
);
}
}
.action {
position: relative;
z-index: 10;
span {
position: relative;
z-index: 10;
}
.shadow {
width: 200px;
height: 76px;
position: absolute;
top: 50%;
left: 50%;
transform: translateX(-50%) translateY(-50%);
z-index: 0;
backdrop-filter: blur(6px);
background-color: hsl(var(--web-color-background) / 50%);
mask-composite: intersect;
mask-image: linear-gradient(
to top,
transparent,
rgba(0, 0, 0, 1) 25%,
rgba(0, 0, 0, 1) 75%,
transparent
),
linear-gradient(
to right,
transparent,
rgba(0, 0, 0, 1) 25%,
rgba(0, 0, 0, 1) 75%,
transparent
);
}
}
}
.shine {
position: absolute;
inset: 0;
background: hsl(var(--web-color-background))
linear-gradient(
-24deg,
rgba(255, 255, 255, 0) 0,
var(--shine) 20%,
rgba(255, 255, 255, 0) 40%,
var(--shine) 50%,
rgba(255, 255, 255, 0) 60%,
var(--shine) 80%,
rgba(255, 255, 255, 0) 100%
);
}
.border {
position: absolute;
left: 0;
right: 0;
height: 1px;
background-color: hsl(var(--web-color-border));
bottom: 0;
}
.lines {
position: absolute;
inset: 0;
z-index: 0;
margin: 0 auto;
opacity: 0.6;
display: flex;
justify-content: space-between;
gap: 16px;
padding: 16px 0;
.group {
display: flex;
flex-direction: column;
gap: 16px;
position: relative;
justify-content: center;
}
--gradient: linear-gradient(
to right,
rgba(255, 255, 255, 0) 0%,
rgba(255, 255, 255, 1) 50%,
rgba(255, 255, 255, 1) 100%
);
--height: 1px;
--starting-position: -80vw;
--duration: 1s;
.line {
position: relative;
height: var(--height);
width: var(--width);
z-index: 20;
@keyframes line {
from {
transform: scaleX(0);
}
to {
transform: scaleX(1);
}
}
&::after {
content: '';
display: block;
position: absolute;
transform-origin: left;
width: var(--width);
height: var(--height);
background: var(--gradient);
border-radius: 8px;
transform: scaleX(0);
filter: drop-shadow(0 0 2px rgba(255, 255, 255, 0.8));
animation: line var(--duration) ease forwards;
animation-delay: var(--initial-delay);
}
}
}
}
</style>

View File

@@ -1,14 +1,32 @@
<script lang="ts"> <script lang="ts">
import Button from "./Button.svelte"; import Button from "./Button.svelte";
import { PUBLIC_APPWRITE_DASHBOARD } from "$env/static/public";
export let classes = ""; export let classes = "";
</script> </script>
<Button class={classes}> <Button class={classes} href={PUBLIC_APPWRITE_DASHBOARD}>
<span class="hidden [data-logged-in]:block" <span class="[data-logged-in]:block hidden"
><slot name="isLoggedIn">Go to Console</slot></span ><slot name="isLoggedIn">Go to Console</slot></span
> >
<span class="block [data-logged-in]:hidden" <span class="btton [data-logged-in]:hidden"
><slot name="isNotLoggedIn">Get started</slot></span ><slot name="isNotLoggedIn">Get started</slot></span
> >
</Button> </Button>
<style lang="scss">
:global(body[data-logged-in]) {
.logged-in {
display: block;
}
.not-logged-in {
display: none;
}
}
.not-logged-in {
display: block;
}
.logged-in {
display: none;
}
</style>

View File

@@ -1,81 +1,95 @@
<script lang="ts"> <script lang="ts">
import { socials } from '$lib/constants'; import { socials } from "$lib/constants";
import ThemeSelect from './ThemeSelect.svelte'; import ThemeSelect from "./ThemeSelect.svelte";
export let variant: 'homepage' | 'docs' = 'homepage'; export let variant: "homepage" | "docs" = "homepage";
const year = new Date().getFullYear(); const year = new Date().getFullYear();
</script> </script>
{#if variant === 'homepage'} {#if variant === "homepage"}
<footer class="web-main-footer relative u-margin-block-start-48"> <footer class="web-main-footer u-margin-block-start-48 relative">
<ul class="flex gap-2"> <ul class="flex gap-2">
{#each socials as social} {#each socials as social}
<li> <li>
<a <a
href={social.link} href={social.link}
class="web-icon-button" class="web-icon-button"
aria-label={social.label} aria-label={social.label}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
> >
<span class={social.icon} aria-hidden="true" /> <span class={social.icon} aria-hidden="true" />
</a> </a>
</li> </li>
{/each} {/each}
</ul> </ul>
<div class="flex gap-4"> <div class="flex gap-4">
<ul class="flex gap-2"> <ul class="flex gap-2">
<li><a class="web-link" href="/terms">Terms</a></li> <li><a class="web-link" href="/terms">Terms</a></li>
<li><a class="web-link" href="/privacy">Privacy</a></li> <li><a class="web-link" href="/privacy">Privacy</a></li>
<li><a class="web-link" href="/cookies">Cookies</a></li> <li><a class="web-link" href="/cookies">Cookies</a></li>
</ul> </ul>
<div>Copyright © {year} Appwrite</div> <div>Copyright © {year} Appwrite</div>
</div> </div>
</footer> </footer>
{:else if variant === 'docs'} {:else if variant === "docs"}
<footer class="web-main-footer is-with-bg-color u-margin-block-start-32 u-small relative"> <footer
<div class="web-main-footer-grid-1"> class="web-main-footer is-with-bg-color u-margin-block-start-32 u-small relative"
<ul class="web-main-footer-grid-1-column-1 flex gap-2"> >
{#each socials as social} <div class="web-main-footer-grid-1">
<li> <ul class="web-main-footer-grid-1-column-1 flex gap-2">
<a {#each socials as social}
href={social.link} <li>
class="web-icon-button" <a
aria-label={social.label} href={social.link}
target="_blank" class="web-icon-button"
rel="noopener noreferrer" aria-label={social.label}
> target="_blank"
<span class={social.icon} aria-hidden="true" /> rel="noopener noreferrer"
</a> >
</li> <span class={social.icon} aria-hidden="true" />
{/each} </a>
</ul> </li>
<div class="web-main-footer-grid-1-column-2"> {/each}
<ThemeSelect /> </ul>
</div> <div class="web-main-footer-grid-1-column-2">
<ul class="web-main-footer-grid-1-column-3 web-main-footer-links"> <ThemeSelect />
<li> </div>
<a href="/discord" target="_blank" rel="noopener noreferrer">Support</a> <ul
</li> class="web-main-footer-grid-1-column-3 u-cross-center web-main-footer-links"
<li> >
<a href="https://appwrite.online" target="_blank" rel="noopener noreferrer" <li>
>Status</a <a href="/discord" target="_blank" rel="noopener noreferrer"
> >Support</a
</li> >
<!-- <li> </li>
<li>
<a
href="https://appwrite.online"
target="_blank"
rel="noopener noreferrer">Status</a
>
</li>
<!-- <li>
<a href="https://github.com/appwrite/appwrite/releases" target="_blank" rel="noopener noreferrer">Changelog</a> <a href="https://github.com/appwrite/appwrite/releases" target="_blank" rel="noopener noreferrer">Changelog</a>
</li> --> </li> -->
</ul> </ul>
<div class="web-main-footer-grid-1-column-4 web-main-footer-copyright"> <div class="web-main-footer-grid-1-column-4 web-main-footer-copyright">
Copyright © {year} Appwrite Copyright © {year} Appwrite
</div> </div>
</div> </div>
</footer> </footer>
{/if} {/if}
<style lang="scss"> <style lang="scss">
.web-icon-button { .web-icon-button {
display: grid; display: grid;
}
@media (max-width: 1023.9px), (min-width: 1024px) and (max-width: 1279.9px) {
.web-main-footer-links {
flex-direction: unset !important;
} }
}
</style> </style>

View File

@@ -15,7 +15,7 @@
<svelte:window on:resize={() => open && (open = false)} /> <svelte:window on:resize={() => open && (open = false)} />
<nav class="web-side-nav web-is-not-desktop" class:u-hide={!open}> <nav class="web-side-nav web-is-not-desktop" class:u-hide={!open}>
<div class="web-side-nav-wrapper ps-4 pe-4"> <div class="web-side-nav-wrapper pe-4 ps-4">
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<a <a
href="https://cloud.appwrite.io/register" href="https://cloud.appwrite.io/register"

View File

@@ -1,4 +1,5 @@
<script lang="ts"> <script lang="ts">
import { PUBLIC_APPWRITE_DASHBOARD } from '$env/static/public';
</script> </script>
<img src="/images/bgs/pre-footer.png" alt="" class="web-pre-footer-bg" style="z-index:-1" /> <img src="/images/bgs/pre-footer.png" alt="" class="web-pre-footer-bg" style="z-index:-1" />
@@ -9,7 +10,7 @@
Start building today Start building today
</h2> </h2>
<a <a
href="https://cloud.appwrite.io" href={PUBLIC_APPWRITE_DASHBOARD}
class="web-button is-transparent web-u-cross-child-center" class="web-button is-transparent web-u-cross-child-center"
> >
<span class="text">Get started</span> <span class="text">Get started</span>
@@ -36,7 +37,7 @@
For personal hobby projects and students. For personal hobby projects and students.
</p> </p>
<a <a
href="https://cloud.appwrite.io/register" href={`${PUBLIC_APPWRITE_DASHBOARD}/register`}
class="web-button is-secondary is-full-width-mobile web-u-cross-child-end" class="web-button is-secondary is-full-width-mobile web-u-cross-child-end"
> >
<span class="text">Get started</span> <span class="text">Get started</span>
@@ -54,7 +55,7 @@
For pro developers and teams that need to scale their products. For pro developers and teams that need to scale their products.
</p> </p>
<a <a
href="https://cloud.appwrite.io/console?type=createPro" href={`${PUBLIC_APPWRITE_DASHBOARD}/console?type=createPro`}
class="web-button is-full-width-mobile web-u-cross-child-end" class="web-button is-full-width-mobile web-u-cross-child-end"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"

View File

@@ -1,68 +1,68 @@
<script lang="ts"> <script lang="ts">
import { createSwitch, melt } from '@melt-ui/svelte'; import { createSwitch, melt } from "@melt-ui/svelte";
export let checked = false; export let checked = false;
const { const {
elements: { root }, elements: { root },
states: { checked: meltChecked } states: { checked: meltChecked },
} = createSwitch({ } = createSwitch({
onCheckedChange({ next }) { onCheckedChange({ next }) {
checked = next; checked = next;
return next; return next;
} },
}); });
$: meltChecked.set(checked); $: meltChecked.set(checked);
</script> </script>
<div class="melt-switch"> <div class="melt-switch">
<button use:melt={$root}> <button use:melt={$root}>
<span class="thumb" /> <span class="thumb" />
</button> </button>
</div> </div>
<style lang="scss"> <style lang="scss">
.melt-switch { .melt-switch {
display: flex; display: flex;
align-items: center; align-items: center;
} }
button { button {
--padding: 0.125rem; --padding: 0.125rem;
--width: 2.25rem; --width: 2.25rem;
position: relative; position: relative;
height: 1.5rem; height: 1.5rem;
width: var(--width); width: var(--width);
cursor: default; cursor: default;
border-radius: 9999px; border-radius: 9999px;
background-color: #19191d; background-color: hsl(var(--web-color-smooth));
transition: ease 150ms; transition: ease 150ms;
} }
.melt-switch :global([data-state='checked']) { .melt-switch :global([data-state="checked"]) {
background-color: #fd366e; background-color: #fd366e;
} }
.thumb { .thumb {
--thumb-size: 1.25rem; --thumb-size: 1.25rem;
position: absolute; position: absolute;
top: 50%; top: 50%;
display: block; display: block;
height: var(--thumb-size); height: var(--thumb-size);
width: var(--thumb-size); width: var(--thumb-size);
border-radius: 9999px; border-radius: 9999px;
background-color: #ffffff; background-color: #ffffff;
transform: translateX(var(--padding)) translateY(-50%); transform: translateX(var(--padding)) translateY(-50%);
transition: ease 150ms; transition: ease 150ms;
} }
:global(button[data-state='checked']) .thumb { :global(button[data-state="checked"]) .thumb {
--x: calc(var(--width) - var(--thumb-size) - var(--padding)); --x: calc(var(--width) - var(--thumb-size) - var(--padding));
transform: translateX(var(--x)) translateY(-50%); transform: translateX(var(--x)) translateY(-50%);
} }
</style> </style>

View File

@@ -1,13 +1,16 @@
export const GITHUB_STARS = '41.9K'; export const GITHUB_STARS = '42.8K';
export const BANNER_KEY = 'discord-banner-01'; // Change key to force banner to show again export const BANNER_KEY: Banners = 'init-banner-02'; // Change key to force banner to show again
export const SENTRY_DSN = export const SENTRY_DSN =
'https://27d41dc8bb67b596f137924ab8599e59@o1063647.ingest.us.sentry.io/4507497727000576'; 'https://27d41dc8bb67b596f137924ab8599e59@o1063647.ingest.us.sentry.io/4507497727000576';
/** /**
* History: * History:
* discord-banner-01 * discord-banner-01
* init-banner-02
* pricing-banner-01 * pricing-banner-01
*/ */
type Banners = 'discord-banner-01' | 'init-banner-02' | 'pricing-banner-01'
export type Social = { export type Social = {
icon: string; icon: string;
@@ -15,6 +18,40 @@ export type Social = {
link: string; link: string;
}; };
export type SocialShareOption = {
icon: string;
label: string;
link: string;
type: 'link' | 'copy';
};
export const socialSharingOptions: Array<SocialShareOption> = [
{
icon: 'web-icon-x',
label: 'Twitter',
link: 'https://x.com/intent/post?text={TITLE}\n&url={URL}',
type: 'link'
},
{
icon: 'web-icon-linkedin',
label: 'LinkedIn',
link: 'https://www.linkedin.com/sharing/share-offsite?text={TITLE}\n&url={URL}',
type: 'link'
},
{
icon: 'web-icon-ycombinator',
label: 'YCombinator',
link: 'https://news.ycombinator.com/submitlink?t={TITLE}\n&u={URL}',
type: 'link'
},
{
icon: 'web-icon-copy',
label: 'Copy',
link: '',
type: 'copy'
}
]
export const socials: Array<Social> = [ export const socials: Array<Social> = [
{ {
icon: 'web-icon-discord', icon: 'web-icon-discord',

View File

@@ -42,6 +42,7 @@
import { isMac } from '$lib/utils/platform'; import { isMac } from '$lib/utils/platform';
import { getContext, setContext } from 'svelte'; import { getContext, setContext } from 'svelte';
import { GITHUB_STARS } from '$lib/constants'; import { GITHUB_STARS } from '$lib/constants';
import { PUBLIC_APPWRITE_DASHBOARD } from '$env/static/public';
export let variant: DocsLayoutVariant = 'default'; export let variant: DocsLayoutVariant = 'default';
export let isReferences = false; export let isReferences = false;
@@ -99,7 +100,7 @@
</a> </a>
</div> </div>
<div class="web-mobile-header-end"> <div class="web-mobile-header-end">
<a href="https://cloud.appwrite.io" class="web-button web-is-only-desktop"> <a href={PUBLIC_APPWRITE_DASHBOARD} class="web-button web-is-only-desktop">
<span class="web-sub-body-500">Go to Console</span> <span class="web-sub-body-500">Go to Console</span>
</a> </a>
<button <button

View File

@@ -207,7 +207,7 @@
<li class="web-main-header-nav-item"> <li class="web-main-header-nav-item">
<a <a
class={classNames( 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", "data-[badge]:after:animate-scale-in data-[badge]:after:translate-full data-[badge]:relative data-[badge]:after:absolute data-[badge]:after:size-1.5 data-[badge]:after:rounded-full",
)} )}
href={navLink.href} href={navLink.href}
data-badge={navLink.showBadge ? "" : undefined} data-badge={navLink.showBadge ? "" : undefined}

View File

@@ -2,10 +2,11 @@ import { derived, writable } from 'svelte/store';
import { browser } from '$app/environment'; import { browser } from '$app/environment';
import { Account, Client, Teams } from '@appwrite.io/console'; import { Account, Client, Teams } from '@appwrite.io/console';
import { Query, type Models } from '@appwrite.io/console'; import { Query, type Models } from '@appwrite.io/console';
import { PUBLIC_APPWRITE_ENDPOINT } from '$env/static/public';
const client = new Client(); const client = new Client();
client.setEndpoint('https://cloud.appwrite.io/v1').setProject('console'); client.setEndpoint(PUBLIC_APPWRITE_ENDPOINT).setProject('console');
const account = new Account(client); const account = new Account(client);
const teams = new Teams(client); const teams = new Teams(client);
@@ -22,6 +23,7 @@ export async function createSource(
utmSource: string | null, utmSource: string | null,
utmCampaign: string | null, utmCampaign: string | null,
utmMedium: string | null utmMedium: string | null
// eslint-disable-next-line @typescript-eslint/no-explicit-any
): Promise<any> { ): Promise<any> {
const path = `/console/sources`; const path = `/console/sources`;
const params = { const params = {

View File

@@ -5,3 +5,15 @@ export const formatDate = (date: string | Date | number): string => {
const year = dt.getFullYear(); const year = dt.getFullYear();
return `${month} ${day}, ${year}`; return `${month} ${day}, ${year}`;
}; };
export const addDays = (date: Date, days: number) => {
return new Date(date.getTime() + days * 24 * 60 * 60 * 1000);
};
export const toReleaseDate = (date: Date) => {
return date.toLocaleDateString('en-US', {
weekday: 'long',
month: 'short',
day: 'numeric'
});
};

6
src/lib/utils/numbers.ts Normal file
View File

@@ -0,0 +1,6 @@
export const numberWithinRange = (number: number, min: number, max: number) =>
Math.min(Math.max(number, min), max);
export const getRandomNumber = (min: number, max: number) => {
return Math.random() * (max - min) + min;
};

View File

@@ -1,13 +1,16 @@
<script lang="ts"> <script lang="ts">
import { Media } from '$lib/UI'; import { Media } from '$lib/UI';
import { scroll } from '$lib/animations'; import { scroll } from '$lib/animations';
import { Article, FooterNav, MainFooter, Newsletter } from '$lib/components'; import { Article, FooterNav, MainFooter, Newsletter, Tooltip } from '$lib/components';
import { Main } from '$lib/layouts'; import { Main } from '$lib/layouts';
import { formatDate } from '$lib/utils/date'; import { formatDate } from '$lib/utils/date';
import { DEFAULT_HOST } from '$lib/utils/metadata'; import { DEFAULT_HOST } from '$lib/utils/metadata';
import type { AuthorData, CategoryData, PostsData } from '$routes/blog/content'; import type { AuthorData, CategoryData, PostsData } from '$routes/blog/content';
import { BLOG_TITLE_SUFFIX } from '$routes/titles'; import { BLOG_TITLE_SUFFIX } from '$routes/titles';
import { getContext } from 'svelte'; import { getContext } from 'svelte';
import { type SocialShareOption, socialSharingOptions } from '$lib/constants';
import { copy } from '$lib/utils/copy';
import { page } from '$app/stores';
export let title: string; export let title: string;
export let description: string; export let description: string;
@@ -32,6 +35,32 @@
} }
let readPercentage = 0; let readPercentage = 0;
enum CopyStatus {
Copy = 'Copy URL',
Copied = 'Copied'
}
let copyText = CopyStatus.Copy;
async function handleCopy() {
const blogPostUrl = encodeURI(`https://appwrite.io${$page.url.pathname}`)
await copy(blogPostUrl);
copyText = CopyStatus.Copied;
setTimeout(() => {
copyText = CopyStatus.Copy;
}, 1000);
}
function getShareLink(shareOption: SocialShareOption): string {
const blogPostUrl = encodeURI(`https://appwrite.io${$page.url.pathname}`)
const shareableLink = shareOption.link
.replace('{TITLE}', title + '.')
.replace('{URL}', blogPostUrl);
return shareableLink;
}
</script> </script>
<svelte:head> <svelte:head>
@@ -151,6 +180,48 @@
</ul> --> </ul> -->
</div> </div>
{/if} {/if}
<div class="share-post-section u-flex u-gap-16 u-margin-block-start-16 u-cross-center">
<span class="web-eyebrow u-padding-inline-end-8" style:color="#adadb0">
SHARE
</span>
<ul class="u-flex u-gap-8">
{#each socialSharingOptions as sharingOption}
<li class="share-list-item">
<Tooltip placement="bottom" disableHoverableContent={true}>
{#if sharingOption.type === 'link'}
<a
class="web-icon-button"
aria-label={sharingOption.label}
href={getShareLink(sharingOption)}
target="_blank"
rel="noopener, noreferrer"
>
<span class={sharingOption.icon} aria-hidden="true" />
</a>
{:else}
<button
class="web-icon-button"
aria-label={sharingOption.label}
on:click="{() => handleCopy()}"
>
<span class={sharingOption.icon} aria-hidden="true" />
</button>
{/if}
<svelte:fragment slot="tooltip">
{
sharingOption.type === 'copy'
? copyText
: `Share on ${sharingOption.label}`
}
</svelte:fragment>
</Tooltip>
</li>
{/each}
</ul>
</div>
</header> </header>
{#if cover} {#if cover}
<div class="web-media-container"> <div class="web-media-container">
@@ -220,4 +291,27 @@
background: hsl(var(--web-color-accent)); background: hsl(var(--web-color-accent));
z-index: 10000; z-index: 10000;
} }
@media (min-width: 1024px) {
.web-main-article-header {
padding-block-end: 0;
border-block-end: unset;
}
}
.share-post-section {
padding: 16px 0;
border-block-end: solid 0.0625rem hsl(var(--web-color-border));
border-block-start: solid 0.0625rem hsl(var(--web-color-border));
}
.web-icon-button {
.web-icon-x {
font-size: 16px;
}
.web-icon-copy {
font-size: 24px;
}
}
</style> </style>

View File

@@ -42,7 +42,7 @@ Reaching your organization's resource limits will have the following effects unt
- **Storage** - **Storage**
- File uploads are disabled. Persists across billing periods until the amount of storage used is below the plan limit. - File uploads are disabled. Persists across billing periods until the amount of storage used is below the plan limit.
{% /table %} {% /table %}
## Switching to Free plan and reaching limits {% #switching-to-free-plan-reaching-resource-limits %} ## Switching to Free plan and reaching limits {% #switching-to-free-plan-reaching-resource-limits %}
@@ -86,4 +86,4 @@ The following consequences should also apply at the project level if the Free pl
- **Functions** - **Functions**
- If more than 5 functions have been created, disable them in order of date created (oldest ones first). - If more than 5 functions have been created, disable them in order of date created (oldest ones first).
{% /table %} {% /table %}

View File

@@ -77,7 +77,7 @@ We kicked off the first Discord session with Appwrite Hero and Vonage dev advoca
### Content list ### Content list
- [Best practices for sending push notifications](https://appwrite.io/blog/post/push-notifications-best-practices) - [Best practices for sending push notifications](https://appwrite.io/blog/post/push-notifications-best-practices)
- [How tools like Twilio can simplify messaging for developers](https://appwrite.io/blog/post/simplify-messaging-twilio) - [How Twilio simplifies messaging for developers](https://appwrite.io/blog/post/simplify-messaging-twilio)
- [Product tour video](https://www.youtube.com/watch?v=QdDgPeuBZ1I) - [Product tour video](https://www.youtube.com/watch?v=QdDgPeuBZ1I)
## Another day ## Another day

View File

@@ -1,7 +1,7 @@
--- ---
layout: post layout: post
title: How Pink Design helped us improve web accessibility in our products. title: How Pink Design helped us improve web accessibility
description: description: Appwrites open-source UI library drastically improved our web accessibility.
date: 2022-11-14 date: 2022-11-14
cover: /images/pages/homepage/dashboard.png cover: /images/pages/homepage/dashboard.png
timeToRead: 3 timeToRead: 3

View File

@@ -0,0 +1,136 @@
---
layout: post
title: "Announcing Appwrite's new Integrations Catalog"
description: We are beyond excited to announce the launch of our brand-new Appwrite Integrations Catalog.
date: 2024-07-29
cover: /images/blog/integrations-catalog.png
timeToRead: 12
author: aditya-oberai
category: product
featured: false
---
Our new Integrations Catalog is designed to empower you by providing a seamless way to integrate your favorite tools, libraries, and services with Appwrite, making your development process even more efficient and enjoyable.
# One-stop shop for all your integrations
Youre building your next big project on top of Appwrite; your backend is all set up and ready to go to production. But then you realize you need a way to sell subscriptions and send emails, and youve decided at the last minute you want to add more OAuth2 providers. Imagine you had to build all these integrations for your app from scratch in a short space of time. With the new [Integrations Catalog](https://appwrite.io/integrations), we have done the heavy lifting for you. Youll find integrations for both Cloud and Self-hosted that cater to every aspect of your development needs, allowing you to build your app exactly as you intended.
# Whats inside the Appwrite Integrations Catalog?
Weve bundled all integrations into different categories. Here are a few notable providers (with many more to come!). Check out the full catalog on the Integrations page.
## Authentication
- [Phone auth](https://appwrite.io/docs/products/auth/phone-sms)
Phone auth allows you to authenticate users using One-Time Passwords (OTPs) sent to their mobile phones via SMS, making it a highly-convenient passwordless alternative to email-password authentication. You can use Twilio to enable phone authentication for your apps.
- [OAuth2](https://appwrite.io/docs/products/auth/oauth2)
You can set up OAuth2 with over 30 providers. OAuth is a way for apps to request limited access to a user's account on another service without needing the user's password. This keeps the users account more secure while making signing up and logging in all the more convenient.
Some of Appwrites OAuth integrations include Amazon, Appwrite, Discord, Google, and Notion.
## Deployments
- [GitHub](https://appwrite.io/docs/advanced/self-hosting/functions)
We've integrated GitHub to simplify your development workflow with Appwrite Functions. This integration enables the generation of repositories for function templates and manages automatic deployments for any Appwrite Functions through GitHub.
## Messaging
- [Twilio](https://appwrite.io/docs/products/messaging/twilio) for SMS
You can use the Twilio provider in Appwrite Messaging to send customized SMS messages to your users. Whether you need to send them instantly or schedule them for later, these messages can serve a variety of purposes like reminders, promotions, announcements, and even custom authentication flows.
- [Sendgrid](https://appwrite.io/docs/products/messaging/sendgrid) for Email
Send custom emails to your users with Sendgrid, either instantly or on a schedule. Use it to deliver reminders, promotions, and announcements, keeping your audience engaged and informed.
- [APNs and FCM](https://appwrite.io/docs/products/messaging/send-push-notifications) for Push notifications
Use Apple Push Notifications service (APNs) or Firebase Cloud Messaging to send in-app or push notifications. Deliver app updates, promotional offers, and other messages straight to your users devices.
- [Vonage](https://appwrite.io/docs/products/messaging/vonage) for WhatsApp messages
Going one step further? Use Vonage to send WhatsApp messages and connect with your users in a modern, user-friendly way.
## Storage
- Amazon S3
Store your files on self-hosted instances with Amazon S3. Amazon S3 provides scalable object storage that allows you to manage and access your data seamlessly, ensuring high availability and durability.
## Logging
Monitor your application's performance by tracing errors across your Appwrite instance and fixing them faster. These tools help you proactively manage issues and deliver a more stable experience:
- Sentry
- Raygun
- AppSignal
## AI
- Hugging Face and Generative AI
Enhance your app with AI through Appwrite's integrations. Build [with our AI function templates](https://appwrite.io/blog/post/building-with-ai-function-templates) from popular tools like Hugging Face, OpenAI, Perplexity, and more. Imagine creating [your very own AI chatbot](https://appwrite.io/blog/post/personal-chatbot-gpt-4o) with OpenAIs GPT-4o — it's easier than you think!
## Payments
- Stripe
Use Stripe to set up app [subscriptions](https://appwrite.io/docs/tutorials/subscriptions-with-stripe/step-1) and payments. It's designed to be straightforward so you can smoothly manage your transactions.
- Lemon Squeezy
Lemon Squeezy lets you set up payments and subscriptions quickly, so you can focus on your apps functionality. Check out [this video for a step-by-step guide](https://www.youtube.com/watch?v=dHo7lvpU6gs).
## Databases
- MongoDB Atlas and Upstash Vector
Easily manage your data using MongoDB Atlas and Upstash Vector integration. MongoDB Atlas offers a flexible database solution, and Upstash Vector improves query capabilities for fast and reliable data access. Together, they help you manage data efficiently and boost your apps performance.
## Search
- Algolia
Improve your apps search functionality with the Algolia integration. Algolia makes it straightforward to integrate practical search features, helping users easily find the information they need without hassle.
# Designed with your experience in mind
As with everything we do here at Appwrite, we heavily focus on the developer experience. Our main goal with the Integration Catalog is to meet your needs when integrating X with Appwrite. We focused on a set of principles that helped us deliver on our promise:
1. Appwrite developers need quick access to integration options, including everything provided in the console and future integrations.
2. Categories should group integrations, and a consistent hierarchy of information should exist on every page.
3. Finding new categories should be logical, intuitive, and fast.
4. The content should be presented in an easy-to-digest manner and help you find what youre looking for.
Missing anything? Have feedback? Let us know on [Discord](https://appwrite.io/discord)!
# Become a Technology Partner
With the launch of our Integrations Catalog, were also inviting developers and companies to join us as **Technology Partners**. This is your chance to integrate your solutions with Appwrite, reach a wider audience, and add value to the developer community.
## Why become a Technology Partner?
- **Improve dev experience:** Improve the experience by collaborating on an integration.
- **Increased exposure**: Feature your integration in front of our growing community of developers.
- **Collaborative marketing**: Partner with us on co-marketing efforts to highlight your integration.
- **Dedicated developer support**: Get assistance from our team to ensure your integration is top-notch.
- **Exclusive developer tools**: Access our API documentation, SDKs, and best practices to make your integration seamless.
# How to apply
Do you think your tool plays well with Appwrite? Visit our [Integrations Catalog Technology partners page](https://appwrite.io/integrations/technology-partner) and fill out the submission form. Tell us about your company, the integration youre proposing, and how it benefits developers using Appwrite. Our team will review your submission and get back to you with the next steps.
# More to come
We believe the Appwrite [Integrations Catalog](https://appwrite.io/integrations) will open up new possibilities for developers, making it easier than ever to build, connect, and deploy applications with Appwrite. Watch for updates on our changelog and socials as we add more integrations.
# Resources
- [Join us on discord](https://appwrite.io/discord)
- [Visit our documentation](https://appwrite.io/docs)
- [Get started with Appwrite](https://cloud.appwrite.io/)

View File

@@ -1,7 +1,7 @@
--- ---
layout: post layout: post
title: "Announcing Messaging: Push, Email and SMS directly from your Appwrite backend" title: "Announcing Messaging: Push, Email and SMS"
description: description: You can now send emails, SMS and push notifications with Appwrites easy-to-use APIs.
date: 2024-02-26 date: 2024-02-26
cover: /images/blog/messaging-announcement.png cover: /images/blog/messaging-announcement.png
timeToRead: 6 timeToRead: 6

View File

@@ -0,0 +1,55 @@
---
layout: post
title: "Announcing: A new Init. Faster. Smoother. Better."
description: Init is about to begin, and were bringing you many new features and products to be excited about. Experience everything new with Appwrite during the week of August 19 to 23.
date: 2024-08-07
cover: /images/blog/announcing-init-faster-smoother-better/init-cover.png
timeToRead: 4
author: eldad-fux
category: init
featured: true
---
The first [Init](https://appwrite.io/blog/post/announcing-init) earlier this year was a blast, and all of you who joined made it a week to remember and will go down in the history of Appwrite. Now were back with another Init to celebrate everything new with Appwrite. Faster. Smoother. Better.
Keep reading to learn more!
# An announcement a day
Starting on the 19th of August, we will bring you five days of exciting announcements. You can expect new products and features and maybe even a new SDK. You will find out soon…
# Claim your ticket
New Init equals a new ticket.
![Image of new Init registration ticket](/images/blog/announcing-init-faster-smoother-better/init-ticket.png)
[Claim your unique ticket to register for Init.](https://appwrite.io/init)
# Discord events
Just like last time, but not quite like last time, we will host Discord sessions with special guests from the community. Were also hosting a live coding session led by our very own Dennis Ivy.
The special guest who will be joining will be announced next week. So keep an eye out for our social media!
You can find the full list of events [here and RVSP](https://apwr.dev/InitKickOff).
# Limited edition Init swags
What would Init be without its limited-edition swags? Were sticking to our theme and setting you up with a swag Performance kit.
Whats inside? Special edition socks, a water bottle, sweatpants, and a vest.
![Image of limited edition Init swags](/images/blog/announcing-init-faster-smoother-better/init-swag.png)
How do you get your hands on one of the five swag packs? Claim your [ticket](https://appwrite.io/init/tickets) and keep an eye out on [X](https://x.com/appwrite), for we will share more details soon. We will announce winners each day of the week during Init.
How is that for a swag giveaway?
# Register and join the fun
Init takes place from 19 to 23 August 2024.
Don't miss out! Be sure to [register and claim](https://appwrite.io/init/tickets) your unique ticket.
**Init_ is the start of something new.**

View File

@@ -1,6 +1,6 @@
--- ---
layout: post layout: post
title: Introducing more and updated runtimes to the Appwrite ecosystem title: New and Updated Runtimes in the Appwrite Ecosystem
description: More runtimes, more flexibility. We're happy to add more and updated runtimes to allow you to build with your preferred tech stack. description: More runtimes, more flexibility. We're happy to add more and updated runtimes to allow you to build with your preferred tech stack.
date: 2024-03-01 date: 2024-03-01
cover: /images/blog/runtimes.png cover: /images/blog/runtimes.png

View File

@@ -49,6 +49,6 @@ Were excited to bring Messaging to Cloud and to offer the service for free fo
Learn more about Messaging in our resources: Learn more about Messaging in our resources:
- [Best practices for sending push notifications](https://appwrite.io/blog/post/push-notifications-best-practices) - [Best practices for sending push notifications](https://appwrite.io/blog/post/push-notifications-best-practices)
- [How tools like Twilio can simplify messaging for developers](https://appwrite.io/blog/post/simplify-messaging-twilio) - [How Twilio simplifies messaging for developers](https://appwrite.io/blog/post/simplify-messaging-twilio)
- [Documentation](https://appwrite.io/docs/products/messaging) - [Documentation](https://appwrite.io/docs/products/messaging)
- [Product tour](https://www.youtube.com/watch?v=QdDgPeuBZ1I) - [Product tour](https://www.youtube.com/watch?v=QdDgPeuBZ1I)

View File

@@ -1,7 +1,7 @@
--- ---
layout: post layout: post
title: Behind the pull request - tales from the open source world title: Behind the pull request - tales from the open source world
description: Celebrating the largest open source event, Hacktoberfest, with inspiring stories from contributors. description: Celebrating the largest open source event, Hacktoberfest, with inspiring stories from contributors.
cover: /images/blog/Behind_the_pull_request_Stories_from_contributors.png cover: /images/blog/Behind_the_pull_request_Stories_from_contributors.png
timeToRead: 4 timeToRead: 4
date: 2023-10-30 date: 2023-10-30

View File

@@ -0,0 +1,336 @@
---
layout: post
title: "Best database pagination technique"
description: "Learn how to fix database-related performance problems using offset and cursor pagination."
date: 2024-08-05
cover: /images/blog/best-pagination-technique/cover.png
timeToRead: 8
author: matej-baco
category: engineering
---
*This article was originally published in January 2022 and was updated for tone and accuracy.*
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?
To avoid these challenging scenarios, its important to be aware of potential risks from the start. A well-prepared developer can mitigate these issues. This article will help you address database-related performance problems using offset and cursor pagination.
# What is pagination?
Pagination is a strategy employed when querying any dataset that holds more than just a few hundred records. Thanks to pagination, we can split our large dataset into chunks that we can gradually fetch and display to the user, thus reducing the load on the database. Pagination also solves a lot of performance issues both on the client and server-side! Without pagination, you'd have to load the entire chat history only to read the latest message sent to you.
These days, pagination has almost become a necessity since **every** application is very likely to deal with large amounts of data. This data could be anything from user-generated content, content added by administrators or editors, or automatically generated audits and logs. As soon as your list grows to more than a few thousand items, your database will take too long to resolve each request and your front-end's speed and accessibility will suffer.
Now that we know what pagination is, how do we actually use it? And why is it necessary?
# Types of pagination
Two widely used pagination strategies are **offset** and **cursor**. Before learning everything about them, let's look at some websites using them.
First, let's visit GitHub's [Stargazer page](https://github.com/appwrite/appwrite/stargazers). Notice how the tab says `5,000+`, not an absolute number. Also, instead of standard page numbers, they use `Previous` and `Next` buttons.
Now, let's switch to [Amazon's products list](https://www.amazon.com/s?k=microwave) and notice the exact number of results: `364`. Standard pagination with all page numbers you can click through is `1 2 3 ... 20`.
It's clear that two tech **giants** could not agree on which solution is better! Why? Well, we'll need to use an answer developers hate: `It depends`. Let's explore both methods to understand their advantages, limitations, and performance implications.
# Offset pagination
Most websites use offset pagination because of its simplicity and how intuitive pagination is to users. To implement offset pagination, we will usually need two pieces of information:
- `limit` - Number of rows to fetch from the database
- `offset` - Number of rows to skip. Offset is like a page number but with a bit of math around it (`offset = (page-1) * limit`)
To get the first page of our data, we set the limit to 10 (because we want 10 items on the page) and offset to 0 (because we want to start counting 10 items from the 0th item). As a result, we will get ten rows.
To get the second page, we keep the limit at 10 (this doesn't change since we want every page to contain 10 rows ) and set the offset to 10 ( return results from the 10th row onwards. We continue this approach thereby allowing the end user to paginate through the results and see all of their content.
In the SQL world, such a query would be written as `SELECT * FROM posts OFFSET 10 LIMIT 10`.
Some websites implementing offset pagination also show the page number of the last page. How do they do it? Alongside results for each page, they also tend to return a `sum` attribute telling you how many rows there are in total. Using `limit`, `sum`, and a bit of math, you can calculate last page number using `lastPage = ceil(sum / limit)`
As convenient as this feature is for the user, developers struggle to scale this type of pagination. Looking at `sum` attribute, we can already see that it can take quite some time to count all rows in a database to the exact number. **Alongside that, the `offset` in databases is implemented in a way that loops through rows to know how many should be skipped.** That means that the higher our offset is, the longer our database query will take.
Another downside of offset pagination is that it doesn't play well with real-time data or data that changes often. Offset says how many rows we want to skip but doesn't account for row deletion or new rows being created. Such an offset can result in showing duplicate data or missing data.
# Cursor pagination
Cursors are successors to offsets, as they solve all issues that offset pagination has - performance, missing data and data duplication because it does not rely on the relative ordering of the rows as in the case of offset pagination. Instead, it relies on an index created and managed by the database. To implement cursor pagination, we will need the following information:
- `limit` - Same as before, the number of rows we want to show on one page
- `cursor` - ID of a reference element in the list. This can be the **first item** if you're querying the **previous** page and the **last item** if querying the **next** page.
- `cursorDirection` - If a user clicks `Next` or `Previous` (`after` or `before`)
When requesting the first page, we don't need to provide anything, just the limit `10`, saying how many rows we want to get. As a result, we get our ten rows.
To get to the next page, we use the ID of the last row as the `cursor` and set `cursor direction` to `after`.
Similarly, if we want to go to the previous page, we use the ID of the first row as a `cursor` and set the direction to `before`.
To compare, in the SQL world, we could write our query as `SELECT * FROM posts WHERE id > 10 LIMIT 10 ORDER BY id DESC`.
**Queries that use a cursor instead of offset are more performant because the `WHERE` query helps skip unwanted rows, while `OFFSET` needs to iterate over them, resulting in a full-table scan.** Skipping rows using `WHERE` can get even faster if you set up proper indexes on your IDs. The index is created by default in the case of your primary key.
Not only that, you no longer need to worry about rows being inserted or deleted. If you were using an offset of 10, you would expect exactly 10 rows to be present ahead of your current page. If this condition is not met, your query will return inconsistent results, leading to data duplication and even missing rows. This can happen if any of the rows ahead of your current page are deleted or new rows are added. Cursor pagination solves this by using the index of the last row you fetched, and it knows exactly where to start looking from when you request for more.
However, cursor pagination is a complex problem if you need to implement it on the backend. To implement cursor pagination, you will need `WHERE` and `ORDER BY` clauses in your query. In addition, you will also need `WHERE` clauses to filter by your required conditions. This can get quite complex very quickly, and you might end up with a huge nested query. You will also need to create indexes for all the columns you need to query.
Now that you have eliminated duplicates and missing data by switching to cursor pagination, you still have one problem left. Since you should not expose incremental numeric IDs to the user (for security reasons), you must now maintain a hashed version of each ID. Whenever you need to query a database, you convert this string ID to its numeric ID by looking at a table that holds these pairs. What if this row is missing? What if you click the `Next`button, take the last row's ID, and request the next page, but the database can't find the ID?
This is a really rare condition and only occurs if the row's ID that you are about to use as a cursor has been just deleted. We can solve this problem by trying previous rows or re-fetching data of earlier requests to update the last row with a new ID, but all of that brings a whole new level of complexity, and the developer needs to understand a bunch of new concepts, such as recursion and proper state management. Thankfully, services such as [Appwrite](https://appwrite.io/) take care of that, so you can simply use cursor pagination as a feature.
# Pagination in Appwrite
[Appwrite](https://appwrite.io/) is an open-source backend-as-a-service that abstracts all the complexity involved in building a modern application by providing you with a set of REST APIs for your core backend needs. Appwrite handles user authentication and authorization, databases, file storage, cloud functions, webhooks, messaging, and more. You can extend Appwrite using your favorite backend language if anything is missing.
Appwrite Database lets you store any text-based data that needs to be shared across your users. Appwrite's database allows you to create multiple collections (tables) and store multiple documents (rows) in it. Each collection has attributes (columns) configured to give your dataset a proper schema. You can also configure indexes to make your search queries more performant. When reading your data, you can use powerful queries, filter them, sort them, limit the number of results, and paginate over them.
Appwrite's pagination supports both offset and cursor pagination. Let's imagine you have a collection with ID `articles`. You can get documents from this collection with either offset or cursor pagination:
```jsx
// Setup
import { Appwrite, Databases, Query } from "appwrite";
const client = new Appwrite();
client
.setEndpoint('https://cloud.appwrite.io/v1') // Your API Endpoint
.setProject('articles-demo'); // Your project ID
const databases = new Databases(client);
// Offset pagination
databases.listDocuments(
'main', // Database ID
'articles', // Collection ID
[
Query.limit(10), // Limit, total documents in the response
Query.offset(500), // Offset, amount of documents to skip
]
).then((response) => {
console.log(response);
});
// Cursor pagination
databases.listDocuments(
'main', // Database ID
'articles', // Collection ID
[
Query.limit(10), // Limit, total documents in the response
Query.cursorAfter('61d6eb2281fce3650c2c', // ID of document I want to paginate after
]
).then((response) => {
console.log(response);
});
```
First, we import the Appwrite SDK and set up an instance that connects to our Appwrite Cloud project. Then, we list 10 documents using offset pagination. Right after, we write the exact same list documents query, but this time using `cursor` instead of offset pagination.
## Benchmarks
Weve frequently mentioned the term "performance" in this article without providing specific numbers, so lets create a benchmark together. We'll use Appwrite as our backend server, as it supports both offset and cursor pagination, and Node.js for writing the benchmark scripts, as JavaScript is straightforward to follow.
> You can find complete source code in the [GitHub repository.](https://github.com/appwrite)
First, you set up Appwrite, register a user, create a project and create a collection called `posts` with read permission set to `any`. To learn more about this process, visit the [Appwrite docs](https://appwrite.io/docs). You should now have Appwrite ready to go.
Use the following script to load data into our MariaDB database and prepare for the benchmark. We could use Appwrite SDK, but talking directly to MariaDB offers more optional write queries for large datasets.
```jsx
const config = {};
// Don't forget to fill config variable with secret information
console.log("🤖 Connecting to database ...");
const connection = await mysql.createConnection({
host: config.mariadbHost,
port: config.mariadbPost,
user: config.mariadbUser,
password: config.mariadbPassword,
database: `appwrite`,
});
const promises = [];
console.log("🤖 Database connection established");
console.log("🤖 Preparing database queries ...");
let index = 1;
for(let i = 0; i < 100; i++) {
const queryValues = [];
for(let l = 0; l < 10000; l++) {
queryValues.push(`('id${index}', '[]', '[]')`);
index++;
}
const query = `INSERT INTO _project_${config.projectId}_collection_posts (_uid, _read, _write) VALUES ${queryValues.join(", ")}`;
promises.push(connection.execute(query));
}
console.log("🤖 Pushing data. Get ready, this will take quite some time ...");
await Promise.all(promises);
console.error(`🌟 Successfully finished`);
```
> We used two layers for loops to increase the speed of the script. First for loop creates query executions that need to be awaited, and the second loop creates a long query holding multiple insert requests. Ideally, we would want everything in one request, but that is impossible due to MySQL configuration, so we split it into 100 requests.
Now you have 1 million documents inserted in less than a minute, and are ready to start the benchmarks. We will be using the [k6](https://k6.io/) load-testing library for this demo.
Let's benchmark the well-known and widely used offset pagination first. During each test scenario, we try to fetch a page with 10 documents, from different parts of our dataset. We will start with offset 0 and go all the way to an offset of 900k in increments of 100k. The benchmark is written in a way that makes only one request at a time to keep it as accurate as possible. We will also run the same benchmark ten times and measure average response times to ensure statistical significance. We'll be using k6's HTTP client to make requests to Appwrite's REST API.
```jsx
// script_offset.sh
import { Query } from 'appwrite';
import http from 'k6/http';
// Before running, make sure to run setup.js
export const options = {
iterations: 10,
summaryTimeUnit: "ms",
summaryTrendStats: ["avg"]
};
const config = JSON.parse(open("config.json"));
export default function () {
const offset = Query.offset(__ENV.OFFSET);
const limit = 10;
http.get(`${config.endpoint}/databases/main/collections/posts/documents?queries[]=${offset}&queries[]=${limit}`, {
headers: {
'content-type': 'application/json',
'X-Appwrite-Project': config.projectId
}
});
}
```
To run the benchmark with different offset configurations and store output in CSV files, I created a simple bash script. This script executes k6 ten times, with a different offset configuration each time. The output will be provided as console output.
```
#!/bin/bash
# benchmark_offset.sh
k6 -e OFFSET=0 run script.js
k6 -e OFFSET=100000 run script.js
k6 -e OFFSET=200000 run script.js
k6 -e OFFSET=300000 run script.js
k6 -e OFFSET=400000 run script.js
k6 -e OFFSET=500000 run script.js
k6 -e OFFSET=600000 run script.js
k6 -e OFFSET=700000 run script.js
k6 -e OFFSET=800000 run script.js
k6 -e OFFSET=900000 run script.js
```
Within a minute, all benchmarks finished, providing me with the average response time for each offset configuration. The results were as expected but not satisfying at all.
| | Offset pagination (ms) |
| --- | --- |
| 0% offset | 3.73 |
| 10% offset | 52.39 |
| 20% offset | 96.83 |
| 30% offset | 144.13 |
| 40% offset | 216.06 |
| 50% offset | 257.71 |
| 60% offset | 313.06 |
| 70% offset | 371.03 |
| 80% offset | 424.63 |
| 90% offset | 482.71 |
![Cursor pagination benchmark](/images/blog/best-pagination-technique/graph.png)
As you can see, offset 0 was pretty fast, responding in less than 4ms. The first jump was to offset 100k, and the change was drastic, increasing response times to 52ms. With each increase in the offset, the duration went up, resulting in almost 500ms to get ten documents after an offset of 900k documents. That is crazy!
Now let's update our script to use cursor pagination. We will update our script to use a cursor instead of offset and update our bash script to provide a cursor (document ID) instead of an offset number.
```jsx
// script_cursor.js
import { Query } from 'appwrite';
import http from 'k6/http';
// Before running, make sure to run setup.js
export const options = {
iterations: 10,
summaryTimeUnit: "ms",
summaryTrendStats: ["avg"]
};
const config = JSON.parse(open("config.json"));
export default function () {
const cursor = Query.cursorAfter(__ENV.CURSOR);
const limit = 10;
http.get(`${config.endpoint}/databases/main/collections/posts/documents?queries[]=${offset}&queries[]=${limit}`, {
headers: {
'content-type': 'application/json',
'X-Appwrite-Project': config.projectId
}
});
}
```
```
#!/bin/bash
# benchmark_cursor.sh
k6 -e CURSOR=id1 run script_cursor.js
k6 -e CURSOR=id100000 run script_cursor.js
k6 -e CURSOR=id200000 run script_cursor.js
k6 -e CURSOR=id300000 run script_cursor.js
k6 -e CURSOR=id400000 run script_cursor.js
k6 -e CURSOR=id500000 run script_cursor.js
k6 -e CURSOR=id600000 run script_cursor.js
k6 -e CURSOR=id700000 run script_cursor.js
k6 -e CURSOR=id800000 run script_cursor.js
k6 -e CURSOR=id900000 run script_cursor.js
```
After running the script, you could already tell that there was a performance boost, as there were noticeable differences in response times. Take a look at this table to compare the two pagination methods side-by-side.
| | Offset pagination (ms) | Cursor pagination (ms) |
| --- | --- | --- |
| 0% offset | 3.73 | 6.27 |
| 10% offset | 52.39 | 4.07 |
| 20% offset | 96.83 | 5.15 |
| 30% offset | 144.13 | 5.29 |
| 40% offset | 216.06 | 6.65 |
| 50% offset | 257.71 | 7.26 |
| 60% offset | 313.06 | 4.61 |
| 70% offset | 371.03 | 6.00 |
| 80% offset | 424.63 | 5.60 |
| 90% offset | 482.71 | 5.05 |
![Cursor pagination benchmark](/images/blog/best-pagination-technique/graph2.png)
As you can see, cursor pagination is highly efficient. The graph shows that cursor pagination maintains consistent performance regardless of the offset size, with each query performing as well as the first or last one. Loading the last page of a large list repeatedly can significantly impact performance.
If you are interested in running tests on your own machine, you can find the complete source code in a [GitHub repository](https://github.com/Meldiron/pagination-benchmark). The repository includes `README.md`, which explains the installation and running of scripts.
# Conclusion
Offset pagination offers a well-known pagination method where you can see page numbers and click through them. This intuitive method comes with a bunch of downsides, such as terrible performance with high offsets and a chance of data duplication and missing data.
Cursor pagination solves all of these problems and brings a reliable pagination system that is fast and can handle real-time (often changing) data. The downside of cursor pagination is not showing page numbers, its implementation complexity, and a new set of challenges to overcome, such as missing cursor ID.
Let's now get back to our original question: Why does GitHub use cursor pagination, but Amazon decided to go with offset pagination? Performance is not always the key. User experience is much more valuable than how many servers your business has to pay for.
I believe Amazon decided to go with offset because it improves UX, but that is a topic for another research. We can already notice that if we visit `amazon.com` and search for a `pen`, it says there are *exactly* `10 000` results, but you can only visit the first seven pages (350 results).
First, there are far more than 10,000 results, but Amazon imposes a limit. Secondly, you can only access the first seven pages; attempting to visit page 8 results in a 404 error. This indicates that Amazon is aware of the performance issues with offset pagination but continues to use it because users prefer seeing page numbers. They had to implement some limits, but it's unlikely that users navigate to page 100 of the search results.
Do you know what is better than reading about pagination? Trying it out! I would encourage you to try both methods because it's best to get first-hand experience. Setting up Appwrite takes less than a few minutes, and you can start playing with both pagination methods. If you have any questions, you can also reach us on [our Discord server](https://appwrite.io/discord).
# Resources
- [Is offset pagination dead? Why cursor pagination is taking over](https://uxdesign.cc/why-facebook-says-cursor-pagination-is-the-greatest-d6b98d86b6c0)
- [Offset pagination vs Cursor pagination](https://stackoverflow.com/questions/55744926/offset-pagination-vs-cursor-pagination)
- [How to implement cursors for pagination in an api](https://stackoverflow.com/questions/18314687/how-to-implement-cursors-for-pagination-in-an-api)
- [How to Implement Cursor Pagination Like a Pro](https://medium.com/swlh/how-to-implement-cursor-pagination-like-a-pro-513140b65f32)

View File

@@ -0,0 +1,136 @@
---
layout: post
title: "How to back up and restore your Appwrite data"
description: Learn how to back up and restore your Appwrite files.
date: 2024-08-07
cover: /images/blog/appwrite-backups-and-restores/cover.png
timeToRead: 3
author: bradley-schofield
category: engineering
---
*This article was originally published in May 2022 and was updated for accuracy.*
Backing up and restoring data is an extremely important part of running servers. It's a virtual safety net against most bad things that can happen. Made a bad config change? Restore a backup. Messed up an update? Restore a backup. Corrupted Drives? Restore a backup.
Not only that, backups can also come in handy when migrating data to other systems, like migrating a development server into a production environment or vice versa.
To make this process as easy as possible, we've written this guide to explain everything you need to know about backing up and restoring your Appwrite instance.
Appwrite consists of multiple sections, and most of it is stateless. This means there are only two main things you need to back up: Appwrite's database (MariaDB) and the Docker volumes that store functions, data, and uploads. The rest can be automatically handled and regenerated by Appwrite.
Please note that all these commands need to be run within the same directory as Appwrite's `docker-compose.yml`
With all that said, lets begin!
# Backing up the MariaDB Database
Due to the fact that Appwrite uses a Docker image of MariaDB it is extremely easy to dump the entire database with just one command and likewise to restore the dump.
Creating a Database backup is just one command:
```bash
docker-compose exec mariadb sh -c 'exec mysqldump --all-databases --add-drop-database -u"$MYSQL_USER" -p"$MYSQL_PASSWORD"' > ./dump.sql
```
This command does a couple things:
1. Docker-compose launches a temporary shell onto the MariaDB container to start work
2. It runs `mysqldump` on the server with two specific options `-all-databases` and `-add-drop-database` these are important since they ensure that when the backup is restored old data doesn't get overlapped with new data.
3. The output of `mysqldump` is piped into a `dump.sql` file. This is your backup.
# Restoring the MariaDB database
Restoring the database is similarly easy and also requires just one command to do:
```bash
docker-compose exec -T mariadb sh -c 'exec mysql -u"$MYSQL_USER" -p"$MYSQL_PASSWORD"' < dump.sql
```
This command is very simple once you break it down:
1. Docker-compose launches a temporary shell onto the MariaDB container to start work
2. Using the `mysql` command we restore the dump through a pipe
# Backing up your Docker volumes
Appwrite stores various types of data in Docker volumes, including file uploads and Cloud Function data. These volumes help coordinate data between the central Appwrite container and the various Appwrite workers. It's crucial to back up these uploads since they contain all your app's file uploads. Keep in mind that these backup commands might take some time to run, depending on the amount of data you need to back up.
> Before running these commands is it highly recommended to shut down your Appwrite instance to ensure you get a complete backup.
>
To backup the functions volume the command is:
```bash
mkdir -p backup && docker run --rm --volumes-from "$(docker-compose ps -q appwrite)" -v $PWD/backup:/backup ubuntu bash -c "cd /storage/functions && tar cvf /backup/functions.tar ."
```
and to backup the uploads volume the command is:
```bash
mkdir -p backup && docker run --rm --volumes-from "$(docker-compose ps -q appwrite)" -v $PWD/backup:/backup ubuntu bash -c "cd /storage/uploads && tar cvf /backup/uploads.tar ."
```
Finally, also backup your builds volume:
```bash
mkdir -p backup && docker run --rm --volumes-from "$(docker-compose ps -q appwrite)" -v $PWD/backup:/backup ubuntu bash -c "cd /storage/builds && tar cvf /backup/builds.tar ."
```
Both these commands do similar things and when you break them down they are pretty simple.
1. Start a new Docker container. This Docker container has a few special options
- `-rm` will delete the container once it's done running. The reason we want this is because this container is only being used to package up our backup and give it to the host machine.
- `-volume-from` This flag special as it will mount all of the volumes of the container we give it. To get the container ID we want we use a `$(docker-compose ps -q appwrite)` to get the ID within the command
- `v` This flag is being used to mount a volume onto our new container which will give us access to a backup folder we created using the `mkdir` command at the start
`ubuntu` is the image we are basing our new container on
1. Finally with this command created we change directories into the normal Appwrite mount point for uploads and create a tarball which will be created in the backup directory where we will be able to access it.
Once these commands are run you should find a new `backup` folder which contains`uploads.tar` and `functions.tar` these are your backups. Keep them safe.
# Restoring your Docker volumes
Restoring your Appwrite volumes is fairly simple as well. Move the backup folder you just created to your destination machine next to the `docker-compose.yml` file and simply run the following commands to restore the backup.
> Please note that the Appwrite instance should be shut down while running these commands.
Restoring functions volume:
```bash
docker run --rm --volumes-from "$(docker-compose ps -q appwrite)" -v $PWD/backup:/restore ubuntu bash -c "cd /storage/functions && tar xvf /restore/functions.tar --strip 1"
```
Restoring uploads volume:
```bash
docker run --rm --volumes-from "$(docker-compose ps -q appwrite)" -v $PWD/backup:/restore ubuntu bash -c "cd /storage/uploads && tar xvf /restore/uploads.tar --strip 1"
```
Restoring the builds volume:
```bash
docker run --rm --volumes-from "$(docker-compose ps -q appwrite)" -v $PWD/backup:/restore ubuntu bash -c "cd /storage/builds && tar xvf /restore/builds.tar --strip 1"
```
This command creates a new temporary Docker container similar to the backup command, but instead of creating a backup, it extracts the tar file back into the functions and uploads endpoints to restore the backup.
# Copy your `_APP_OPENSSL_KEY_V1` environment variable
Appwrite keeps all your data encrypted, to ensure that all your files, hashes and all other encrypted data is accessible to your new Appwrite instance make sure to copy the `_APP_OPENSSL_KEY_V1` from your original instance to your new instance.
# Conclusion
To create a complete Appwrite backup, you'll need to back up MariaDB and the two specified volumes. Once youve done this, ensure the backup is stored safely. The best practice is to store it in multiple locations, both locally and across different cloud services. Like with any cloud-native application, regular backups of your Appwrite instance are essential to prevent data loss in case of a server failure.
This process makes migrating an Appwrite installation simple. Just copy the backup files to another server and run the restore steps.
We hope you enjoyed this article! We love contributions and encourage you to take a look at our [open issues](https://github.com/appwrite/appwrite/issues) and [ongoing RFCs](https://github.com/appwrite/rfc/pulls).
If you get stuck anywhere, feel free to reach out to us on our [friendly support channels](https://appwrite.io/discord) run by humans.
Here are some handy links for more information:
- [Appwrite contribution guide](https://github.com/appwrite/appwrite/blob/master/CONTRIBUTING.md)
- [Appwrite Github](https://github.com/appwrite)
- [Appwrite docs](https://appwrite.io/docs)

View File

@@ -1,6 +1,6 @@
--- ---
layout: post layout: post
title: Improving user experience with passwordless authentication title: Improving UX with passwordless authentication
description: Understanding what passwordless authentication is and how it is a game-changer for both user experience and security. description: Understanding what passwordless authentication is and how it is a game-changer for both user experience and security.
date: 2024-03-18 date: 2024-03-18
cover: /images/blog/improve-ux-passwordless-auth/cover.png cover: /images/blog/improve-ux-passwordless-auth/cover.png

View File

@@ -1,6 +1,6 @@
--- ---
layout: post layout: post
title: "Introducing Enum SDK support: Enhanced dev experience across SDKs" title: "Introducing Enum SDK support: enhanced DX across SDKs"
description: A new feature that will reduce friction and improve your experience. description: A new feature that will reduce friction and improve your experience.
date: 2024-02-28 date: 2024-02-28
cover: /images/blog/enum-announcement.png cover: /images/blog/enum-announcement.png

View File

@@ -1,6 +1,6 @@
--- ---
layout: post layout: post
title: It's the contributors in open source who make the community great title: Open-source contributors make the community great
description: Celebrating open source, Appwrite 1.4 contributors, and the largest open source event, Hacktoberfest. Discover more about contributing and why you should start. description: Celebrating open source, Appwrite 1.4 contributors, and the largest open source event, Hacktoberfest. Discover more about contributing and why you should start.
date: 2023-10-01 date: 2023-10-01
cover: /images/blog/contributors-post-1.4.png cover: /images/blog/contributors-post-1.4.png

View File

@@ -108,6 +108,6 @@ We hope this blog improved your understanding of how Appwrite Messaging works. W
- [Play around with Messaging on the product page](https://appwrite.io/products/messaging) - [Play around with Messaging on the product page](https://appwrite.io/products/messaging)
- [Best practices for sending push notifications](https://appwrite.io/blog/post/push-notifications-best-practices) - [Best practices for sending push notifications](https://appwrite.io/blog/post/push-notifications-best-practices)
- [How tools like Twilio can simplify messaging for developers](https://appwrite.io/blog/post/simplify-messaging-twilio) - [How Twilio simplifies messaging for developers](https://appwrite.io/blog/post/simplify-messaging-twilio)
- [Documentation](https://appwrite.io/docs/products/messaging) - [Documentation](https://appwrite.io/docs/products/messaging)
- [Product video tour](https://www.youtube.com/watch?v=QdDgPeuBZ1I) - [Product video tour](https://www.youtube.com/watch?v=QdDgPeuBZ1I)

View File

@@ -1,7 +1,7 @@
--- ---
layout: post layout: post
title: "From passwords to protection: Implementing 2FA in your applications" title: How to implement 2FA in your applications
description: Understand the importance and process of integrating 2FA into their applications, enhancing security, and protecting user data effectively. description: Understand the importance and process of integrating 2FA into your applications, enhancing security, and protecting user data effectively.
date: 2024-02-28 date: 2024-02-28
cover: /images/blog/password-protection-2fa.png cover: /images/blog/password-protection-2fa.png
timeToRead: 7 timeToRead: 7

View File

@@ -1,6 +1,6 @@
--- ---
layout: post layout: post
title: "Password protection for developers: importance and best practices" title: "Password protection for developers: best practices"
description: Why it is necessary to implement strong password protection policies in your app and best practices to follow. description: Why it is necessary to implement strong password protection policies in your app and best practices to follow.
date: 2023-10-11 date: 2023-10-11
cover: /images/blog/password-protection.png cover: /images/blog/password-protection.png

View File

@@ -1,6 +1,6 @@
--- ---
layout: post layout: post
title: Build a personal chatbot with GPT-4o and Appwrite Functions title: Build a chatbot with GPT-4o and Appwrite Functions
description: Learn how to use OpenAI's new flagship model, GPT-4o to create an intelligent chatbot. description: Learn how to use OpenAI's new flagship model, GPT-4o to create an intelligent chatbot.
date: 2024-05-17 date: 2024-05-17
cover: /images/blog/personal-chatbot-gpt-4o/cover.png cover: /images/blog/personal-chatbot-gpt-4o/cover.png

View File

@@ -1,6 +1,6 @@
--- ---
layout: post layout: post
title: How tools like Twilio can simplify messaging for developers title: How Twilio simplifies messaging for developers
description: Learn the challenges of building a messaging system and how Twilio helps in simplifying implementation. description: Learn the challenges of building a messaging system and how Twilio helps in simplifying implementation.
date: 2024-02-26 date: 2024-02-26
cover: /images/blog/simplify-messaging-twilio.png cover: /images/blog/simplify-messaging-twilio.png

View File

@@ -0,0 +1,283 @@
---
layout: post
title: "Swift 101: how to build a library with Swift Package Manager"
description: Learn how to build a library with Swift Package Manager.
date: 2024-08-02
cover: /images/blog/swift-101/cover.png
timeToRead: 6
author: jake-barnby
category: engineering
featured: false
---
*Originally published in October 2021, this article has been updated for relevancy and comprehensiveness.*
The Swift Package Manager, or SwiftPM, has been part of Swift since version 3.0. Initially, it was just for server-side or command-line Swift projects. However, starting with Swift 5 and Xcode 11, SwiftPM now works across the entire Apple ecosystem for building apps. This is great because packages let you organize your code into reusable, logical groups that you can easily share between projects or even with the world.
## Modules
Before looking at **packages**, we first need to understand **modules**. Swift organizes code into modules. Each module defines a namespace and which parts of the code can be used from outside the module. You can define all of your code in a single module or break it up into multiple modules that can depend on each other. Using modules lets you easily build on your own reusable code or others code.
## Packages
So, what is a Swift package? A package is a collection of Swift source code files as well as a manifest file called `Package.swift`, that defines various properties about the package, such as its name, the products it produces, any dependencies it has, and the targets it is built up of.
## Anatomy of a package
- **Products** define the libraries and executables produced by a package. A library is simply a collection of files for use as a dependency by other Swift code. An executable is a package that can be run, such as a web server.
- **Dependencies** are other Swift Packages you want to use code from within your package.
- **Targets** are what defines the module(s) that a package contains and that other packages can import. Targets define their own dependencies and can depend on other targets in the package or products from packages that this package depends on.
## Creating a Swift package
> This tutorial assumes you already have Swift installed. You can check by running swift-help from your terminal.
>
Creating a Swift Package from the command line is easy and can be completed with one simple command from the directory in which you want to create your package. For this example, we'll start with a directory named `FooPackage`.
```
$ mkdir FooPackage
$ cd FooPackage
FooPackage$ swift package init --type=library
```
Thats it! Therell be some output detailing the files created for your new package. You should see:
- 1 source file created inside a `Sources` directory
- 1 test source file inside a `Tests` directory
- A `Package.swift` manifest file at the root level
- A README.md file at the root level
- A .gitignore file at the root level
Of these files, only the single file in the `Sources` directory and the manifest file are required for the package to build. This means you could easily create your own package by manually creating these two files as well.
By default, the `Sources` directory must contain all source code for the package, but you can use sub-directories to define sub-modules if they are also defined as separate targets in your manifest file. Let's take a look at the generated `Package.swift` for the new package to see the pieces we've discussed so far:
```swift
// swift-tools-version:5.3
// The swift-tools-version declares the minimum version of Swift required to build this package.
import PackageDescription
let package = Package(
name: "TestPackage",
products: [
.library(
name: "FooPackage",
targets: ["FooPackage"]),
],
dependencies: [
],
targets: [
.target(
name: "FooPackage",
dependencies: [
]
)
.testTarget(
name: "FooPackageTests",
dependencies: [
"FooPackage"
]
)
]
)
```
Here, you can see that our package defines one **library**, `TestPackage`, one **target** of the same name, and one test target, which depends on the module target.
## The first build
Now that the package has been created lets build it for the first time with the build command:
```
$ swift build
```
Because the package has no dependencies or code yet, this should be completed almost instantly, displaying “Build Completed!” on success.
## Adding dependencies
Let's add a dependency and some code. Adding dependencies with SwiftPM is easy as you can use git URL's directly. We can add the following to our `Package.swift` top-level dependencies block to allow us to the Appwrite Swift SDK in our library:
```swift
.package(name: "Appwrite", url: "https://github.com/appwrite/sdk-for-swift", from: "0.1.0")
```
This declares that our package will pull in the code from the `Appwrite` module in the `sdk-for-swift` repository on GitHub, from the tag `0.1.0` and allow us to add it to our target dependencies as follows:
```swift
.target(
name: "FooPackage",
dependencies: [
"Appwrite"
]
)
```
Here, we added `Appwrite`, as this is the name of the library we're using from the `sdk-for-swift` repository.
Let's take a look at the full manifest file with the new dependency added:
```swift
// swift-tools-version:5.3
// The swift-tools-version declares the minimum version of Swift required to build this package.
import PackageDescription
let package = Package(
name: "FooPackage",
products: [
.library(
name: "FooPackage",
targets: ["FooPackage"]),
],
dependencies: [
.package(name: "Appwrite", url: "https://github.com/appwrite/sdk-for-swift", from: "0.1.0")
],
targets: [
.target(
name: "FooPackage",
dependencies: [
"Appwrite"
]
)
.testTarget(
name: "FooPackageTests",
dependencies: [
"FooPackage"
]
)
]
)
```
Since we've changed the dependencies of our package, we need to **resolve** them. This will happen automatically the first time you run `swift build` with a new dependency, but if you manually update a version, you'll need to manually resolve the new version. This can be done by running:
```
$ swift package resolve
```
This will update the `Package.resolved` to contain the version metadata about the `Appwrite` module we just added.
> **What's going on here?**
>
>
> Swift Package Manager uses a lockfile system similar to `package.lock` for NPM and `composer.lock` for Composer. This comes in the form of a file called `Package.resolved`, which contains metadata about the packages dependencies versions, as well as their transitive dependencies. When you run `swift build` and the dependencies are fetched, the versions from the `Package.resolved` file will be used if found.
>
Once resolved, we can build our package with `swift build` again. This time we'll see the `sdk-for-swift` repository pulled into the build checkouts, as well as built with the rest of the library.
## Adding library code
Time to add some code. Let's open up the source file created earlier as `Sources/FooPackage/FooPackage.swift` and update with the following:
```swift
import Appwrite
struct FooPackage {
static let client = Client()
static let account = Account(client)
public static func login(
endpoint: String,
projectId: String,
email: String,
password: String,
completion: @escaping (Result<Session, AppwriteError>) -> Void
) {
client
.setEndpoint(endpoint)
.setProject(projectId)
account.createSession(
email: email,
password: password,
completion: completion
)
}
}
```
We now have a login function! We just need to deploy the package, and we'll be able to use the login function from any other package or Apple app.
## Deploying the package
Fortunately deploying packages with Swift Package Manager is very easy. As the packages are Git based, all you need to do is push your changes to your default branch and create a tag for your release:
```
$ git init
$ git add .
$ git remote add origin [GitHub Repository URL]
$ git commit -m "Initial Commit"
$ git tag 1.0.0
$ git push origin main --tags
```
## Using as a dependency
Using the same method we used earlier to add the Appwrite Apple SDK as a dependency, we can now add the newly deployed package as a dependency of a second package:
```swift
...
dependencies: [
.package(name: "FooPackage", url: "https://github.com/[YOUR GITHUB USERNAME]/[YOUR GITHUB REPOSITORY]", from: "1.0.0")
],
targets: [
.target(
name: "FooPackage",
dependencies: [
"FooPackage"
]
)
]
...
)
```
## Using the dependency
With the package added as a dependency, we can now use the function we defined earlier anywhere in the second package:
```swift
import FooPackage
FooPackage.login(
endpoint: "http://localhost/v1",
projectId: "6bfgh45fng3",
email: "test@test.test",
password: "password"
) { result in
...
}
```
## Updating your package
The process for updating your package is the same as deploying the initial version. You just need to push your changes to the default branch and add a new version tag.
## That's it!
You've now created, deployed, used and updated your very own Swift Package! Packages are a great way to re-use code and share your creations with the world.
## Resources
- [Swift](https://swift.org/)
- [Swift Package Manager](https://swift.org/package-manager/)
- [Creating Swift Packages with Xcode](https://developer.apple.com/documentation/xcode/creating_a_standalone_swift_package_with_xcode)

View File

@@ -1,6 +1,6 @@
--- ---
layout: changelog layout: changelog
title: "Introducing new Database operators: or & contains query methods" title: "New Database operators: OR & contains query methods"
date: 2024-02-29 date: 2024-02-29
cover: /images/changelog/2024-02-29.png cover: /images/changelog/2024-02-29.png
--- ---

View File

@@ -1,6 +1,6 @@
--- ---
layout: changelog layout: changelog
title: Introducing more and updated runtimes to the Appwrite Cloud ecosystem title: Introducing new runtimes to the Appwrite ecosystem
date: 2024-03-01 date: 2024-03-01
cover: /images/changelog/2024-03-01.png cover: /images/changelog/2024-03-01.png
--- ---

View File

@@ -0,0 +1,16 @@
---
layout: changelog
title: "Announcing: A new Init. Faster. Smoother. Better."
date: 2024-08-07
cover: /images/changelog/2024-08-07.png
---
Init is about to begin, and were bringing you many new features and products to be excited about. Experience everything new with Appwrite during the week of August 19 to 23.
The first [Init](https://appwrite.io/blog/post/announcing-init) earlier this year was a blast, and all of you who joined made it a week to remember and will go down in the history of Appwrite. Now were back with another Init to celebrate everything new with Appwrite. Get ready to *not* break a sweat with the new and improved Appwrite. Bringing you a faster, smoother, better experience.
Keep reading to learn more!
{% arrow_link href="/blog/post/announcing-init-faster-smoother-better" %}
Read the announcement to learn more
{% /arrow_link %}

View File

@@ -1,10 +1,13 @@
<script lang="ts"> <script lang="ts">
import { FooterNav, MainFooter } from '$lib/components'; import { FooterNav, MainFooter, Tooltip } from '$lib/components';
import PreFooter from '$lib/components/PreFooter.svelte'; import PreFooter from '$lib/components/PreFooter.svelte';
import { Main } from '$lib/layouts'; import { Main } from '$lib/layouts';
import { formatDate } from '$lib/utils/date'; import { formatDate } from '$lib/utils/date';
import { DEFAULT_DESCRIPTION, DEFAULT_HOST } from '$lib/utils/metadata'; import { DEFAULT_DESCRIPTION, DEFAULT_HOST } from '$lib/utils/metadata';
import { CHANGELOG_TITLE_SUFFIX } from '$routes/titles'; import { CHANGELOG_TITLE_SUFFIX } from '$routes/titles';
import { type SocialShareOption, socialSharingOptions } from '$lib/constants';
import { page } from '$app/stores';
import { copy } from '$lib/utils/copy';
export let data; export let data;
@@ -15,6 +18,34 @@
? DEFAULT_HOST + data.cover ? DEFAULT_HOST + data.cover
: `${DEFAULT_HOST}/images/open-graph/website.png` : `${DEFAULT_HOST}/images/open-graph/website.png`
}; };
const sharingOptions = socialSharingOptions.filter(option => option.label !== 'YCombinator');
enum CopyStatus {
Copy = 'Copy URL',
Copied = 'Copied'
}
let copyText = CopyStatus.Copy;
async function handleCopy() {
const blogPostUrl = encodeURI(`https://appwrite.io${$page.url.pathname}`)
await copy(blogPostUrl);
copyText = CopyStatus.Copied;
setTimeout(() => {
copyText = CopyStatus.Copy;
}, 1000);
}
function getShareLink(shareOption: SocialShareOption): string {
const blogPostUrl = encodeURI(`https://appwrite.io${$page.url.pathname}`)
const shareableLink = shareOption.link
.replace('{TITLE}', seo.title + '.')
.replace('{URL}', blogPostUrl);
return shareableLink;
}
</script> </script>
<svelte:head> <svelte:head>
@@ -55,6 +86,48 @@
</li> </li>
</ul> </ul>
<h1 class="web-title web-u-color-text-primary">{data.title}</h1> <h1 class="web-title web-u-color-text-primary">{data.title}</h1>
<div class="share-post-section u-flex u-gap-16 u-margin-block-start-16 u-cross-center">
<span
class="web-eyebrow u-padding-inline-end-8" style:color="#adadb0">
SHARE
</span>
<ul class="u-flex u-gap-8">
{#each sharingOptions as sharingOption}
<li class="share-list-item">
<Tooltip placement="bottom" disableHoverableContent={true}>
{#if sharingOption.type === 'link'}
<a
class="web-icon-button"
aria-label={sharingOption.label}
href={getShareLink(sharingOption)}
target="_blank"
rel="noopener, noreferrer"
>
<span class={sharingOption.icon} aria-hidden="true" />
</a>
{:else}
<button
class="web-icon-button"
aria-label={sharingOption.label}
on:click="{() => handleCopy()}"
>
<span class={sharingOption.icon} aria-hidden="true" />
</button>
{/if}
<svelte:fragment slot="tooltip">
{
sharingOption.type === 'copy'
? copyText
: `Share on ${sharingOption.label}`
}
</svelte:fragment>
</Tooltip>
</li>
{/each}
</ul>
</div>
</header> </header>
{#if data.cover} {#if data.cover}
<div class="web-media-container"> <div class="web-media-container">
@@ -80,3 +153,28 @@
</div> </div>
</div> </div>
</Main> </Main>
<style lang="scss">
@media (min-width: 1024px) {
.web-main-article-header {
padding-block-end: 0;
border-block-end: unset;
}
}
.share-post-section {
padding: 16px 0;
border-block-end: solid 0.0625rem hsl(var(--web-color-border));
border-block-start: solid 0.0625rem hsl(var(--web-color-border));
}
.web-icon-button {
.web-icon-x {
font-size: 16px;
}
.web-icon-copy {
font-size: 24px;
}
}
</style>

View File

@@ -95,9 +95,9 @@
const metrics = [ const metrics = [
{ metric: `${GITHUB_STARS}+`, description: 'GitHub Stars' }, { metric: `${GITHUB_STARS}+`, description: 'GitHub Stars' },
{ metric: '3K+', description: 'Pull Requests' }, { metric: '3K+', description: 'Pull Requests' },
{ metric: '18K+', description: 'Commits' }, { metric: '21K+', description: 'Commits' },
{ metric: '3K+', description: 'Issues' }, { metric: '3K+', description: 'Issues' },
{ metric: '600+', description: 'Open Issues' }, { metric: '500+', description: 'Open Issues' },
{ metric: '2.5K+', description: 'Closed Issues' }, { metric: '2.5K+', description: 'Closed Issues' },
{ metric: '3.5K+', description: 'Forks' }, { metric: '3.5K+', description: 'Forks' },
{ metric: '800+', description: 'Contributors' } { metric: '800+', description: 'Contributors' }

View File

@@ -128,6 +128,12 @@
icon: 'web-icon-platform', icon: 'web-icon-platform',
isParent: true isParent: true
}, },
{
label: 'Integrations',
href: '/integrations',
icon: 'icon-puzzle',
isParent: true
},
{ {
label: 'Migrations', label: 'Migrations',
href: '/docs/advanced/migrations', href: '/docs/advanced/migrations',

View File

@@ -17,10 +17,10 @@ Appwrite supports a variety of authentication methods to fit every app and every
{% cards %} {% cards %}
{% cards_item href="/docs/products/auth/email-password" title="Email and password" %} {% cards_item href="/docs/products/auth/email-password" title="Email and password" %}
Email and password login with just a few lines of code secured with state of the art Argon2 hasing. Email and password login with just a few lines of code secured with state of the art Argon2 hashing.
{% /cards_item %} {% /cards_item %}
{% cards_item href="/docs/products/auth/phone-sms" title="Phone (SMS)" %} {% cards_item href="/docs/products/auth/phone-sms" title="Phone (SMS)" %}
Log in users with out a password using their phone number and SMS verification. Log in users without a password using their phone number and SMS verification.
{% /cards_item %} {% /cards_item %}
{% cards_item href="/docs/products/auth/magic-url" title="Magic URL" %} {% cards_item href="/docs/products/auth/magic-url" title="Magic URL" %}
Passwordless login with a magic link sent to the user's email. Passwordless login with a magic link sent to the user's email.
@@ -54,4 +54,4 @@ Appwrite Authentication provides permissions for individual users and groups of
# Built in preferences {% #built-in-preferences %} # Built in preferences {% #built-in-preferences %}
Appwrite Authentication comes with built-in [preferences](/docs/products/auth/accounts#preferences) for users to manage their account settings. Appwrite Authentication comes with built-in [preferences](/docs/products/auth/accounts#preferences) for users to manage their account settings.
Store notification settings, themes, and other user preferences to be shared across devices. Store notification settings, themes, and other user preferences to be shared across devices.

View File

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 3.3 KiB

View File

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

View File

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

Before

Width:  |  Height:  |  Size: 386 KiB

After

Width:  |  Height:  |  Size: 386 KiB

View File

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

View File

Before

Width:  |  Height:  |  Size: 200 KiB

After

Width:  |  Height:  |  Size: 200 KiB

View File

Before

Width:  |  Height:  |  Size: 60 KiB

After

Width:  |  Height:  |  Size: 60 KiB

View File

Before

Width:  |  Height:  |  Size: 252 KiB

After

Width:  |  Height:  |  Size: 252 KiB

View File

Before

Width:  |  Height:  |  Size: 942 B

After

Width:  |  Height:  |  Size: 942 B

View File

Before

Width:  |  Height:  |  Size: 288 KiB

After

Width:  |  Height:  |  Size: 288 KiB

View File

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

Before

Width:  |  Height:  |  Size: 92 KiB

After

Width:  |  Height:  |  Size: 92 KiB

View File

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

View File

Before

Width:  |  Height:  |  Size: 438 KiB

After

Width:  |  Height:  |  Size: 438 KiB

View File

Before

Width:  |  Height:  |  Size: 230 KiB

After

Width:  |  Height:  |  Size: 230 KiB

View File

Before

Width:  |  Height:  |  Size: 94 KiB

After

Width:  |  Height:  |  Size: 94 KiB

View File

Before

Width:  |  Height:  |  Size: 227 KiB

After

Width:  |  Height:  |  Size: 227 KiB

View File

Before

Width:  |  Height:  |  Size: 87 KiB

After

Width:  |  Height:  |  Size: 87 KiB

View File

Before

Width:  |  Height:  |  Size: 242 KiB

After

Width:  |  Height:  |  Size: 242 KiB

View File

Before

Width:  |  Height:  |  Size: 104 KiB

After

Width:  |  Height:  |  Size: 104 KiB

View File

Before

Width:  |  Height:  |  Size: 338 KiB

After

Width:  |  Height:  |  Size: 338 KiB

View File

Before

Width:  |  Height:  |  Size: 86 KiB

After

Width:  |  Height:  |  Size: 86 KiB

View File

Before

Width:  |  Height:  |  Size: 226 KiB

After

Width:  |  Height:  |  Size: 226 KiB

View File

Before

Width:  |  Height:  |  Size: 85 KiB

After

Width:  |  Height:  |  Size: 85 KiB

View File

Before

Width:  |  Height:  |  Size: 343 KiB

After

Width:  |  Height:  |  Size: 343 KiB

View File

Before

Width:  |  Height:  |  Size: 88 KiB

After

Width:  |  Height:  |  Size: 88 KiB

View File

Before

Width:  |  Height:  |  Size: 240 KiB

After

Width:  |  Height:  |  Size: 240 KiB

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