Merge branch 'main' into regions-map

This commit is contained in:
Jesse Winton
2025-04-09 09:56:12 -04:00
1949 changed files with 28724 additions and 8649 deletions

View File

@@ -2,11 +2,13 @@ PUBLIC_APPWRITE_COL_MESSAGES_ID=
PUBLIC_APPWRITE_COL_THREADS_ID= PUBLIC_APPWRITE_COL_THREADS_ID=
PUBLIC_APPWRITE_DB_MAIN_ID= PUBLIC_APPWRITE_DB_MAIN_ID=
PUBLIC_APPWRITE_FN_TLDR_ID= PUBLIC_APPWRITE_FN_TLDR_ID=
PUBLIC_APPWRITE_ENDPOINT= PUBLIC_APPWRITE_ENDPOINT=https://cloud.appwrite.io/v1
PUBLIC_APPWRITE_PROJECT_ID= PUBLIC_APPWRITE_PROJECT_ID=
PUBLIC_APPWRITE_DASHBOARD=https://cloud.appwrite.io
PUBLIC_APPWRITE_PROJECT_INIT_ID= PUBLIC_APPWRITE_PROJECT_INIT_ID=
PUBLIC_GROWTH_ENDPOINT= PUBLIC_GROWTH_ENDPOINT=
PUBLIC_POSTHOG_API_KEY=
APPWRITE_DB_INIT_ID= APPWRITE_DB_INIT_ID=
APPWRITE_COL_INIT_ID= APPWRITE_COL_INIT_ID=
APPWRITE_API_KEY_INIT= APPWRITE_API_KEY_INIT=
SENTRY_AUTH_TOKEN= SENTRY_AUTH_TOKEN=

View File

@@ -1,13 +0,0 @@
.DS_Store
node_modules
/build
/.svelte-kit
/package
.env
.env.*
!.env.example
# Ignore files for PNPM, NPM and YARN
pnpm-lock.yaml
package-lock.json
yarn.lock

View File

@@ -1,30 +0,0 @@
module.exports = {
root: true,
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:svelte/recommended',
'prettier'
],
parser: '@typescript-eslint/parser',
plugins: ['@typescript-eslint'],
parserOptions: {
sourceType: 'module',
ecmaVersion: 2020,
extraFileExtensions: ['.svelte']
},
env: {
browser: true,
es2017: true,
node: true
},
overrides: [
{
files: ['*.svelte'],
parser: 'svelte-eslint-parser',
parserOptions: {
parser: '@typescript-eslint/parser'
}
}
]
};

View File

@@ -15,24 +15,24 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout the repo - name: Checkout the repo
uses: actions/checkout@v2 uses: actions/checkout@v4
- name: Login to DockerHub - name: Login to DockerHub
uses: docker/login-action@v1 uses: docker/login-action@v3
with: with:
registry: ghcr.io registry: ghcr.io
username: ${{ github.actor }} username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }} password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push - name: Build and push
uses: docker/build-push-action@v2 uses: docker/build-push-action@v6
with: with:
context: . context: .
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_ENDPOINT=${{ vars.PUBLIC_APPWRITE_ENDPOINT }}"
"PUBLIC_APPWRITE_DASHBOARD=${{ secrets.PUBLIC_APPWRITE_DASHBOARD }}" "PUBLIC_APPWRITE_DASHBOARD=${{ vars.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 }}"
@@ -40,11 +40,13 @@ jobs:
"PUBLIC_APPWRITE_FN_TLDR_ID=${{ vars.PUBLIC_APPWRITE_FN_TLDR_ID }}" "PUBLIC_APPWRITE_FN_TLDR_ID=${{ vars.PUBLIC_APPWRITE_FN_TLDR_ID }}"
"PUBLIC_APPWRITE_PROJECT_INIT_ID=${{ vars.PUBLIC_APPWRITE_PROJECT_INIT_ID }}" "PUBLIC_APPWRITE_PROJECT_INIT_ID=${{ vars.PUBLIC_APPWRITE_PROJECT_INIT_ID }}"
"PUBLIC_GROWTH_ENDPOINT=${{ vars.PUBLIC_GROWTH_ENDPOINT }}" "PUBLIC_GROWTH_ENDPOINT=${{ vars.PUBLIC_GROWTH_ENDPOINT }}"
"PUBLIC_POSTHOG_API_KEY=${{ vars.PUBLIC_POSTHOG_API_KEY }}"
"APPWRITE_DB_INIT_ID=${{ secrets.APPWRITE_DB_INIT_ID }}" "APPWRITE_DB_INIT_ID=${{ secrets.APPWRITE_DB_INIT_ID }}"
"APPWRITE_COL_INIT_ID=${{ secrets.APPWRITE_COL_INIT_ID }}" "APPWRITE_COL_INIT_ID=${{ secrets.APPWRITE_COL_INIT_ID }}"
"APPWRITE_API_KEY_INIT=${{ secrets.APPWRITE_API_KEY_INIT }}" "APPWRITE_API_KEY_INIT=${{ secrets.APPWRITE_API_KEY_INIT }}"
"GITHUB_TOKEN=${{ secrets.GITHUB_TOKEN }}" "GITHUB_TOKEN=${{ secrets.GITHUB_TOKEN }}"
"SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }}" "SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }}"
"SENTRY_RELEASE=${{ github.event.release.tag_name }}"
deploy: deploy:
needs: build needs: build
@@ -72,7 +74,7 @@ jobs:
echo "_APP_VERSION=${{ env.TAG }}" >> .env echo "_APP_VERSION=${{ env.TAG }}" >> .env
echo "_APP_DOMAIN=${{ secrets.PRD_APP_DOMAIN }}" >> .env echo "_APP_DOMAIN=${{ secrets.PRD_APP_DOMAIN }}" >> .env
echo "_APP_SYSTEM_SECURITY_EMAIL_ADDRESS=${{ secrets.APP_SYSTEM_SECURITY_EMAIL_ADDRESS }}" >> .env echo "_APP_SYSTEM_SECURITY_EMAIL_ADDRESS=${{ secrets.APP_SYSTEM_SECURITY_EMAIL_ADDRESS }}" >> .env
echo "SEMATEXT_TOKEN=${{ secrets.SEMATEXT_TOKEN }}" >> .env echo "_APP_BETTER_STACK_INCIDENT_URL=${{ secrets.BETTER_STACK_INCIDENT_URL }}" >> .env
echo ${{ secrets.GH_REGISTRY_TOKEN }} | docker login ghcr.io --username ${{ env.REGISTRY_USERNAME }} --password-stdin echo ${{ secrets.GH_REGISTRY_TOKEN }} | docker login ghcr.io --username ${{ env.REGISTRY_USERNAME }} --password-stdin
docker-compose -f ${{ env.STACK_FILE }} config docker-compose -f ${{ env.STACK_FILE }} config

View File

@@ -17,24 +17,24 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout the repo - name: Checkout the repo
uses: actions/checkout@v2 uses: actions/checkout@v4
- name: Login to DockerHub - name: Login to DockerHub
uses: docker/login-action@v1 uses: docker/login-action@v3
with: with:
registry: ghcr.io registry: ghcr.io
username: ${{ github.actor }} username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }} password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push - name: Build and push
uses: docker/build-push-action@v2 uses: docker/build-push-action@v6
with: with:
context: . context: .
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_ENDPOINT=${{ vars.PUBLIC_APPWRITE_ENDPOINT }}"
"PUBLIC_APPWRITE_DASHBOARD=${{ secrets.PUBLIC_APPWRITE_DASHBOARD }}" "PUBLIC_APPWRITE_DASHBOARD=${{ vars.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 }}"
@@ -42,6 +42,7 @@ jobs:
"PUBLIC_APPWRITE_FN_TLDR_ID=${{ vars.PUBLIC_APPWRITE_FN_TLDR_ID }}" "PUBLIC_APPWRITE_FN_TLDR_ID=${{ vars.PUBLIC_APPWRITE_FN_TLDR_ID }}"
"PUBLIC_APPWRITE_PROJECT_INIT_ID=${{ vars.PUBLIC_APPWRITE_PROJECT_INIT_ID }}" "PUBLIC_APPWRITE_PROJECT_INIT_ID=${{ vars.PUBLIC_APPWRITE_PROJECT_INIT_ID }}"
"PUBLIC_GROWTH_ENDPOINT=${{ vars.PUBLIC_GROWTH_ENDPOINT }}" "PUBLIC_GROWTH_ENDPOINT=${{ vars.PUBLIC_GROWTH_ENDPOINT }}"
"PUBLIC_POSTHOG_API_KEY=${{ vars.PUBLIC_POSTHOG_API_KEY }}"
"APPWRITE_DB_INIT_ID=${{ secrets.APPWRITE_DB_INIT_ID }}" "APPWRITE_DB_INIT_ID=${{ secrets.APPWRITE_DB_INIT_ID }}"
"APPWRITE_COL_INIT_ID=${{ secrets.APPWRITE_COL_INIT_ID }}" "APPWRITE_COL_INIT_ID=${{ secrets.APPWRITE_COL_INIT_ID }}"
"APPWRITE_API_KEY_INIT=${{ secrets.APPWRITE_API_KEY_INIT }}" "APPWRITE_API_KEY_INIT=${{ secrets.APPWRITE_API_KEY_INIT }}"
@@ -74,7 +75,7 @@ jobs:
echo "_APP_VERSION=${{ env.TAG }}" >> .env echo "_APP_VERSION=${{ env.TAG }}" >> .env
echo "_APP_DOMAIN=${{ secrets.STG_APP_DOMAIN }}" >> .env echo "_APP_DOMAIN=${{ secrets.STG_APP_DOMAIN }}" >> .env
echo "_APP_SYSTEM_SECURITY_EMAIL_ADDRESS=${{ secrets.APP_SYSTEM_SECURITY_EMAIL_ADDRESS }}" >> .env echo "_APP_SYSTEM_SECURITY_EMAIL_ADDRESS=${{ secrets.APP_SYSTEM_SECURITY_EMAIL_ADDRESS }}" >> .env
echo "SEMATEXT_TOKEN=${{ secrets.SEMATEXT_TOKEN }}" >> .env echo "_APP_BETTER_STACK_INCIDENT_URL=${{ secrets.BETTER_STACK_INCIDENT_URL }}" >> .env
echo ${{ secrets.GH_REGISTRY_TOKEN }} | docker login ghcr.io --username ${{ env.REGISTRY_USERNAME }} --password-stdin echo ${{ secrets.GH_REGISTRY_TOKEN }} | docker login ghcr.io --username ${{ env.REGISTRY_USERNAME }} --password-stdin
docker-compose -f ${{ env.STACK_FILE }} config docker-compose -f ${{ env.STACK_FILE }} config

View File

@@ -7,6 +7,79 @@ on:
permissions: read-all permissions: read-all
jobs: jobs:
format:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha }}
- uses: actions/setup-node@v4
with:
node-version: 20
- name: Install corepack
run: npm i -g corepack@latest
- name: Install pnpm
run: corepack enable
- name: Get pnpm store directory
shell: bash
run: |
echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
- uses: actions/cache@v4
name: Setup pnpm cache
with:
path: ${{ env.STORE_PATH }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Check formatting
run: pnpm format:check
tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha }}
- uses: actions/setup-node@v4
with:
node-version: 20
- name: Install corepack
run: npm i -g corepack@latest
- name: Install pnpm
run: corepack enable
- name: Get pnpm store directory
shell: bash
run: |
echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
- uses: actions/cache@v4
name: Setup pnpm cache
with:
path: ${{ env.STORE_PATH }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Install playwright dependencies
run: pnpm exec playwright install --with-deps chromium
- name: Run tests
env:
PUBLIC_APPWRITE_ENDPOINT: ${{ vars.PUBLIC_APPWRITE_ENDPOINT }}
PUBLIC_APPWRITE_DASHBOARD: ${{ vars.PUBLIC_APPWRITE_DASHBOARD }}
PUBLIC_APPWRITE_PROJECT_ID: ${{ vars.PUBLIC_APPWRITE_PROJECT_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_MESSAGES_ID: ${{ vars.PUBLIC_APPWRITE_COL_MESSAGES_ID }}
PUBLIC_APPWRITE_FN_TLDR_ID: ${{ vars.PUBLIC_APPWRITE_FN_TLDR_ID }}
PUBLIC_APPWRITE_PROJECT_INIT_ID: ${{ vars.PUBLIC_APPWRITE_PROJECT_INIT_ID }}
PUBLIC_GROWTH_ENDPOINT: ${{ vars.PUBLIC_GROWTH_ENDPOINT }}
PUBLIC_POSTHOG_API_KEY: ${{ vars.PUBLIC_POSTHOG_API_KEY }}
APPWRITE_DB_INIT_ID: ${{ secrets.APPWRITE_DB_INIT_ID }}
APPWRITE_COL_INIT_ID: ${{ secrets.APPWRITE_COL_INIT_ID }}
APPWRITE_API_KEY_INIT: ${{ secrets.APPWRITE_API_KEY_INIT }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: pnpm test
build: build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
@@ -14,9 +87,11 @@ jobs:
with: with:
ref: ${{ github.event.pull_request.head.sha }} ref: ${{ github.event.pull_request.head.sha }}
- name: Use Node.js - name: Use Node.js
uses: actions/setup-node@v3 uses: actions/setup-node@v4
with: with:
node-version: 20 node-version: 20
- name: Install corepack
run: npm i -g corepack@latest
- name: Install pnpm - name: Install pnpm
run: corepack enable run: corepack enable
- name: Get pnpm store directory - name: Get pnpm store directory
@@ -24,7 +99,7 @@ jobs:
run: | run: |
echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
- uses: actions/cache@v3 - uses: actions/cache@v4
name: Setup pnpm cache name: Setup pnpm cache
with: with:
path: ${{ env.STORE_PATH }} path: ${{ env.STORE_PATH }}
@@ -35,16 +110,17 @@ jobs:
run: pnpm install --frozen-lockfile run: pnpm install --frozen-lockfile
- name: Build Website - name: Build Website
env: env:
NODE_OPTIONS: '--max_old_space_size=8192' NODE_OPTIONS: '--max_old_space_size=16384'
PUBLIC_APPWRITE_ENDPOINT: ${{ secrets.PUBLIC_APPWRITE_ENDPOINT }} PUBLIC_APPWRITE_ENDPOINT: ${{ vars.PUBLIC_APPWRITE_ENDPOINT }}
PUBLIC_APPWRITE_DASHBOARD: ${{ secrets.PUBLIC_APPWRITE_DASHBOARD }} PUBLIC_APPWRITE_DASHBOARD: ${{ vars.PUBLIC_APPWRITE_DASHBOARD }}
PUBLIC_APPWRITE_PROJECT_ID: ${{ secrets.PUBLIC_APPWRITE_PROJECT_ID }} PUBLIC_APPWRITE_PROJECT_ID: ${{ vars.PUBLIC_APPWRITE_PROJECT_ID }}
PUBLIC_APPWRITE_DB_MAIN_ID: ${{ secrets.PUBLIC_APPWRITE_DB_MAIN_ID }} PUBLIC_APPWRITE_DB_MAIN_ID: ${{ vars.PUBLIC_APPWRITE_DB_MAIN_ID }}
PUBLIC_APPWRITE_COL_THREADS_ID: ${{ secrets.PUBLIC_APPWRITE_COL_THREADS_ID }} PUBLIC_APPWRITE_COL_THREADS_ID: ${{ vars.PUBLIC_APPWRITE_COL_THREADS_ID }}
PUBLIC_APPWRITE_COL_MESSAGES_ID: ${{ secrets.PUBLIC_APPWRITE_COL_MESSAGES_ID }} PUBLIC_APPWRITE_COL_MESSAGES_ID: ${{ vars.PUBLIC_APPWRITE_COL_MESSAGES_ID }}
PUBLIC_APPWRITE_FN_TLDR_ID: ${{ secrets.PUBLIC_APPWRITE_FN_TLDR_ID }} PUBLIC_APPWRITE_FN_TLDR_ID: ${{ vars.PUBLIC_APPWRITE_FN_TLDR_ID }}
PUBLIC_APPWRITE_PROJECT_INIT_ID: ${{ secrets.PUBLIC_APPWRITE_PROJECT_INIT_ID }} PUBLIC_APPWRITE_PROJECT_INIT_ID: ${{ vars.PUBLIC_APPWRITE_PROJECT_INIT_ID }}
PUBLIC_GROWTH_ENDPOINT: ${{ secrets.PUBLIC_GROWTH_ENDPOINT }} PUBLIC_GROWTH_ENDPOINT: ${{ vars.PUBLIC_GROWTH_ENDPOINT }}
PUBLIC_POSTHOG_API_KEY: ${{ vars.PUBLIC_POSTHOG_API_KEY }}
APPWRITE_DB_INIT_ID: ${{ secrets.APPWRITE_DB_INIT_ID }} APPWRITE_DB_INIT_ID: ${{ secrets.APPWRITE_DB_INIT_ID }}
APPWRITE_COL_INIT_ID: ${{ secrets.APPWRITE_COL_INIT_ID }} APPWRITE_COL_INIT_ID: ${{ secrets.APPWRITE_COL_INIT_ID }}
APPWRITE_API_KEY_INIT: ${{ secrets.APPWRITE_API_KEY_INIT }} APPWRITE_API_KEY_INIT: ${{ secrets.APPWRITE_API_KEY_INIT }}

5
.gitignore vendored
View File

@@ -3,6 +3,7 @@ node_modules
/build /build
/.svelte-kit /.svelte-kit
/.idea /.idea
/.zed
/package /package
.env .env
.env.* .env.*
@@ -18,4 +19,6 @@ package-lock.json
.history .history
terraform/**/.t* terraform/**/.t*
terraform/**/.env terraform/**/.env
terraform/**/**/*.tfstate* terraform/**/**/*.tfstate*
/.cache
/test-results

View File

@@ -1,9 +0,0 @@
{
"useTabs": false,
"tabWidth": 4,
"singleQuote": true,
"trailingComma": "none",
"printWidth": 100,
"plugins": ["prettier-plugin-svelte", "prettier-plugin-tailwindcss"],
"overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }]
}

View File

@@ -49,9 +49,9 @@ Create ordered (numbered) and unordered (bulleted) lists using 1., \*, or -.
**Unordered List**: **Unordered List**:
```md ```md
- Apple - Apple
- Banana - Banana
- Cherry - Cherry
``` ```
#### Links #### Links
@@ -130,18 +130,18 @@ Alternatively, use markdoc tables.
```md ```md
{% table %} {% table %}
- Heading 1 - Heading 1
- Heading 2 - Heading 2
--- ---
- Row 1 Cell 1 - Row 1 Cell 1
- Row 1 Cell 2 - Row 1 Cell 2
--- ---
- Row 2 Cell 1 - Row 2 Cell 1
- Row 2 cell 2 - Row 2 cell 2
{% /table %} {% /table %}
``` ```
@@ -296,7 +296,7 @@ Configure FCM for push notification to Android and Apple devices.
#### Accordions #### Accordions
Use accordions to reduce page size and collapse information that's not important when a reader is skilling the page. Use accordions to reduce page size and collapse information that's not important when a reader is scrolling the page.
``` ```
{% accordion %} {% accordion %}

View File

@@ -20,7 +20,7 @@ To contribute to the Appwrite website, you need to fork, clone, and run the webs
Start by [forking the repository](https://github.com/appwrite/website/fork), which makes a copy of the repo on your GitHub profile. This allows you to make code changes when you don't have permissions in the main Appwrite website repo. Start by [forking the repository](https://github.com/appwrite/website/fork), which makes a copy of the repo on your GitHub profile. This allows you to make code changes when you don't have permissions in the main Appwrite website repo.
Then, [clone the respository](https://docs.github.com/en/repositories/creating-and-managing-repositories/cloning-a-repository#cloning-a-repository). Then, [clone the repository](https://docs.github.com/en/repositories/creating-and-managing-repositories/cloning-a-repository#cloning-a-repository).
Alternatively, you can develop the website repo in your browser using [Code Spaces](https://github.com/features/codespaces) or [GitPod](https://www.gitpod.io/#https://github.com/appwrite/website). Alternatively, you can develop the website repo in your browser using [Code Spaces](https://github.com/features/codespaces) or [GitPod](https://www.gitpod.io/#https://github.com/appwrite/website).
@@ -34,13 +34,15 @@ Once you've cloned the Appwrite website repo, running the following command to i
pnpm i pnpm i
``` ```
**Locate the `.env.example` file in the root directory and rename it to `.env`.** This file contains the required environment variables for the project to function properly.
Then, run the following command to start a development server. Then, run the following command to start a development server.
```sh ```sh
pnpm run dev pnpm run dev
``` ```
Before commiting your code changes, make sure the website repo builds by running: Before committing your code changes, make sure the website repo builds by running:
```sh ```sh
pnpm run build pnpm run build
@@ -60,11 +62,11 @@ doc-548-submit-a-pull-request-section-to-contribution-guide
When `TYPE` can be: When `TYPE` can be:
- **feat** - is a new feature - **feat** - is a new feature
- **doc** - documentation only changes - **doc** - documentation only changes
- **cicd** - changes related to CI/CD system - **cicd** - changes related to CI/CD system
- **fix** - a bug fix - **fix** - a bug fix
- **refactor** - code change that neither fixes a bug nor adds a feature - **refactor** - code change that neither fixes a bug nor adds a feature
**All PRs must include a commit message with a description of the changes made!** **All PRs must include a commit message with a description of the changes made!**

View File

@@ -27,6 +27,9 @@ ENV PUBLIC_APPWRITE_PROJECT_INIT_ID ${PUBLIC_APPWRITE_PROJECT_INIT_ID}
ARG PUBLIC_GROWTH_ENDPOINT ARG PUBLIC_GROWTH_ENDPOINT
ENV PUBLIC_GROWTH_ENDPOINT ${PUBLIC_GROWTH_ENDPOINT} ENV PUBLIC_GROWTH_ENDPOINT ${PUBLIC_GROWTH_ENDPOINT}
ARG PUBLIC_POSTHOG_API_KEY
ENV PUBLIC_POSTHOG_API_KEY ${PUBLIC_POSTHOG_API_KEY}
ARG APPWRITE_DB_INIT_ID ARG APPWRITE_DB_INIT_ID
ENV APPWRITE_DB_INIT_ID ${APPWRITE_DB_INIT_ID} ENV APPWRITE_DB_INIT_ID ${APPWRITE_DB_INIT_ID}
@@ -42,19 +45,26 @@ ENV GITHUB_TOKEN ${GITHUB_TOKEN}
ARG SENTRY_AUTH_TOKEN ARG SENTRY_AUTH_TOKEN
ENV SENTRY_AUTH_TOKEN ${SENTRY_AUTH_TOKEN} ENV SENTRY_AUTH_TOKEN ${SENTRY_AUTH_TOKEN}
ARG SENTRY_RELEASE
ENV SENTRY_RELEASE ${SENTRY_RELEASE}
ARG PUBLIC_POSTHOG_API_KEY
ENV PUBLIC_POSTHOG_API_KEY ${PUBLIC_POSTHOG_API_KEY}
ENV PNPM_HOME="/pnpm" ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH" ENV PATH="$PNPM_HOME:$PATH"
WORKDIR /app WORKDIR /app
COPY package.json package.json COPY package.json package.json
COPY pnpm-lock.yaml pnpm-lock.yaml COPY pnpm-lock.yaml pnpm-lock.yaml
RUN npm i -g corepack@latest
RUN corepack enable RUN corepack enable
FROM base as build FROM base as build
COPY . . COPY . .
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile
RUN NODE_OPTIONS=--max_old_space_size=8192 pnpm run build RUN NODE_OPTIONS=--max_old_space_size=16384 pnpm run build
FROM base as final FROM base as final
@@ -69,4 +79,4 @@ COPY --from=build /app/build/ build
COPY --from=build /app/server/ server COPY --from=build /app/server/ server
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile --prod RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile --prod
CMD [ "node", "server/main.js"] CMD [ "node", "server/main.js"]

View File

@@ -10,8 +10,8 @@ The Appwrite Website repo features the main Appwrite website, including our [hom
The Appwrite Website has been built with the following frameworks: The Appwrite Website has been built with the following frameworks:
- [Svelte](https://svelte.dev/) - [Svelte](https://svelte.dev/)
- [SvelteKit](https://kit.svelte.dev/) - [SvelteKit](https://kit.svelte.dev/)
## Development ## Development

234
STYLE.md
View File

@@ -6,17 +6,17 @@ Read this document carefully before making PRs to the Appwrite Website repo.
The Appwrite documentation is meant to provide general guidance that's: The Appwrite documentation is meant to provide general guidance that's:
- Unopinionated - Unopinionated
- Focused on the correct use of Appwrite product - Focused on the correct use of Appwrite product
- Includes examples for all relevant and applicable SDKs - Includes examples for all relevant and applicable SDKs
- Agnostic to the user's implementation and stack. - Agnostic to the user's implementation and stack.
Examples of things not fit for docs, and better as a blog or video: Examples of things not fit for docs, and better as a blog or video:
- General programming advice - General programming advice
- Opinionated implementation patterns like MVVM, factory methods, etc. - Opinionated implementation patterns like MVVM, factory methods, etc.
- Examples that only include a select subset of Appwrite SDKs. - Examples that only include a select subset of Appwrite SDKs.
- Examples that do not work for all developers using Appwrite, but specific to Appwrite + technology. - Examples that do not work for all developers using Appwrite, but specific to Appwrite + technology.
Note that the tutorials and blogs available on the Appwrite blog and docs are meant for these types of information. Note that the tutorials and blogs available on the Appwrite blog and docs are meant for these types of information.
@@ -28,39 +28,39 @@ Appwrite's navigation increases in complexity from top down. We expect users to
Introduction Section: Introduction Section:
- [Homes](https://appwrite.io/docs) - [Homes](https://appwrite.io/docs)
- [Quick start](https://appwrite.io/docs/quick-start) - [Quick start](https://appwrite.io/docs/quick-start)
- [Tutorial](https://appwrite.io/docs/tutorial) - [Tutorial](https://appwrite.io/docs/tutorial)
- [SDKs](https://appwrite.io/docs/sdks) - [SDKs](https://appwrite.io/docs/sdks)
- [API references](https://appwrite.io/docs/references) - [API references](https://appwrite.io/docs/references)
Products section: Products section:
- [Auth](https://appwrite.io/docs/products/auth) - [Auth](https://appwrite.io/docs/products/auth)
- [Databases](https://appwrite.io/docs/products/databases) - [Databases](https://appwrite.io/docs/products/databases)
- [Functions](https://appwrite.io/docs/products/functions) - [Functions](https://appwrite.io/docs/products/functions)
- [Storage](https://appwrite.io/docs/products/storage) - [Storage](https://appwrite.io/docs/products/storage)
- [Messaging](https://appwrite.io/docs/products/messaging) - [Messaging](https://appwrite.io/docs/products/messaging)
- [AI](https://appwrite.io/docs/products/ai) - [AI](https://appwrite.io/docs/products/ai)
APIs section: APIs section:
- [GraphQL](https://appwrite.io/docs/apis/graphql) - [GraphQL](https://appwrite.io/docs/apis/graphql)
- [REST](https://appwrite.io/docs/apis/rest) - [REST](https://appwrite.io/docs/apis/rest)
- [Realtime](https://appwrite.io/docs/apis/realtime) - [Realtime](https://appwrite.io/docs/apis/realtime)
Tooling section: Tooling section:
- [CLI](https://appwrite.io/docs/command-line) - [CLI](https://appwrite.io/docs/command-line)
- [Command center](https://appwrite.io/docs/tooling/command-center) - [Command center](https://appwrite.io/docs/tooling/command-center)
- [Assistant](https://appwrite.io/docs/tooling/assistant) - [Assistant](https://appwrite.io/docs/tooling/assistant)
Advanced section: Advanced section:
- [Platform](https://appwrite.io/docs/advanced/platform) - [Platform](https://appwrite.io/docs/advanced/platform)
- [Migrations](https://appwrite.io/docs/advanced/migrations) - [Migrations](https://appwrite.io/docs/advanced/migrations)
- [Self-hosting](https://appwrite.io/docs/advanced/self-hosting) - [Self-hosting](https://appwrite.io/docs/advanced/self-hosting)
- [Security](https://appwrite.io/docs/advanced/security) - [Security](https://appwrite.io/docs/advanced/security)
Here's the intended purpose and structure of each section. Here's the intended purpose and structure of each section.
@@ -70,10 +70,10 @@ This section is focused on introducing what Appwrite is and giving examples to t
Documentation here is focused on a **single flow** which means a single platform/framework + Appwrite. Documentation here is focused on a **single flow** which means a single platform/framework + Appwrite.
Content here is not specific to a specific product, but usually covers multiple Appwrite products. Content here is not specific to a specific product, but usually covers multiple Appwrite products.
- If your tutorial can be followed in about 15 minutes and fits on one page, write it under quick start - If your tutorial can be followed in about 15 minutes and fits on one page, write it under quick start
- If you're writing a long piece of documentation that integrates Appwrite with another technology, with lots of details that's opinionated or isn't relevant for all use cases, write it under tutorial. This is similar to "cook book" at other organizations. - If you're writing a long piece of documentation that integrates Appwrite with another technology, with lots of details that's opinionated or isn't relevant for all use cases, write it under tutorial. This is similar to "cook book" at other organizations.
- If you have information like helpers and methods that are only on SDKs but not the API, they go under SDK - If you have information like helpers and methods that are only on SDKs but not the API, they go under SDK
- API references are generated from source from the appwrite/appwrite repo - API references are generated from source from the appwrite/appwrite repo
### Products ### Products
@@ -82,17 +82,17 @@ Code examples should cover **all available SDKs**.
Each product page has three main sections Each product page has three main sections
- Introduction - Introduction
- Overview - Describes at a high level, why you might need this product - Overview - Describes at a high level, why you might need this product
- Quick start - Shows the most basic and quickest example to make something happen with a product. Keep it really short. - Quick start - Shows the most basic and quickest example to make something happen with a product. Keep it really short.
- Concept - Concept
- These pages usually align with sections shown in the product in the Appwrite Console. - These pages usually align with sections shown in the product in the Appwrite Console.
- Focused on describing concepts a user should know, but not actions you might take. - Focused on describing concepts a user should know, but not actions you might take.
- Cover all the details - Cover all the details
- Journeys - Journeys
- These pages focus on common actions and work flows - These pages focus on common actions and work flows
- Detailed examples that span many concepts - Detailed examples that span many concepts
- Like cookbook at other organizations' documentation. - Like cookbook at other organizations' documentation.
### APIs section ### APIs section
@@ -106,10 +106,10 @@ Describes tools that help you work with Appwrite, but are usually non-essential
For information that's not used commonly during the development cycle. For information that's not used commonly during the development cycle.
- Platform: covers concepts that apply to the entire Appwrite Cloud platform, like API keys, rate limits, etc. - Platform: covers concepts that apply to the entire Appwrite Cloud platform, like API keys, rate limits, etc.
- Migrations: covers migrations feature of Appwrite that helps you move data around. - Migrations: covers migrations feature of Appwrite that helps you move data around.
- Security: purely information about measures Appwrite use to ensure security of the platform and data. - Security: purely information about measures Appwrite use to ensure security of the platform and data.
- Self-hosting: The Appwrite self-hosted platform is meant to behave identically to Cloud after being configured corrrectly. This section focuses on how to configure Appwrite self-hosted such that it behaves like Cloud. - Self-hosting: The Appwrite self-hosted platform is meant to behave identically to Cloud after being configured corrrectly. This section focuses on how to configure Appwrite self-hosted such that it behaves like Cloud.
## Documentation sources ## Documentation sources
@@ -117,22 +117,22 @@ The Appwrite docs are compiled from different repositories. Here are the signifi
[appwrite/website](https://github.com/appwrite/website): [appwrite/website](https://github.com/appwrite/website):
- Tutorials - Tutorials
- Quick starts - Quick starts
- Product, API, Tooling and Advanced sections - Product, API, Tooling and Advanced sections
[appwrite/appwrite](https://github.com/appwrite/appwrite): [appwrite/appwrite](https://github.com/appwrite/appwrite):
- [API Reference](https://appwrite.io/docs/references) pages - [API Reference](https://appwrite.io/docs/references) pages
- API specification - API specification
- API description - API description
- API endpoint description - API endpoint description
- API request parameters - API request parameters
- API response model - API response model
[appwrite/sdk-generator](https://github.com/appwrite/sdk-generator): [appwrite/sdk-generator](https://github.com/appwrite/sdk-generator):
- Generated examples - Generated examples
## Markdown Style guidelines ## Markdown Style guidelines
@@ -141,10 +141,10 @@ the tone and voice remains consistent.
### Headings ### Headings
- All titles, headings, buttons, and labels should be written in **sentence case**. If you're not sure what sentence case should look like, check [APA's style guide](https://apastyle.apa.org/style-grammar-guidelines/capitalization/sentence-case) or check with ChatGPT and other LLMs which reliably converts titles to sentence case. - All titles, headings, buttons, and labels should be written in **sentence case**. If you're not sure what sentence case should look like, check [APA's style guide](https://apastyle.apa.org/style-grammar-guidelines/capitalization/sentence-case) or check with ChatGPT and other LLMs which reliably converts titles to sentence case.
- All headings in a docs page begin with `# Heading` then `## Heading` and `### Heading`. Internally, they're converted to H2 to H4 tags. - All headings in a docs page begin with `# Heading` then `## Heading` and `### Heading`. Internally, they're converted to H2 to H4 tags.
- All headings should have an ID label, for example `# Cool heading {% #cool-heading %}` the `#cool-heading` ID will be used to generate the table of contents and add links to the heading. - All headings should have an ID label, for example `# Cool heading {% #cool-heading %}` the `#cool-heading` ID will be used to generate the table of contents and add links to the heading.
- Prefer verbs over gerunds, for example, say "Create documents" not "Creating documents". - Prefer verbs over gerunds, for example, say "Create documents" not "Creating documents".
### Extended Markdoc components ### Extended Markdoc components
@@ -152,14 +152,14 @@ Appwrite's documentation uses extended markdown syntax. You can find all of the
### Screenshots ### Screenshots
- When contributing upload original screenshots. The Appwrite design team will edit the screenshot to be consistent with other screenshots in the docs. - When contributing upload original screenshots. The Appwrite design team will edit the screenshot to be consistent with other screenshots in the docs.
- Screenshots must be 16:9 - Screenshots must be 16:9
- Screnshots should be taken in a 1400 x 900 view port on 3x DPR in browser developer tools. - Screnshots should be taken in a 1400 x 900 view port on 3x DPR in browser developer tools.
- Use generic and sensible organization, project, and resource names. Avoid names like `test`, `demo`, or `sdlkfj`. - Use generic and sensible organization, project, and resource names. Avoid names like `test`, `demo`, or `sdlkfj`.
- All screenshot should be take from a user named Walter O'Brien. You can change the name of your current user by going to your Appwrite Console and clicking the **top right profile icon** > **Your Account** > **Name**. - All screenshot should be take from a user named Walter O'Brien. You can change the name of your current user by going to your Appwrite Console and clicking the **top right profile icon** > **Your Account** > **Name**.
- Screenshots are stored in the `/images/docs/` folder, in a parent folder that is consistent with the path of the docs that reference the image. - Screenshots are stored in the `/images/docs/` folder, in a parent folder that is consistent with the path of the docs that reference the image.
- All screenshots must be both dark and light mode, with `/path/` holding the lightmode version and `/path/dark/` holding the dark mode version. - All screenshots must be both dark and light mode, with `/path/` holding the lightmode version and `/path/dark/` holding the dark mode version.
- Screenshots should be uploaded as un-edited original. Request help from the Appwrite design team to help you edit and refine your photos according to our guidelines. - Screenshots should be uploaded as un-edited original. Request help from the Appwrite design team to help you edit and refine your photos according to our guidelines.
```md ```md
{% only_dark %} {% only_dark %}
@@ -210,52 +210,59 @@ Split content such that each piece makes sense without reading dependents or exp
### Release prep ### Release prep
- [ ] Add new version to [src/lib/utils/references.ts](src/lib/utils/references.ts) - [ ] Add new version to [src/lib/utils/references.ts](src/lib/utils/references.ts)
- [ ] Point Cloud to new version in [src/routes/docs/references/[version]/[platform]/[service]/+page.server.ts](src/routes/docs/references/[version]/[platform]/[service]/+page.server.ts) - [ ] Point Cloud to new version in [src/routes/docs/references/[version]/[platform]/[service]/+page.server.ts](src/routes/docs/references/[version]/[platform]/[service]/+page.server.ts)
- [ ] Update install command in [/workspaces/website/src/routes/docs/advanced/self-hosting/+page.markdoc](/workspaces/website/src/routes/docs/advanced/self-hosting/+page.markdoc) - [ ] Update install command in [/workspaces/website/src/routes/docs/advanced/self-hosting/+page.markdoc](/workspaces/website/src/routes/docs/advanced/self-hosting/+page.markdoc)
- [ ] Update events [src/partials/[product]-events.md](src/partials/) - [ ] Update events [src/partials/[product]-events.md](src/partials/)
- [ ] Update response code [src/routes/docs/advanced/platform/response-codes/+page.markdoc](src/routes/docs/advanced/platform/response-codes/+page.markdoc) - [ ] Update response code [src/routes/docs/advanced/platform/response-codes/+page.markdoc](src/routes/docs/advanced/platform/response-codes/+page.markdoc)
- [ ] Bump latest SDK versions in SDKs page, quick start, and tutorials - [ ] Bump latest SDK versions in SDKs page, quick start, and tutorials
- [ ] Create new sections for new products - [ ] Create new sections for new products
- [ ] Create new concept and journey pages for new features - [ ] Create new concept and journey pages for new features
- [ ] Update docs for breaking changes - [ ] Update docs for breaking changes
### Documenting a new API ### Documenting a new API
- Add a new .md file describing the new API here: <https://github.com/appwrite/appwrite/tree/main/docs/references> - Add a new .md file describing the new API here: <https://github.com/appwrite/appwrite/tree/main/docs/references>
- Add descriptions for methods and parameters in the controller code: <https://github.com/appwrite/appwrite/tree/main/app/controllers/api> - Add descriptions for methods and parameters in the controller code: <https://github.com/appwrite/appwrite/tree/main/app/controllers/api>
- Check new response models have meaningful descriptions - Check new response models have meaningful descriptions
### Adding a new quickstart ### Adding a new quickstart
- Copy a quick start from the [src/routes/docs/quick-starts](src/routes/docs/quick-starts) folder. - Copy a quick start from the [src/routes/docs/quick-starts](src/routes/docs/quick-starts) folder.
- Add a new entry and logo to [src/routes/docs/quick-starts/+page.svelte](src/routes/docs/quick-starts/+page.svelte) - Add a new entry and logo to [src/routes/docs/quick-starts/+page.svelte](src/routes/docs/quick-starts/+page.svelte)
- If you need a new logo, contact the Appwrite team to add one to Pink design. - If you need a new logo, contact the Appwrite team to add one to Pink design.
- Update the content of your tutorial. Remember to update the front matter! - Update the content of your tutorial. Remember to update the front matter!
- Try to be consistent in both the quickstart's content and format when compared to existing quick starts - Try to be consistent in both the quickstart's content and format when compared to existing quick starts
- Add the quick start to the footer and front page of Appwrite - Add the quick start to the footer and front page of Appwrite
- Use sections for steps on your page - Use sections for steps on your page
### Adding a new tutorial ### Adding a new tutorial
- Copy a tutorial from the [src/routes/docs/tutorials](src/routes/docs/tutorials) folder. - Copy a tutorial from the [src/routes/docs/tutorials](src/routes/docs/tutorials) folder.
- Update the `+page.ts`'s redirect, for example, the Android tutorial has this: [src/routes/docs/tutorials/android/+page.ts](src/routes/docs/tutorials/android/+page.ts) - Update the `+page.ts`'s redirect, for example, the Android tutorial has this: [src/routes/docs/tutorials/android/+page.ts](src/routes/docs/tutorials/android/+page.ts)
- Update [src/routes/docs/tutorials/+page.svelte](src/routes/docs/tutorials/+page.svelte) and add your new tutorial - Update [src/routes/docs/tutorials/+page.svelte](src/routes/docs/tutorials/+page.svelte) and add your new tutorial
- Update [src/routes/docs/tutorials/android/+layout.ts](src/routes/docs/tutorials/android/+layout.ts) and add your new tutorial - Update [src/routes/docs/tutorials/android/+layout.ts](src/routes/docs/tutorials/android/+layout.ts) and add your new tutorial
- Add the content of your tutorial. Keep pages short, separated by a different distinct feature for each step. - Add the content of your tutorial. Keep pages short, separated by a different distinct feature for each step.
- If you need a new logo, contact the Appwrite team to add one to Pink design. - If you need a new logo, contact the Appwrite team to add one to Pink design.
- Add the tutorial to the footer and front page of Appwrite - Add the tutorial to the footer and front page of Appwrite
## Language and diction ## Language and diction
### Headings ### Headings
Prefer simple nouns and root form verbs. Navigation labels should be short (ideally, one word) and not have verbs or a directive to keep labels concise. Also make sure not to repeat a term if already under certain context
✅ Create screen (root verb, noun) - ✅ User verification
✅ Authentication (noun) - ✅ Release (under a Policies section)
❌ Authenticating (present participle verb) - ❌ Verify user
❌ Create a new screen (too wordy) - ❌ Release policies (under a Policies section)
For content headings, prefer simple nouns and root form verbs.
- ✅ Create screen (root verb, noun)
- ✅ Authentication (noun)
- ❌ Authenticating (present participle verb)
- ❌ Create a new screen (too wordy)
Try your best to stick to simple headings, if it's not possible, don't worry and write a full heading if need be. Try your best to stick to simple headings, if it's not possible, don't worry and write a full heading if need be.
@@ -264,19 +271,19 @@ Try your best to stick to simple headings, if it's not possible, don't worry and
Avoid unclear [links](https://www.youtube.com/watch?v=dQw4w9WgXcQ) such as learn more [here](https://www.youtube.com/watch?v=dQw4w9WgXcQ). Avoid unclear [links](https://www.youtube.com/watch?v=dQw4w9WgXcQ) such as learn more [here](https://www.youtube.com/watch?v=dQw4w9WgXcQ).
Readers will be unsure where a link may take them. Those using a screen reader will find it especially difficult. Readers will be unsure where a link may take them. Those using a screen reader will find it especially difficult.
✅ [Learn more about authentication](https://appwrite.io/docs/products/auth/email-password#login) - ✅ [Learn more about authentication](https://appwrite.io/docs/products/auth/email-password#login)
❌ Learn more about authentication [here](https://www.youtube.com/watch?v=dQw4w9WgXcQ) - ❌ Learn more about authentication [here](https://www.youtube.com/watch?v=dQw4w9WgXcQ)
### Sentences ### Sentences
Use a directive that's straight to the point when providing an action a developer must perform. Use a directive that's straight to the point when providing an action a developer must perform.
The action and verb always comes first, the explanation after. The action and verb always comes first, the explanation after.
✅ Create a new database. - ✅ Create a new database.
✅ Update a document so its permissions include your new users. - ✅ Update a document so its permissions include your new users.
❌ To allow access, update your permissions. - ❌ To allow access, update your permissions.
❌ You can create a new database for each tenant. - ❌ You can create a new database for each tenant.
❌ Creating a new bucket lets you set different permissions for images uploaded by users. - ❌ Creating a new bucket lets you set different permissions for images uploaded by users.
The action always comes first and is in the beginning of the sentence, which makes important steps easier to follow. The action always comes first and is in the beginning of the sentence, which makes important steps easier to follow.
@@ -288,11 +295,11 @@ to skip and scan a document.
Like sentences, important information always comes first. Like sentences, important information always comes first.
This makes it easier to scan through the page. This makes it easier to scan through the page.
✅ Clear, important information such as actions come first - ✅ Clear, important information such as actions come first
> Store secrets as environment variables in vaults by navigating to **settings** > **security** > **vault**. Your secrets should never be shared. You must ensure data privacy, sharing secrets can compromise security during development. > Store secrets as environment variables in vaults by navigating to **settings** > **security** > **vault**. Your secrets should never be shared. You must ensure data privacy, sharing secrets can compromise security during development.
❌ Unclear, important information is in the middle of the paragraph - ❌ Unclear, important information is in the middle of the paragraph
> Security is important in development. That's why you should take care to protect secrets. Secrets should be safely stored as a environment variable in a vault. You can find vaults under **settings** > **security** > **vault**. Don't share this with anyone! > Security is important in development. That's why you should take care to protect secrets. Secrets should be safely stored as a environment variable in a vault. You can find vaults under **settings** > **security** > **vault**. Don't share this with anyone!
@@ -301,6 +308,13 @@ Even if your paragraph is just one or two sentences, shorter paragraphs are easi
### Diction ### Diction
Avoid using possession as it is less welcoming.
- ✅ read the documentation
- ✅ the API
- ❌ read on our documentation
- ❌ our API
If you're unsure about which word to use to describe a concept, you shuold look for precedence in the following order. If you're unsure about which word to use to describe a concept, you shuold look for precedence in the following order.
1. Appwrite docs 1. Appwrite docs

View File

@@ -35,6 +35,7 @@ services:
- PUBLIC_APPWRITE_COL_THREADS_ID=$PUBLIC_APPWRITE_COL_THREADS_ID - PUBLIC_APPWRITE_COL_THREADS_ID=$PUBLIC_APPWRITE_COL_THREADS_ID
- PUBLIC_APPWRITE_COL_MESSAGES_ID=$PUBLIC_APPWRITE_COL_MESSAGES_ID - PUBLIC_APPWRITE_COL_MESSAGES_ID=$PUBLIC_APPWRITE_COL_MESSAGES_ID
- PUBLIC_APPWRITE_FN_TLDR_ID=$PUBLIC_APPWRITE_FN_TLDR_ID - PUBLIC_APPWRITE_FN_TLDR_ID=$PUBLIC_APPWRITE_FN_TLDR_ID
- PUBLIC_POSTHOG_API_KEY=$PUBLIC_POSTHOG_API_KEY
restart: always restart: always
networks: networks:
- homepage - homepage

View File

@@ -80,6 +80,7 @@ services:
- PUBLIC_APPWRITE_COL_THREADS_ID - PUBLIC_APPWRITE_COL_THREADS_ID
- PUBLIC_APPWRITE_COL_MESSAGES_ID - PUBLIC_APPWRITE_COL_MESSAGES_ID
- PUBLIC_APPWRITE_FN_TLDR_ID - PUBLIC_APPWRITE_FN_TLDR_ID
- PUBLIC_POSTHOG_API_KEY
deploy: deploy:
<<: *x-update-config <<: *x-update-config
mode: replicated mode: replicated
@@ -119,23 +120,23 @@ services:
- TIME_BETWEEN_RUNS=3600 - TIME_BETWEEN_RUNS=3600
- UNUSED_TIME=6h - UNUSED_TIME=6h
sematext-agent: resource-monitor:
image: sematext/agent:latest image: ghcr.io/appwrite/monitoring:0.1.0
environment: entrypoint: monitoring
REGION: EU command:
INFRA_TOKEN: $SEMATEXT_TOKEN - '--url=${_APP_BETTER_STACK_INCIDENT_URL}'
deploy: - '--interval=60'
mode: global - '--cpu-limit=85'
restart_policy: - '--memory-limit=80'
condition: any - '--disk-limit=85'
hostname: '{{.Node.Hostname}}'
<<: *x-logging
volumes: volumes:
- /:/hostfs:ro - /mnt:/mnt:ro
- /etc/passwd:/etc/passwd:ro deploy:
- /etc/group:/etc/group:ro <<: *x-update-config
- /sys:/host/sys:ro endpoint_mode: dnsrr
- /dev:/hostfs/dev:ro mode: global
- /var/run:/var/run
- /sys/kernel/debug:/sys/kernel/debug
networks: networks:
cloud: cloud:

View File

@@ -74,6 +74,7 @@ services:
- PUBLIC_APPWRITE_COL_THREADS_ID - PUBLIC_APPWRITE_COL_THREADS_ID
- PUBLIC_APPWRITE_COL_MESSAGES_ID - PUBLIC_APPWRITE_COL_MESSAGES_ID
- PUBLIC_APPWRITE_FN_TLDR_ID - PUBLIC_APPWRITE_FN_TLDR_ID
- PUBLIC_POSTHOG_API_KEY
deploy: deploy:
<<: *x-update-config <<: *x-update-config
mode: replicated mode: replicated
@@ -114,24 +115,6 @@ services:
volumes: volumes:
- /var/run/docker.sock:/var/run/docker.sock - /var/run/docker.sock:/var/run/docker.sock
sematext-agent:
image: sematext/agent:latest
environment:
REGION: EU
INFRA_TOKEN: $SEMATEXT_TOKEN
deploy:
mode: global
restart_policy:
condition: any
volumes:
- /:/hostfs:ro
- /etc/passwd:/etc/passwd:ro
- /etc/group:/etc/group:ro
- /sys:/host/sys:ro
- /dev:/hostfs/dev:ro
- /var/run:/var/run
- /sys/kernel/debug:/sys/kernel/debug
networks: networks:
cloud: cloud:
driver: overlay driver: overlay

51
eslint.config.js Normal file
View File

@@ -0,0 +1,51 @@
import { includeIgnoreFile } from '@eslint/compat';
import js from '@eslint/js';
import prettier from 'eslint-config-prettier';
import svelte from 'eslint-plugin-svelte';
import globals from 'globals';
import { fileURLToPath } from 'node:url';
import ts from 'typescript-eslint';
import svelteConfig from './svelte.config.js';
const gitignorePath = fileURLToPath(new URL('./.gitignore', import.meta.url));
export default ts.config(
includeIgnoreFile(gitignorePath),
js.configs.recommended,
...ts.configs.recommended,
...svelte.configs.recommended,
prettier,
...svelte.configs.prettier,
{
languageOptions: {
globals: { ...globals.browser, ...globals.node }
},
rules: {
// TODO: remove them one by one
'@typescript-eslint/no-unused-vars': 'off',
'@typescript-eslint/no-duplicate-enum-values': 'off',
'@typescript-eslint/no-empty-object-type': 'off',
'@typescript-eslint/no-unused-expressions': 'off',
'svelte/infinite-reactive-loop': 'off',
'svelte/require-each-key': 'off',
'svelte/no-immutable-reactive-statements': 'off',
'svelte/no-at-html-tags': 'off',
'svelte/no-useless-mustaches': 'off',
'svelte/no-reactive-reassign': 'off',
'svelte/no-reactive-literals': 'off'
}
},
{
files: ['**/*.svelte', '**/*.svelte.ts', '**/*.svelte.js'],
ignores: ['eslint.config.js', 'svelte.config.js'],
languageOptions: {
parserOptions: {
// Only uncomment this if you want it to take 3 minutes https://github.com/sveltejs/eslint-plugin-svelte/issues/1084
// projectService: true,
extraFileExtensions: ['.svelte'],
parser: ts.parser,
svelteConfig
}
}
}
);

View File

@@ -7,6 +7,10 @@
"type": "esm", "type": "esm",
"property": "default", "property": "default",
"watch": true "watch": true
},
"partials": {
"auth-security.md": "./src/partials/auth-security.md",
"prohibited-activities.md": "./src/partials/prohibited-activities.md"
} }
} }
] ]

View File

@@ -11,56 +11,63 @@
"dev": "vite dev", "dev": "vite dev",
"download-contributors": "node ./scripts/download-contributor-data.js", "download-contributors": "node ./scripts/download-contributor-data.js",
"format": "prettier --write .", "format": "prettier --write .",
"format:check": "prettier --check .",
"generate:icons": "pnpx svgo -rf src/icons/svg -o src/icons/optimized",
"icons:build": "node ./src/icons/build.js", "icons:build": "node ./src/icons/build.js",
"icons:generate": "node ./src/icons/optimize.js && node ./src/icons/build.js", "icons:generate": "node ./src/icons/optimize.js && node ./src/icons/build.js",
"icons:optimize": "node ./src/icons/optimize.js", "icons:optimize": "node ./src/icons/optimize.js",
"lint": "prettier --check . && eslint .", "lint": "prettier --check . && eslint .",
"preview": "vite preview", "preview": "vite preview",
"test": "npm run test:integration && npm run test:unit", "test": "npm run test:integration",
"test:integration": "playwright test", "test:integration": "playwright test",
"test:unit": "vitest", "test:unit": "vitest",
"optimize": "node ./scripts/optimize-assets.js" "optimize": "node ./scripts/optimize-assets.js",
"optimize:all": "node ./scripts/optimize-all.js"
}, },
"packageManager": "pnpm@9.4.0+sha512.f549b8a52c9d2b8536762f99c0722205efc5af913e77835dbccc3b0b0b2ca9e7dc8022b78062c17291c48e88749c70ce88eb5a74f1fa8c4bf5e18bb46c8bd83a", "packageManager": "pnpm@10.6.2",
"dependencies": { "dependencies": {
"@sentry/sveltekit": "^8.12.0", "@number-flow/svelte": "^0.3.3",
"h3": "^1.12.0", "h3": "^1.14.0",
"sharp": "^0.33.4" "melt": "^0.28.2",
"posthog-js": "^1.210.2",
"sharp": "^0.33.5"
}, },
"devDependencies": { "devDependencies": {
"@appwrite.io/console": "^0.6.2", "@appwrite.io/console": "^0.6.4",
"@appwrite.io/pink": "~0.26.0", "@appwrite.io/pink": "~0.26.0",
"@appwrite.io/pink-icons": "~0.26.0", "@appwrite.io/pink-icons": "~0.26.0",
"@appwrite.io/repo": "github:appwrite/appwrite#1.6.x", "@appwrite.io/repo": "github:appwrite/appwrite#1.6.x",
"@eslint/compat": "^1.2.7",
"@eslint/js": "^9.21.0",
"@fingerprintjs/fingerprintjs": "^4.5.1",
"@internationalized/date": "3.5.0", "@internationalized/date": "3.5.0",
"@melt-ui/pp": "^0.3.2", "@melt-ui/pp": "^0.3.2",
"@melt-ui/svelte": "^0.74.4", "@melt-ui/svelte": "^0.86.5",
"@playwright/test": "^1.44.1", "@playwright/test": "^1.50.0",
"@sveltejs/adapter-node": "^4.0.1", "@sveltejs/adapter-node": "^5.2.12",
"@sveltejs/enhanced-img": "^0.1.9", "@sveltejs/enhanced-img": "^0.4.4",
"@sveltejs/kit": "^2.5.17", "@sveltejs/kit": "^2.20.2",
"@sveltejs/vite-plugin-svelte": "^3.1.1", "@sveltejs/vite-plugin-svelte": "^5.0.3",
"@tailwindcss/postcss": "4.0.0-alpha.17", "@tailwindcss/postcss": "^4.1.2",
"@types/compression": "^1.7.5", "@types/compression": "^1.7.5",
"@types/glob": "^8.1.0", "@types/glob": "^8.1.0",
"@types/markdown-it": "^13.0.8", "@types/markdown-it": "^13.0.9",
"@types/morgan": "^1.9.9", "@types/morgan": "^1.9.9",
"@typescript-eslint/eslint-plugin": "^7.13.1", "analytics": "^0.8.16",
"@typescript-eslint/parser": "^7.13.1",
"analytics": "^0.8.14",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"cobe": "^0.6.3", "cva": "npm:class-variance-authority@^0.7.1",
"cva": "npm:class-variance-authority@^0.7.0",
"date-fns": "^3.6.0", "date-fns": "^3.6.0",
"dequal": "^2.0.3", "dequal": "^2.0.3",
"embla-carousel": "^8.1.5", "embla-carousel": "^8.5.2",
"embla-carousel-svelte": "^8.1.5", "embla-carousel-auto-scroll": "^8.5.2",
"embla-carousel-svelte": "^8.5.2",
"embla-carousel-wheel-gestures": "^8.0.1", "embla-carousel-wheel-gestures": "^8.0.1",
"eslint": "^8.57.0", "eslint": "^9.19.0",
"eslint-config-prettier": "^8.10.0", "eslint-config-prettier": "^9.1.0",
"eslint-plugin-svelte": "^2.40.0", "eslint-plugin-svelte": "^2.46.1",
"fuse.js": "^7.0.0", "fuse.js": "^7.0.0",
"highlight.js": "^11.9.0", "globals": "^15.14.0",
"highlight.js": "^11.11.1",
"markdown-it": "^14.1.0", "markdown-it": "^14.1.0",
"meilisearch": "^0.37.0", "meilisearch": "^0.37.0",
"motion": "^10.18.0", "motion": "^10.18.0",
@@ -68,24 +75,39 @@
"openapi-types": "^12.1.3", "openapi-types": "^12.1.3",
"oslllo-svg-fixer": "^3.0.0", "oslllo-svg-fixer": "^3.0.0",
"plausible-tracker": "^0.3.9", "plausible-tracker": "^0.3.9",
"postcss": "^8.4.39", "postcss": "^8.5.1",
"prettier": "^3.3.3", "posthog-js": "^1.204.0",
"prettier-plugin-svelte": "^3.2.5", "posthog-node": "^4.4.1",
"prettier-plugin-tailwindcss": "^0.6.6", "prettier": "^3.4.2",
"remeda": "^2.10.0", "prettier-plugin-svelte": "^3.3.3",
"sass": "^1.77.6", "prettier-plugin-tailwindcss": "^0.6.11",
"svelte": "^4.2.18", "remeda": "^2.20.0",
"svelte-check": "^3.8.1", "reodotdev": "^1.0.0",
"svelte-markdoc-preprocess": "^2.0.0", "sass": "^1.83.4",
"svelte": "^5.25.6",
"svelte-check": "^4.0.0",
"svelte-markdoc-preprocess": "3.0.0",
"svelte-markdown": "^0.4.1", "svelte-markdown": "^0.4.1",
"svgtofont": "^4.2.1", "svgtofont": "^4.2.3",
"tailwind-merge": "^2.5.2", "tailwind-merge": "^3.0.2",
"tailwindcss": "4.0.0-alpha.17", "tailwindcss": "^4.1.2",
"tslib": "^2.6.3", "tslib": "^2.8.1",
"typescript": "^5.5.2", "typescript": "^5.8.2",
"vite": "^5.3.1", "typescript-eslint": "^8.21.0",
"vite-plugin-dynamic-import": "^1.5.0", "vite": "^6.2.4",
"vite-plugin-dynamic-import": "^1.6.0",
"vite-plugin-image-optimizer": "^1.1.8", "vite-plugin-image-optimizer": "^1.1.8",
"vitest": "^1.6.0" "vite-plugin-manifest-sri": "^0.2.0",
"vitest": "^3.1.1"
},
"pnpm": {
"onlyBuiltDependencies": [
"@parcel/watcher",
"core-js",
"esbuild",
"sharp",
"svelte-preprocess",
"ttf2woff2"
]
} }
} }

View File

@@ -2,9 +2,10 @@ import type { PlaywrightTestConfig } from '@playwright/test';
const config: PlaywrightTestConfig = { const config: PlaywrightTestConfig = {
webServer: { webServer: {
command: 'npm run build && npm run preview', command: 'pnpm run dev',
port: 4173 port: 5173
}, },
fullyParallel: true,
testDir: 'tests', testDir: 'tests',
testMatch: /(.+\.)?(test|spec)\.[jt]s/ testMatch: /(.+\.)?(test|spec)\.[jt]s/
}; };

5156
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

15
prettier.config.js Normal file
View File

@@ -0,0 +1,15 @@
/** @type {import("prettier").Config} */
export default {
useTabs: false,
tabWidth: 4,
singleQuote: true,
trailingComma: 'none',
printWidth: 100,
plugins: ['prettier-plugin-svelte', 'prettier-plugin-tailwindcss'],
overrides: [
{
files: '*.svelte',
options: { parser: 'svelte' }
}
]
};

153
scripts/optimize-all.js Normal file
View File

@@ -0,0 +1,153 @@
import { readdirSync, statSync } from 'fs';
import { join, relative, resolve } from 'path';
import sharp from 'sharp';
import { fileURLToPath } from 'url';
const __dirname = fileURLToPath(new URL('.', import.meta.url));
const project_root = resolve(__dirname, '..');
// Directories to search in
const search_dirs = ['src', 'static', 'routes', 'lib'];
// Directories to skip
const excluded_dirs = ['node_modules', '.svelte-kit', 'build', '.git', 'assets/'];
/**
* @type {{
* jpeg: sharp.JpegOptions,
* webp: sharp.WebpOptions,
* png: sharp.PngOptions,
* gif: sharp.GifOptions,
* avif: sharp.AvifOptions
* }}
*/
const config = {
jpeg: {
quality: 100
},
webp: {
lossless: true
},
png: {
quality: 100
},
gif: {
quality: 100
},
avif: {
lossless: true
}
};
/** @type {sharp.ResizeOptions} */
const resize_config = {
width: 1280,
height: 1280,
fit: sharp.fit.inside,
withoutEnlargement: true
};
function* walk_directory(dir) {
try {
const files = readdirSync(dir);
for (const file of files) {
const pathToFile = join(dir, file);
const relativePath = relative(project_root, pathToFile);
// Skip excluded directories
if (excluded_dirs.some((excluded) => relativePath.includes(excluded))) {
continue;
}
const isDirectory = statSync(pathToFile).isDirectory();
if (isDirectory) {
yield* walk_directory(pathToFile);
} else {
yield pathToFile;
}
}
} catch (error) {
console.error(`Error accessing directory ${dir}:`, error.message);
}
}
function is_image(file) {
const extension = file.split('.').pop()?.toLowerCase();
return extension && Object.keys(config).includes(extension);
}
function format_size(bytes) {
const sizes = ['B', 'KB', 'MB'];
const i = Math.floor(Math.log(bytes) / Math.log(1024));
return `${(bytes / Math.pow(1024, i)).toFixed(2)} ${sizes[i]}`;
}
async function optimize_image(file) {
const is_animated = file.endsWith('.gif');
const image = sharp(file, { animated: is_animated });
try {
const size_before = (await image.toBuffer()).length;
const meta = await image.metadata();
if (!meta.format || !config[meta.format]) {
console.warn(`Unsupported format for file: ${file}`);
return;
}
const buffer = await image[meta.format](config[meta.format])
.resize(resize_config)
.toBuffer();
const size_after = buffer.length;
if (size_after >= size_before) {
console.log(`Skipping ${relative(project_root, file)} - no size reduction possible`);
return;
}
const savings = (((size_before - size_after) / size_before) * 100).toFixed(2);
console.log(`Optimizing ${relative(project_root, file)}`);
console.log(` Before: ${format_size(size_before)}`);
console.log(` After: ${format_size(size_after)}`);
console.log(` Saved: ${savings}%`);
await sharp(buffer).toFile(file);
} catch (error) {
console.error(`Error processing ${file}:`, error.message);
}
}
async function main() {
let total_files = 0;
let processed_files = 0;
console.log('Starting image optimization...\n');
for (const search_dir of search_dirs) {
const full_path = join(project_root, search_dir);
try {
if (!statSync(full_path).isDirectory()) continue;
for (const file of walk_directory(full_path)) {
if (!is_image(file)) continue;
total_files++;
await optimize_image(file);
processed_files++;
}
} catch (error) {
if (error.code === 'ENOENT') {
console.log(`Directory ${search_dir} not found - skipping`);
} else {
console.error(`Error processing ${search_dir}:`, error.message);
}
}
}
console.log(`\nOptimization complete!`);
console.log(`Processed ${processed_files} of ${total_files} image files`);
}
await main();

View File

@@ -1,12 +1,13 @@
import { createApp, fromNodeMiddleware, toNodeListener } from 'h3'; import { sitemaps } from './sitemap.js';
import { createServer } from 'node:http'; import { createServer } from 'node:http';
import { handler } from '../build/handler.js'; import { handler } from '../build/handler.js';
import { sitemap } from './sitemap.js'; import { createApp, fromNodeMiddleware, toNodeListener } from 'h3';
async function main() { async function main() {
const port = process.env.PORT || 3000; const port = process.env.PORT || 3000;
const app = createApp(); const app = createApp();
app.use('/sitemap.xml', await sitemap()); app.use(['/sitemap.xml', '/sitemaps'], await sitemaps());
app.use(fromNodeMiddleware(handler)); app.use(fromNodeMiddleware(handler));
const server = createServer(toNodeListener(app)).listen(port); const server = createServer(toNodeListener(app)).listen(port);
server.addListener('listening', () => { server.addListener('listening', () => {

View File

@@ -1,43 +1,154 @@
import { fileURLToPath } from 'node:url';
import { createRequire } from 'node:module'; import { createRequire } from 'node:module';
import { defineEventHandler, setResponseHeader } from 'h3'; import { dirname, join } from 'node:path';
import { readFile, stat } from 'node:fs/promises';
import { mkdirSync, writeFileSync } from 'node:fs';
import {
defineEventHandler,
getRequestURL,
sendRedirect,
serveStatic,
setResponseHeader
} from 'h3';
/** const MAX_THREADS_PER_FILE = 1000;
* @returns {Promise<import('h3').EventHandler>} const BASE_URL = 'https://appwrite.io';
*/ const BASE_DIR = dirname(fileURLToPath(import.meta.url));
export async function sitemap() {
const SITEMAP_DIR = join(BASE_DIR, './sitemaps');
const THREADS_DIR = join(SITEMAP_DIR, 'threads');
const NAMED_GROUPS = {
blog: '/blog',
docs: '/docs',
integrations: '/integrations'
};
export async function sitemaps() {
console.info('Preparing Sitemap...'); console.info('Preparing Sitemap...');
const manifest = await import('../build/server/manifest.js'); const { manifest } = await import('../build/server/manifest.js');
const prerendered = manifest.prerendered; const threads = collectThreads().map((id) => `/threads/${id}`);
const file_route_extensions = ['.json', '.xml']; const otherRoutes = manifest._.routes
const routes = [...prerendered, ...collectThreads()].filter( .filter((r) => r.params.length === 0)
(route) => !file_route_extensions.some((ext) => route.endsWith(ext)) .map((r) => r.id)
); .filter(
console.info(`Sitemap loaded with ${routes.length} routes!`); (id) => !id.startsWith('/threads/') && !id.endsWith('.json') && !id.endsWith('.xml')
);
const sitemap = `<?xml version="1.0" encoding="UTF-8"?> mkdirSync(SITEMAP_DIR, { recursive: true });
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"> mkdirSync(THREADS_DIR, { recursive: true });
${routes
let totalCount = 0;
const sitemapIndexOrder = [];
const grouped = {},
fallback = [];
for (const route of otherRoutes) {
const match = Object.entries(NAMED_GROUPS).find(([, prefix]) => route.startsWith(prefix));
if (match) {
const [group] = match;
grouped[group] ??= [];
grouped[group].push(route);
} else fallback.push(route);
}
totalCount += writeSitemap('pages.xml', fallback, SITEMAP_DIR);
sitemapIndexOrder.push('pages.xml');
for (const group of ['docs', 'blog', 'integrations']) {
if (grouped[group]?.length) {
const filename = `${group}.xml`;
totalCount += writeSitemap(filename, grouped[group], SITEMAP_DIR);
sitemapIndexOrder.push(filename);
}
}
const threadChunks = chunkArray(threads, MAX_THREADS_PER_FILE);
threadChunks.forEach((chunk, i) => {
const filename = `${i + 1}.xml`;
totalCount += writeSitemap(filename, chunk, THREADS_DIR);
sitemapIndexOrder.push(`threads/${filename}`);
});
const sitemapIndex = `
<?xml version="1.0" encoding="UTF-8"?>
<sitemapindex xmlns="https://www.sitemaps.org/schemas/sitemap/0.9">
${sitemapIndexOrder
.map( .map(
(route) => `<url> (name) => `
<loc>https://appwrite.io${route}</loc> <sitemap>
</url> <loc>${BASE_URL}/sitemaps/${name}</loc>
` </sitemap>`
) )
.join('')} .join('\n')}
</urlset>`; </sitemapindex>`.trim();
return defineEventHandler((event) => { console.info(`✅ Sitemap generation complete — ${totalCount} URLs in total.\n`);
setResponseHeader(event, 'Content-Type', 'application/xml');
return sitemap; return defineEventHandler(async (event) => {
const url = getRequestURL(event);
if (url.pathname === '/sitemap.xml') {
setResponseHeader(event, 'Content-Type', 'application/xml');
return sitemapIndex;
}
if (url.pathname === '/sitemaps') {
return sendRedirect(event, '/sitemap.xml', 307);
}
if (url.pathname === '/sitemaps/threads') {
return sendRedirect(event, '/sitemaps/threads/1.xml', 307);
}
const dir = import.meta.resolve('./sitemaps');
return serveStatic(event, {
fallthrough: true,
indexNames: undefined,
getContents: (id) => readFile(new URL(dir + id)),
getMeta: async (id) => {
const stats = await stat(new URL(dir + id)).catch(() => null);
if (!stats?.isFile()) return;
return {
size: stats.size,
mtime: stats.mtimeMs
};
}
});
}); });
} }
/** function writeSitemap(filename, routes, dir) {
* @returns {string[]} const body = `
*/ <?xml version="1.0" encoding="UTF-8" ?>
function collectThreads() { <urlset
const threads = createRequire(import.meta.url)('../build/prerendered/threads/data.json'); xmlns="https://www.sitemaps.org/schemas/sitemap/0.9"
xmlns:xhtml="https://www.w3.org/1999/xhtml"
xmlns:mobile="https://www.google.com/schemas/sitemap-mobile/1.0"
xmlns:news="https://www.google.com/schemas/sitemap-news/0.9"
xmlns:image="https://www.google.com/schemas/sitemap-image/1.1"
xmlns:video="https://www.google.com/schemas/sitemap-video/1.1"
>
${routes.map((route) => ` <url>\n <loc>${BASE_URL}${route}</loc>\n </url>`).join('\n')}
</urlset>`.trim();
return threads.map((id) => `/threads/${id}`); const filepath = join(dir, filename);
writeFileSync(filepath, body);
const label = filepath.replace(BASE_DIR + '/sitemaps', '');
console.info(` └── Generated ${label} with ${routes.length} URLs`);
return routes.length;
}
function chunkArray(arr, size) {
const chunks = [];
for (let i = 0; i < arr.length; i += size) {
chunks.push(arr.slice(i, i + size));
}
return chunks;
}
function collectThreads() {
return createRequire(import.meta.url)('../build/prerendered/threads/data.json');
} }

View File

@@ -1,13 +1,21 @@
@import 'tailwindcss'; @import 'tailwindcss';
@import './styles/typography.css';
@variant dark (&:is(.dark *));
@theme { @theme {
/* Colors */ /* Colors */
--color-*: initial; --color-*: initial;
/* base */ /* base */
--color-primary: hsl(var(--color-primary)); --color-black: #000;
--color-secondary: hsl(var(--color-secondary)); --color-white: #fff;
--color-transparent: transparent;
/* theme */
--color-primary: var(--color-primary);
--color-secondary: var(--color-secondary);
--color-accent: var(--color-secondary); --color-accent: var(--color-secondary);
--color-smooth: var(--color-smooth);
/* pink */ /* pink */
--color-pink-200: hsl(var(--color-pink-hue) 98% 84%); --color-pink-200: hsl(var(--color-pink-hue) 98% 84%);
@@ -16,9 +24,9 @@
--color-pink-700: hsl(var(--color-pink-hue) 65% 36%); --color-pink-700: hsl(var(--color-pink-hue) 65% 36%);
/* red */ /* red */
--color-red-200: calc(hsl(var(--color-red-hue) - 2) 100% 92%); --color-red-200: hsl(calc(var(--color-red-hue) - 2) 100% 92%);
--color-red-500: hsl(var(--color-red-hue) 100% 61%); --color-red-500: hsl(var(--color-red-hue) 100% 61%);
--color-red-700: calc(hsl(var(--color-red-hue) - 3) 82% 39%); --color-red-700: hsl(calc(var(--color-red-hue) - 3) 82% 39%);
/* orange */ /* orange */
--color-orange-200: hsl(var(--color-orange-hue) 100% 88%); --color-orange-200: hsl(var(--color-orange-hue) 100% 88%);
@@ -27,33 +35,33 @@
/* mint */ /* mint */
--color-mint-200: hsl(var(--color-mint-hue) 56% 88%); --color-mint-200: hsl(var(--color-mint-hue) 56% 88%);
--color-mint-500: calc(hsl(var(--color-mint-hue) + 1) 54% 69%); --color-mint-500: hsl(calc(var(--color-mint-hue) + 1), 54%, 69%);
--color-mint-700: calc(hsl(var(--color-mint-hue) + 2) 24% 41%); --color-mint-700: hsl(calc(var(--color-mint-hue) + 2), 24%, 41%);
/* purple */ /* purple */
--color-purple-200: hsl(var(--color-purple-hue) 100% 88%); --color-purple-200: hsl(var(--color-purple-hue) 100% 88%);
--color-purple-500: calc(hsl(var(--color-purple-hue) - 1) 99% 70%); --color-purple-500: hsl(calc(var(--color-purple-hue) - 1), 99%, 70%);
--color-purple-700: calc(hsl(var(--color-purple-hue) - 1) 42% 42%); --color-purple-700: hsl(calc(var(--color-purple-hue) - 1), 42%, 42%);
/* yellow */ /* yellow */
--color-yellow-200: hsl(var(--color-yellow-hue) 100% 88%); --color-yellow-200: hsl(var(--color-yellow-hue) 100% 88%);
--color-yellow-500: hsl(var(--color-yellow-hue) 99% 70%); --color-yellow-500: hsl(var(--color-yellow-hue) 99% 70%);
--color-yellow-700: calc(hsl(var(--color-yellow-hue) + 1) 42% 42%); --color-yellow-700: hsl(calc(var(--color-yellow-hue) + 1), 42%, 42%);
/* blue */ /* blue */
--color-blue-200: hsl(var(--color-blue-hue) 100% 88%); --color-blue-200: hsl(var(--color-blue-hue) 100% 88%);
--color-blue-500: calc(hsl(var(--color-blue-hue) - 1) 99% 70%); --color-blue-500: hsl(calc(var(--color-blue-hue) - 1), 99%, 70%);
--color-blue-700: calc(hsl(var(--color-blue-hue) - 1) 42% 42%); --color-blue-700: hsl(calc(var(--color-blue-hue) - 1), 42%, 42%);
/* green */
--color-green-700: #0a714f;
/* secondary */ /* secondary */
--color-secondary-100: hsl(var(--color-secondary-hue) 99% 66%); --color-secondary-100: hsl(var(--color-secondary-hue) 99% 66%);
--color-accent-200: hsl(var(--color-secondary-hue), 78%, 60%, 0.32); --color-accent-200: hsl(var(--color-secondary-hue), 78%, 60%, 0.32);
/* greyscale */ /* greyscale */
--color-white: hsl(0 0% 100%); --color-offset: hsl(var(--color-greyscale-hue) 2%, 11%, 0.94);
--color-black: hsl(0 0% 0%);
--color-transparent: rgba(0, 0, 0, 0);
--color-smooth: hsl(var(--color-greyscale-hue) 6%, 10%, 0.04);
--color-greyscale-25: hsl(var(--color-greyscale-hue) 11% 98%); --color-greyscale-25: hsl(var(--color-greyscale-hue) 11% 98%);
--color-greyscale-50: hsl(var(--color-greyscale-hue) 11% 94%); --color-greyscale-50: hsl(var(--color-greyscale-hue) 11% 94%);
--color-greyscale-100: hsl(var(--color-greyscale-hue) 6% 90%); --color-greyscale-100: hsl(var(--color-greyscale-hue) 6% 90%);
@@ -69,11 +77,9 @@
--color-greyscale-850: hsl(var(--color-greyscale-hue) 3% 14%); --color-greyscale-850: hsl(var(--color-greyscale-hue) 3% 14%);
--color-greyscale-900: hsl(var(--color-greyscale-hue) 5.7% 10.4%); --color-greyscale-900: hsl(var(--color-greyscale-hue) 5.7% 10.4%);
/* utility colors */ /* Easings */
--color-badge-bg-light: #f2c8d6; --easing-bounce: linear(0, 0.063, 0.25 18.2%, 1 36.4%, 0.813, 0.75, 0.813, 1, 0.938, 1, 1);
--color-badge-border-light: #f69db7; --easing-spring: linear(0, 0.938 16.7%, 1.149 24.3%, 1.154 29.9%, 0.977 51%, 1);
--color-badge-bg-dark: #2c2c2f;
--color-badge-border-dark: #39393c;
/* Animations */ /* Animations */
--animate-scale-in: scale-in 200ms ease-out forwards; --animate-scale-in: scale-in 200ms ease-out forwards;
@@ -85,6 +91,13 @@
/* Pink polyfills */ /* Pink polyfills */
--transition: 0.2s; --transition: 0.2s;
--animate-caret-blink: caret-blink 1s ease-in-out infinite;
--animate-text:
fade 0.75s ease-in-out both, blur 0.75s ease-in-out both, up 0.75s ease-in-out both;
--animate-scroll: scroll 60s linear infinite;
--animate-fade-in: fade-in 0.5s ease-in-out both;
--animate-marquee: marquee var(--speed, 30s) linear infinite var(--direction, forwards);
--animate-lighting: lighting 1.25s ease-out forwards;
/* Keyframes */ /* Keyframes */
@keyframes scale-in { @keyframes scale-in {
@@ -96,6 +109,18 @@
} }
} }
@keyframes caret-blink {
0%,
70%,
100% {
opacity: 1;
}
20%,
50% {
opacity: 0;
}
}
@keyframes blur { @keyframes blur {
0% { 0% {
filter: blur(5px); filter: blur(5px);
@@ -123,6 +148,7 @@
} }
} }
@keyframes map { @keyframes map {
0% { 0% {
opacity: 0; opacity: 0;
@@ -130,6 +156,31 @@
} }
100% { 100% {
opacity: 1; opacity: 1;
@keyframes scroll {
0% {
transform: translateX(0);
}
100% {
transform: translateX(-100%);
}
}
@keyframes marquee {
to {
transform: translateX(-50%);
}
}
@keyframes lighting {
0% {
opacity: 0;
clip-path: inset(5%);
transform: scale(111.11%);
}
100% {
opacity: 1;
clip-path: inset(0);
transform: scale(1); transform: scale(1);
} }
} }
@@ -146,59 +197,117 @@
} }
/* Fonts */ /* Fonts */
--font-family-sans: 'Inter', arial, sans-serif; --font-sans: 'Inter', arial, sans-serif;
--font-family-mono: 'Fira Code', monospace; --font-mono: 'Fira Code', monospace;
--font-family-aeonik-fono: 'Aenoik Fono', monospace; --font-aeonik-fono: 'Aenoik Fono', monospace;
--font-family-aeonik-pro: 'Aeonik Pro', var(--font-family-sans); --font-aeonik-pro: 'Aeonik Pro', var(--font-sans);
--font-family-archia: 'Archia', arial, sans-serif; --font-archia: 'Archia', arial, sans-serif;
/* Font sizes */ /* Font sizes */
--font-size-micro: 0.75rem; --text-x-micro: 0.625rem;
--font-size-micro--line-height: 1rem; --text-x-micro--line-height: 0.875rem;
--font-size-micro--letter-spacing: var(--letter-spacing-loose); --text-x-micro--letter-spacing: var(--tracking-tighter);
--font-size-caption: 0.875rem; --text-micro: 0.75rem;
--font-size-caption--line-height: 1.375rem; --text-micro--line-height: 1rem;
--font-size-caption--letter-spacing: var(--letter-spacing-tight); --text-micro--letter-spacing: var(--tracking-tighter);
--font-size-sub-body: clamp(0.875rem, 2vw, 1rem); --text-caption: 0.875rem;
--font-size-sub-body--line-height: 1.375rem; --text-caption--line-height: 1.375rem;
--font-size-sub-body--letter-spacing: var(--letter-spacing-tight); --text-caption--letter-spacing: var(--tracking-tight);
--font-size-body: clamp(1rem, 2.5vw, 1.125rem); --text-sub-body: clamp(0.875rem, 2vw, 1rem);
--font-size-body--line-height: clamp(1.375rem, 3vw, 1.625rem); --text-sub-body--line-height: 1.375rem;
--font-size-body--letter-spacing: var(--letter-spacing-tight); --text-sub-body--letter-spacing: var(--tracking-tight);
--font-size-paragraph-md: 1rem; --text-body: clamp(1rem, 2.5vw, 1.125rem);
--font-size-paragraph-md--line-height: 1.625rem; --text-body--line-height: clamp(1.375rem, 3vw, 1.625rem);
--font-size-paragraph-md--letter-spacing: var(--letter-spacing-tight); --text-body--letter-spacing: var(--tracking-tight);
--font-size-paragraph-lg: 1.125rem; --text-paragraph-md: 1rem;
--font-size-paragraph-lg--line-height: 1.75rem; --text-paragraph-md--line-height: 1.625rem;
--font-size-paragraph-lg--letter-spacing: var(--letter-spacing-tight); --text-paragraph-md--letter-spacing: var(--tracking-tight);
--font-size-description: clamp(1.125rem, 3vw, 1.25rem); --text-paragraph-lg: 1.125rem;
--font-size-description--line-height: clamp(1.625rem, 3.5vw, 1.75rem); --text-paragraph-lg--line-height: 1.75rem;
--font-size-description--letter-spacing: var(--letter-spacing-tighter); --text-paragraph-lg--letter-spacing: var(--tracking-tight);
--font-size-label: 1.5rem; --text-description: clamp(1.125rem, 3vw, 1.25rem);
--font-size-label--line-height: 1.75rem; --text-description--line-height: clamp(1.625rem, 3.5vw, 1.75rem);
--font-size-title: clamp(2rem, 5vw, 2.5rem); --text-description--letter-spacing: var(--tracking-tighter);
--font-size-title--line-height: clamp(2.125rem, 5.5vw, 2.75rem); --text-label: 1.5rem;
--font-size-title--letter-spacing: var(--letter-spacing-squeezed); --text-label--line-height: 1.75rem;
--font-size-display: clamp(3rem, 7vw, 4rem); --text-title: clamp(2rem, 5vw, 2.5rem);
--font-size-display--line-height: clamp(3.125rem, 7.5vw, 4.25rem); --text-title--line-height: clamp(2.125rem, 5.5vw, 2.75rem);
--font-size-display--letter-spacing: var(--letter-spacing-compressed); --text-title--letter-spacing: var(--tracking-squeezed);
--font-size-headline: clamp(3.5rem, 8vw, 5.5rem); --text-display: clamp(3rem, 7vw, 4rem);
--font-size-headline--line-height: clamp(3.5rem, 8.5vw, 5.75rem); --text-display--line-height: clamp(3.125rem, 7.5vw, 4.25rem);
--font-size-headline--letter-spacing: var(--letter-spacing-compressed); --text-display--letter-spacing: var(--tracking-compressed);
--text-headline: clamp(3.5rem, 8vw, 5.5rem);
--text-headline--line-height: clamp(3.5rem, 8.5vw, 5.75rem);
--text-headline--letter-spacing: var(--tracking-compressed);
/* Letter spacing */ /* Letter spacing */
--letter-spacing-*: initial; --tracking-*: initial;
--letter-spacing-compressed: -0.022em; --tracking-compressed: -0.022em;
--letter-spacing-squeezed: -0.01em; --tracking-squeezed: -0.01em;
--letter-spacing-tighter: -0.018em; --tracking-tighter: -0.018em;
--letter-spacing-tight: -0.0045em; --tracking-tight: -0.0045em;
--letter-spacing-none: 0em; --tracking-none: 0em;
--letter-spacing-loose: 0.08em; --tracking-loose: 0.08em;
}
@utility container {
margin-inline: auto;
padding-inline: 1.25rem;
max-width: 75rem;
}
@utility border-gradient {
--border-gradient-before: linear-gradient(
180deg,
rgba(255, 255, 255, 0.16) 0%,
rgba(255, 255, 255, 0) 100%
);
--border-gradient-after: linear-gradient(
180deg,
rgba(255, 255, 255, 0.12) 0%,
rgba(255, 255, 255, 0) 125.11%
);
--border-radius: 0.5rem;
&::before,
&::after {
content: '';
position: absolute;
inset: 0;
border-radius: var(--border-radius);
border: 1px solid transparent;
mask:
linear-gradient(#fff 0 0) padding-box,
linear-gradient(#fff 0 0);
mask-composite: exclude;
pointer-events: none;
}
&::before {
background: var(--border-gradient-before) border-box;
}
&::after {
background: var(--border-gradient-after) border-box;
}
}
@utility mask {
mask-image: linear-gradient(
to var(--mask-direction, top),
transparent,
black var(--mask-height, 32px),
black calc(100% - var(--mask-height, 32px)),
black
);
} }
/* Themes */
:root, :root,
.light { .light {
/* pink polyfills */
--transition: 0.2s;
/* color hues */
--color-pink-hue: 343; --color-pink-hue: 343;
--color-secondary-hue: 351; --color-secondary-hue: 351;
--color-red-hue: 3; --color-red-hue: 3;
@@ -215,19 +324,13 @@
--color-accent: var(--color-pink-600); --color-accent: var(--color-pink-600);
--color-badge-bg: var(--color-badge-bg-light); --color-badge-bg: var(--color-badge-bg-light);
--color-badge-border: var(--color-badge-border-light); --color-badge-border: var(--color-badge-border-light);
--color-smooth: hsl(var(--color-greyscale-hue) 6%, 10%, 0.04);
} }
/* dark theme */
.dark { .dark {
--color-primary: var(--color-greyscale-100); --color-primary: var(--color-greyscale-100);
--color-secondary: var(--color-greyscale-300); --color-secondary: var(--color-greyscale-300);
--color-badge-bg: var(--color-badge-bg-dark); --color-badge-bg: var(--color-badge-bg-dark);
--color-badge-border: var(--color-badge-border-dark); --color-badge-border: var(--color-badge-border-dark);
} --color-smooth: hsl(0 0%, 100%, 0.06);
/* Container */
@layer components {
.container {
@apply mx-auto box-content max-w-[75rem] px-5;
}
} }

1
src/global.d.ts vendored Normal file
View File

@@ -0,0 +1 @@
declare module 'reodotdev';

View File

@@ -1,31 +0,0 @@
import { dev } from '$app/environment';
import { SENTRY_DSN } from '$lib/constants';
import { handleErrorWithSentry, replayIntegration } from '@sentry/sveltekit';
import * as Sentry from '@sentry/sveltekit';
Sentry.init({
enabled: !dev,
dsn: SENTRY_DSN,
allowUrls: [/appwrite\.io/],
tracesSampleRate: 1.0,
// This sets the sample rate to be 10%. You may want this to be 100% while
// in development and sample at a lower rate in production
replaysSessionSampleRate: 0,
// If the entire session is not sampled, use the below sample rate to sample
// sessions when an error occurs.
replaysOnErrorSampleRate: 1.0,
// If you don't want to use Session Replay, just remove the line below:
integrations: [
replayIntegration({
maskAllInputs: true,
maskAllText: false,
blockAllMedia: false
})
]
});
// If you have a custom error handler, pass it to `handleErrorWithSentry`
export const handleError = handleErrorWithSentry();

View File

@@ -1,39 +0,0 @@
import * as Sentry from '@sentry/sveltekit';
import type { Handle } from '@sveltejs/kit';
import redirects from './redirects.json';
import { sequence } from '@sveltejs/kit/hooks';
import { BANNER_KEY, SENTRY_DSN } from '$lib/constants';
import { dev } from '$app/environment';
Sentry.init({
enabled: !dev,
dsn: SENTRY_DSN,
tracesSampleRate: 1,
allowUrls: [/appwrite\.io/]
});
const redirectMap = new Map(redirects.map(({ link, redirect }) => [link, redirect]));
const redirecter: Handle = async ({ event, resolve }) => {
const currentPath = event.url.pathname;
if (redirectMap.has(currentPath)) {
return new Response(null, {
status: 308,
headers: {
location: redirectMap.get(currentPath) ?? ''
}
});
}
return await resolve(event);
};
const bannerRewriter: Handle = async ({ event, resolve }) => {
const response = await resolve(event, {
transformPageChunk: ({ html }) => html.replace('%aw_banner_key%', BANNER_KEY)
});
return response;
};
export const handle = sequence(Sentry.sentryHandle(), redirecter, bannerRewriter);
export const handleError = Sentry.handleErrorWithSentry();

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" viewBox="0 0 40 40" fill="none"><path d="M4.015 4.066 C 3.386 4.200,2.770 4.680,2.463 5.277 C 2.236 5.716,2.064 6.483,2.023 7.233 C 1.935 8.835,2.638 17.067,2.973 18.357 C 3.589 20.735,5.311 22.511,7.700 23.232 C 8.757 23.552,9.349 23.630,10.709 23.632 L 11.985 23.633 11.309 23.811 C 9.494 24.287,8.019 25.049,7.075 25.999 C 5.448 27.635,5.423 29.739,7.004 32.000 C 7.474 32.673,9.092 34.267,9.833 34.789 C 12.203 36.458,14.280 36.389,16.049 34.584 C 16.654 33.967,16.770 33.822,17.217 33.133 C 17.895 32.087,18.469 30.836,19.112 29.002 C 19.311 28.435,19.483 27.962,19.494 27.951 C 19.505 27.939,19.657 28.343,19.831 28.848 C 20.459 30.672,21.101 32.082,21.783 33.133 C 22.230 33.822,22.346 33.967,22.951 34.584 C 24.131 35.788,25.508 36.235,26.930 35.876 C 28.186 35.558,29.303 34.831,30.735 33.401 C 31.772 32.364,32.258 31.722,32.666 30.848 C 33.293 29.506,33.328 28.241,32.768 27.135 C 32.028 25.676,30.176 24.463,27.691 23.811 L 27.015 23.633 28.291 23.632 C 29.651 23.630,30.243 23.552,31.300 23.232 C 33.689 22.511,35.411 20.735,36.027 18.357 C 36.329 17.194,36.950 10.234,36.961 7.900 C 36.966 6.701,36.951 6.514,36.816 6.045 C 36.533 5.072,36.180 4.596,35.482 4.253 C 35.055 4.043,35.005 4.034,34.301 4.041 C 33.713 4.047,33.447 4.084,32.967 4.228 C 30.211 5.051,27.121 7.741,23.683 12.309 C 22.331 14.106,20.761 16.557,19.901 18.215 L 19.502 18.983 19.077 18.175 C 17.210 14.622,14.087 10.393,11.391 7.765 C 9.470 5.892,7.671 4.706,6.037 4.235 C 5.380 4.046,4.469 3.970,4.015 4.066 " fill="#19191C" stroke="none" fill-rule="evenodd"></path></svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="30" height="30" viewBox="0 0 30 30" fill="none"><path d="M10.375 0.031 C 9.250 0.064,7.859 0.154,7.350 0.229 C 5.392 0.514,3.783 1.277,2.553 2.502 C 1.586 3.465,1.038 4.367,0.597 5.719 C 0.088 7.282,0.000 8.647,0.000 15.000 C 0.000 21.460,0.086 22.729,0.633 24.391 C 1.424 26.792,3.137 28.515,5.556 29.343 C 7.226 29.914,8.467 30.000,15.018 30.000 C 21.544 29.999,22.704 29.918,24.425 29.345 C 26.565 28.633,28.221 27.121,29.091 25.086 C 29.908 23.175,30.035 21.556,29.984 13.750 C 29.953 8.945,29.910 7.999,29.668 6.800 C 29.482 5.878,29.077 4.749,28.694 4.086 C 27.595 2.185,25.765 0.894,23.444 0.383 C 22.093 0.085,21.209 0.045,15.675 0.029 C 13.063 0.022,10.678 0.023,10.375 0.031 M21.147 2.797 C 22.819 2.913,23.916 3.209,24.775 3.776 C 25.544 4.284,26.291 5.189,26.632 6.026 C 27.069 7.096,27.176 7.907,27.255 10.713 C 27.321 13.071,27.286 19.608,27.200 21.022 C 27.078 23.016,26.694 24.234,25.881 25.205 C 24.921 26.349,23.631 26.958,21.761 27.147 C 20.680 27.257,17.315 27.312,13.575 27.283 C 8.791 27.245,7.856 27.189,6.778 26.875 C 5.764 26.580,5.080 26.193,4.446 25.556 C 3.505 24.610,3.027 23.461,2.854 21.725 C 2.698 20.170,2.663 11.634,2.801 9.100 C 2.888 7.513,3.055 6.701,3.477 5.807 C 3.757 5.217,4.041 4.820,4.507 4.370 C 5.314 3.590,6.285 3.142,7.650 2.919 C 8.064 2.852,9.417 2.763,10.525 2.731 C 11.806 2.694,20.435 2.747,21.147 2.797 M22.514 5.270 C 21.719 5.529,21.239 6.180,21.239 7.000 C 21.239 7.839,21.771 8.518,22.593 8.728 C 23.215 8.886,23.866 8.689,24.316 8.206 C 24.683 7.811,24.775 7.570,24.775 7.000 C 24.775 6.590,24.757 6.486,24.641 6.240 C 24.357 5.633,23.775 5.245,23.107 5.217 C 22.875 5.207,22.642 5.228,22.514 5.270 M13.941 7.373 C 12.249 7.616,10.798 8.346,9.572 9.572 C 8.915 10.229,8.502 10.798,8.102 11.598 C 7.065 13.671,7.038 16.178,8.030 18.250 C 8.471 19.170,8.803 19.650,9.497 20.368 C 10.613 21.524,12.030 22.283,13.625 22.582 C 14.320 22.712,15.680 22.712,16.375 22.582 C 19.542 21.989,21.989 19.542,22.582 16.375 C 22.712 15.680,22.712 14.320,22.582 13.625 C 22.283 12.030,21.524 10.613,20.368 9.497 C 19.215 8.383,17.889 7.698,16.351 7.419 C 15.751 7.311,14.537 7.287,13.941 7.373 M16.175 10.144 C 17.970 10.592,19.337 11.932,19.836 13.732 C 19.958 14.171,19.973 14.310,19.973 15.000 C 19.973 15.851,19.896 16.256,19.596 16.978 C 19.142 18.068,18.068 19.142,16.978 19.596 C 16.256 19.896,15.851 19.973,15.000 19.973 C 14.310 19.973,14.171 19.958,13.732 19.836 C 10.960 19.068,9.387 16.230,10.235 13.526 C 10.801 11.721,12.266 10.434,14.167 10.071 C 14.656 9.977,15.653 10.014,16.175 10.144 " fill="#19191C" stroke="none" fill-rule="evenodd"></path></svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

@@ -0,0 +1 @@
<svg width="156" height="174" viewBox="0 0 156 174" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M85.450 1.455 C 81.141 2.234,76.461 4.304,72.822 7.040 C 71.652 7.920,56.551 22.842,35.705 43.717 C 5.175 74.290,0.524 79.047,0.282 79.950 C -0.169 81.632,-0.054 82.990,0.663 84.428 C 2.141 87.394,5.753 88.659,8.631 87.218 C 9.103 86.981,25.343 70.962,44.720 51.620 C 71.709 24.680,80.316 16.236,81.512 15.526 C 82.372 15.016,84.010 14.273,85.152 13.876 C 87.017 13.227,87.590 13.152,90.740 13.141 C 94.566 13.129,96.050 13.437,98.846 14.823 C 102.071 16.422,105.112 19.380,106.815 22.576 C 109.478 27.571,109.369 34.670,106.551 39.774 C 105.561 41.568,103.245 43.951,78.625 68.510 C 63.859 83.239,51.597 95.658,51.376 96.108 C 49.799 99.320,50.831 102.870,53.796 104.435 C 55.168 105.159,57.489 105.251,58.963 104.639 C 59.643 104.357,67.967 96.221,86.970 77.269 C 107.275 57.017,114.383 50.082,115.507 49.427 C 121.192 46.113,128.442 46.115,134.091 49.432 C 136.073 50.596,139.508 54.083,140.732 56.173 C 142.241 58.751,142.787 60.761,142.936 64.293 C 143.131 68.931,142.427 71.626,140.068 75.270 C 139.379 76.334,128.857 87.003,106.991 108.810 L 74.924 140.790 73.854 143.035 C 71.870 147.199,71.877 151.367,73.873 155.480 C 74.582 156.940,75.725 158.209,82.262 164.795 C 86.424 168.989,90.165 172.597,90.574 172.812 C 91.627 173.366,93.786 173.532,95.160 173.164 C 97.440 172.555,99.329 169.878,99.314 167.280 C 99.302 165.187,98.587 164.263,91.377 157.029 C 85.048 150.678,84.500 150.056,84.500 149.232 C 84.500 148.390,86.387 146.448,116.181 116.613 C 133.605 99.166,148.384 84.188,149.022 83.330 C 155.704 74.345,156.964 63.298,152.485 52.957 C 149.873 46.927,144.189 41.025,138.190 38.115 C 133.355 35.769,130.143 35.034,124.667 35.019 L 120.764 35.009 120.765 31.220 C 120.766 22.678,117.870 15.632,111.940 9.750 C 107.451 5.297,102.090 2.487,96.087 1.440 C 93.271 0.949,88.212 0.956,85.450 1.455 M88.400 25.498 C 87.493 25.906,81.620 31.626,62.308 50.907 C 37.311 75.865,34.383 78.887,32.634 81.522 C 30.557 84.651,28.818 88.943,28.052 92.831 C 27.565 95.301,27.643 101.341,28.193 103.870 C 29.577 110.226,32.363 115.302,36.833 119.616 C 47.455 129.867,63.723 130.867,75.660 122.002 C 76.819 121.142,88.184 109.995,103.668 94.532 C 124.060 74.168,129.839 68.256,130.247 67.340 C 131.563 64.387,130.220 60.827,127.259 59.422 C 126.130 58.886,123.449 58.902,122.239 59.451 C 121.628 59.729,111.809 69.333,94.640 86.446 C 70.071 110.937,67.847 113.084,66.158 113.952 C 63.380 115.379,60.769 116.005,57.634 115.996 C 51.934 115.979,47.144 113.709,43.646 109.368 C 40.910 105.971,39.778 102.684,39.784 98.155 C 39.791 93.042,41.016 89.812,44.429 85.904 C 45.541 84.631,57.567 72.534,71.154 59.020 C 88.076 42.189,96.001 34.134,96.315 33.446 C 96.897 32.167,96.912 29.733,96.344 28.536 C 94.911 25.516,91.369 24.162,88.400 25.498 " stroke="none" fill-rule="evenodd" fill="black"></path></svg>

After

Width:  |  Height:  |  Size: 2.8 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" viewBox="0 0 40 40" fill="none"><path d="M20.933 15.427 C 20.933 27.068,20.957 26.455,20.471 27.433 C 19.554 29.280,17.606 30.221,15.707 29.735 C 14.100 29.324,12.853 27.987,12.526 26.324 C 12.414 25.752,12.486 24.537,12.662 24.033 C 13.214 22.458,14.427 21.383,15.983 21.089 L 16.600 20.972 16.600 18.419 L 16.600 15.867 16.417 15.868 C 16.316 15.869,15.918 15.912,15.533 15.964 C 11.480 16.512,8.319 19.669,7.623 23.866 C 7.487 24.688,7.487 26.179,7.623 27.001 C 8.204 30.507,10.559 33.375,13.768 34.487 C 14.884 34.873,15.425 34.961,16.700 34.964 C 17.921 34.967,18.480 34.885,19.500 34.555 C 21.214 34.001,22.910 32.774,24.036 31.274 C 24.872 30.160,25.559 28.557,25.839 27.067 C 25.947 26.497,25.961 25.860,25.984 20.721 L 26.009 15.009 26.569 15.374 C 28.134 16.394,30.132 17.074,31.878 17.180 L 32.535 17.220 32.517 14.660 L 32.500 12.100 32.033 12.053 C 30.985 11.946,29.712 11.463,28.843 10.843 C 27.985 10.230,27.175 9.279,26.692 8.317 C 26.217 7.372,26.024 6.668,25.896 5.417 L 25.854 5.000 23.393 5.000 L 20.933 5.000 20.933 15.427 " fill="#19191C" stroke="none" fill-rule="evenodd"></path></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -5,48 +5,52 @@ $web-icon-arrow-ext-link: "\ea04";
$web-icon-arrow-left: "\ea05"; $web-icon-arrow-left: "\ea05";
$web-icon-arrow-right: "\ea06"; $web-icon-arrow-right: "\ea06";
$web-icon-arrow-up: "\ea07"; $web-icon-arrow-up: "\ea07";
$web-icon-calendar: "\ea08"; $web-icon-bluesky: "\ea08";
$web-icon-check: "\ea09"; $web-icon-calendar: "\ea09";
$web-icon-chevron-down: "\ea0a"; $web-icon-check: "\ea0a";
$web-icon-chevron-left: "\ea0b"; $web-icon-chevron-down: "\ea0b";
$web-icon-chevron-right: "\ea0c"; $web-icon-chevron-left: "\ea0c";
$web-icon-chevron-up: "\ea0d"; $web-icon-chevron-right: "\ea0d";
$web-icon-close: "\ea0e"; $web-icon-chevron-up: "\ea0e";
$web-icon-command: "\ea0f"; $web-icon-close: "\ea0f";
$web-icon-copy: "\ea10"; $web-icon-command: "\ea10";
$web-icon-daily-dev: "\ea11"; $web-icon-copy: "\ea11";
$web-icon-dark: "\ea12"; $web-icon-daily-dev: "\ea12";
$web-icon-discord: "\ea13"; $web-icon-dark: "\ea13";
$web-icon-divider-vertical: "\ea14"; $web-icon-discord: "\ea14";
$web-icon-download: "\ea15"; $web-icon-divider-vertical: "\ea15";
$web-icon-ext-link: "\ea16"; $web-icon-download: "\ea16";
$web-icon-firebase: "\ea17"; $web-icon-ext-link: "\ea17";
$web-icon-github: "\ea18"; $web-icon-firebase: "\ea18";
$web-icon-google: "\ea19"; $web-icon-github: "\ea19";
$web-icon-hamburger-menu: "\ea1a"; $web-icon-google: "\ea1a";
$web-icon-light: "\ea1b"; $web-icon-hamburger-menu: "\ea1b";
$web-icon-linkedin: "\ea1c"; $web-icon-instagram: "\ea1c";
$web-icon-location: "\ea1d"; $web-icon-light: "\ea1d";
$web-icon-logout-left: "\ea1e"; $web-icon-linkedin: "\ea1e";
$web-icon-logout-right: "\ea1f"; $web-icon-location: "\ea1f";
$web-icon-mailgun: "\ea20"; $web-icon-logout-left: "\ea20";
$web-icon-message: "\ea21"; $web-icon-logout-right: "\ea21";
$web-icon-microsoft: "\ea22"; $web-icon-mailgun: "\ea22";
$web-icon-minus: "\ea23"; $web-icon-mcp: "\ea23";
$web-icon-nuxt: "\ea24"; $web-icon-message: "\ea24";
$web-icon-platform: "\ea25"; $web-icon-microsoft: "\ea25";
$web-icon-play: "\ea26"; $web-icon-minus: "\ea26";
$web-icon-plus: "\ea27"; $web-icon-nuxt: "\ea27";
$web-icon-product-hunt: "\ea28"; $web-icon-platform: "\ea28";
$web-icon-refine: "\ea29"; $web-icon-play: "\ea29";
$web-icon-rest: "\ea2a"; $web-icon-plus: "\ea2a";
$web-icon-search: "\ea2b"; $web-icon-product-hunt: "\ea2b";
$web-icon-sendgrid: "\ea2c"; $web-icon-refine: "\ea2c";
$web-icon-star: "\ea2d"; $web-icon-rest: "\ea2d";
$web-icon-system: "\ea2e"; $web-icon-search: "\ea2e";
$web-icon-textmagic: "\ea2f"; $web-icon-sendgrid: "\ea2f";
$web-icon-twitter: "\ea30"; $web-icon-star: "\ea30";
$web-icon-vue: "\ea31"; $web-icon-system: "\ea31";
$web-icon-x: "\ea32"; $web-icon-textmagic: "\ea32";
$web-icon-ycombinator: "\ea33"; $web-icon-tiktok: "\ea33";
$web-icon-youtube: "\ea34"; $web-icon-twitter: "\ea34";
$web-icon-vue: "\ea35";
$web-icon-x: "\ea36";
$web-icon-ycombinator: "\ea37";
$web-icon-youtube: "\ea38";

View File

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

View File

@@ -41,138 +41,150 @@
.web-icon-arrow-up:before { .web-icon-arrow-up:before {
content: '\ea07'; content: '\ea07';
} }
.web-icon-calendar:before { .web-icon-bluesky:before {
content: '\ea08'; content: '\ea08';
} }
.web-icon-check:before { .web-icon-calendar:before {
content: '\ea09'; content: '\ea09';
} }
.web-icon-chevron-down:before { .web-icon-check:before {
content: '\ea0a'; content: '\ea0a';
} }
.web-icon-chevron-left:before { .web-icon-chevron-down:before {
content: '\ea0b'; content: '\ea0b';
} }
.web-icon-chevron-right:before { .web-icon-chevron-left:before {
content: '\ea0c'; content: '\ea0c';
} }
.web-icon-chevron-up:before { .web-icon-chevron-right:before {
content: '\ea0d'; content: '\ea0d';
} }
.web-icon-close:before { .web-icon-chevron-up:before {
content: '\ea0e'; content: '\ea0e';
} }
.web-icon-command:before { .web-icon-close:before {
content: '\ea0f'; content: '\ea0f';
} }
.web-icon-copy:before { .web-icon-command:before {
content: '\ea10'; content: '\ea10';
} }
.web-icon-daily-dev:before { .web-icon-copy:before {
content: '\ea11'; content: '\ea11';
} }
.web-icon-dark:before { .web-icon-daily-dev:before {
content: '\ea12'; content: '\ea12';
} }
.web-icon-discord:before { .web-icon-dark:before {
content: '\ea13'; content: '\ea13';
} }
.web-icon-divider-vertical:before { .web-icon-discord:before {
content: '\ea14'; content: '\ea14';
} }
.web-icon-download:before { .web-icon-divider-vertical:before {
content: '\ea15'; content: '\ea15';
} }
.web-icon-ext-link:before { .web-icon-download:before {
content: '\ea16'; content: '\ea16';
} }
.web-icon-firebase:before { .web-icon-ext-link:before {
content: '\ea17'; content: '\ea17';
} }
.web-icon-github:before { .web-icon-firebase:before {
content: '\ea18'; content: '\ea18';
} }
.web-icon-google:before { .web-icon-github:before {
content: '\ea19'; content: '\ea19';
} }
.web-icon-hamburger-menu:before { .web-icon-google:before {
content: '\ea1a'; content: '\ea1a';
} }
.web-icon-light:before { .web-icon-hamburger-menu:before {
content: '\ea1b'; content: '\ea1b';
} }
.web-icon-linkedin:before { .web-icon-instagram:before {
content: '\ea1c'; content: '\ea1c';
} }
.web-icon-location:before { .web-icon-light:before {
content: '\ea1d'; content: '\ea1d';
} }
.web-icon-logout-left:before { .web-icon-linkedin:before {
content: '\ea1e'; content: '\ea1e';
} }
.web-icon-logout-right:before { .web-icon-location:before {
content: '\ea1f'; content: '\ea1f';
} }
.web-icon-mailgun:before { .web-icon-logout-left:before {
content: '\ea20'; content: '\ea20';
} }
.web-icon-message:before { .web-icon-logout-right:before {
content: '\ea21'; content: '\ea21';
} }
.web-icon-microsoft:before { .web-icon-mailgun:before {
content: '\ea22'; content: '\ea22';
} }
.web-icon-minus:before { .web-icon-mcp:before {
content: '\ea23'; content: '\ea23';
} }
.web-icon-nuxt:before { .web-icon-message:before {
content: '\ea24'; content: '\ea24';
} }
.web-icon-platform:before { .web-icon-microsoft:before {
content: '\ea25'; content: '\ea25';
} }
.web-icon-play:before { .web-icon-minus:before {
content: '\ea26'; content: '\ea26';
} }
.web-icon-plus:before { .web-icon-nuxt:before {
content: '\ea27'; content: '\ea27';
} }
.web-icon-product-hunt:before { .web-icon-platform:before {
content: '\ea28'; content: '\ea28';
} }
.web-icon-refine:before { .web-icon-play:before {
content: '\ea29'; content: '\ea29';
} }
.web-icon-rest:before { .web-icon-plus:before {
content: '\ea2a'; content: '\ea2a';
} }
.web-icon-search:before { .web-icon-product-hunt:before {
content: '\ea2b'; content: '\ea2b';
} }
.web-icon-sendgrid:before { .web-icon-refine:before {
content: '\ea2c'; content: '\ea2c';
} }
.web-icon-star:before { .web-icon-rest:before {
content: '\ea2d'; content: '\ea2d';
} }
.web-icon-system:before { .web-icon-search:before {
content: '\ea2e'; content: '\ea2e';
} }
.web-icon-textmagic:before { .web-icon-sendgrid:before {
content: '\ea2f'; content: '\ea2f';
} }
.web-icon-twitter:before { .web-icon-star:before {
content: '\ea30'; content: '\ea30';
} }
.web-icon-vue:before { .web-icon-system:before {
content: '\ea31'; content: '\ea31';
} }
.web-icon-x:before { .web-icon-textmagic:before {
content: '\ea32'; content: '\ea32';
} }
.web-icon-ycombinator:before { .web-icon-tiktok:before {
content: '\ea33'; content: '\ea33';
} }
.web-icon-youtube:before { .web-icon-twitter:before {
content: '\ea34'; content: '\ea34';
} }
.web-icon-vue:before {
content: '\ea35';
}
.web-icon-x:before {
content: '\ea36';
}
.web-icon-ycombinator:before {
content: '\ea37';
}
.web-icon-youtube:before {
content: '\ea38';
}

Binary file not shown.

View File

@@ -23,51 +23,55 @@
.web-icon-arrow-left:before { content: "\ea05"; } .web-icon-arrow-left:before { content: "\ea05"; }
.web-icon-arrow-right:before { content: "\ea06"; } .web-icon-arrow-right:before { content: "\ea06"; }
.web-icon-arrow-up:before { content: "\ea07"; } .web-icon-arrow-up:before { content: "\ea07"; }
.web-icon-calendar:before { content: "\ea08"; } .web-icon-bluesky:before { content: "\ea08"; }
.web-icon-check:before { content: "\ea09"; } .web-icon-calendar:before { content: "\ea09"; }
.web-icon-chevron-down:before { content: "\ea0a"; } .web-icon-check:before { content: "\ea0a"; }
.web-icon-chevron-left:before { content: "\ea0b"; } .web-icon-chevron-down:before { content: "\ea0b"; }
.web-icon-chevron-right:before { content: "\ea0c"; } .web-icon-chevron-left:before { content: "\ea0c"; }
.web-icon-chevron-up:before { content: "\ea0d"; } .web-icon-chevron-right:before { content: "\ea0d"; }
.web-icon-close:before { content: "\ea0e"; } .web-icon-chevron-up:before { content: "\ea0e"; }
.web-icon-command:before { content: "\ea0f"; } .web-icon-close:before { content: "\ea0f"; }
.web-icon-copy:before { content: "\ea10"; } .web-icon-command:before { content: "\ea10"; }
.web-icon-daily-dev:before { content: "\ea11"; } .web-icon-copy:before { content: "\ea11"; }
.web-icon-dark:before { content: "\ea12"; } .web-icon-daily-dev:before { content: "\ea12"; }
.web-icon-discord:before { content: "\ea13"; } .web-icon-dark:before { content: "\ea13"; }
.web-icon-divider-vertical:before { content: "\ea14"; } .web-icon-discord:before { content: "\ea14"; }
.web-icon-download:before { content: "\ea15"; } .web-icon-divider-vertical:before { content: "\ea15"; }
.web-icon-ext-link:before { content: "\ea16"; } .web-icon-download:before { content: "\ea16"; }
.web-icon-firebase:before { content: "\ea17"; } .web-icon-ext-link:before { content: "\ea17"; }
.web-icon-github:before { content: "\ea18"; } .web-icon-firebase:before { content: "\ea18"; }
.web-icon-google:before { content: "\ea19"; } .web-icon-github:before { content: "\ea19"; }
.web-icon-hamburger-menu:before { content: "\ea1a"; } .web-icon-google:before { content: "\ea1a"; }
.web-icon-light:before { content: "\ea1b"; } .web-icon-hamburger-menu:before { content: "\ea1b"; }
.web-icon-linkedin:before { content: "\ea1c"; } .web-icon-instagram:before { content: "\ea1c"; }
.web-icon-location:before { content: "\ea1d"; } .web-icon-light:before { content: "\ea1d"; }
.web-icon-logout-left:before { content: "\ea1e"; } .web-icon-linkedin:before { content: "\ea1e"; }
.web-icon-logout-right:before { content: "\ea1f"; } .web-icon-location:before { content: "\ea1f"; }
.web-icon-mailgun:before { content: "\ea20"; } .web-icon-logout-left:before { content: "\ea20"; }
.web-icon-message:before { content: "\ea21"; } .web-icon-logout-right:before { content: "\ea21"; }
.web-icon-microsoft:before { content: "\ea22"; } .web-icon-mailgun:before { content: "\ea22"; }
.web-icon-minus:before { content: "\ea23"; } .web-icon-mcp:before { content: "\ea23"; }
.web-icon-nuxt:before { content: "\ea24"; } .web-icon-message:before { content: "\ea24"; }
.web-icon-platform:before { content: "\ea25"; } .web-icon-microsoft:before { content: "\ea25"; }
.web-icon-play:before { content: "\ea26"; } .web-icon-minus:before { content: "\ea26"; }
.web-icon-plus:before { content: "\ea27"; } .web-icon-nuxt:before { content: "\ea27"; }
.web-icon-product-hunt:before { content: "\ea28"; } .web-icon-platform:before { content: "\ea28"; }
.web-icon-refine:before { content: "\ea29"; } .web-icon-play:before { content: "\ea29"; }
.web-icon-rest:before { content: "\ea2a"; } .web-icon-plus:before { content: "\ea2a"; }
.web-icon-search:before { content: "\ea2b"; } .web-icon-product-hunt:before { content: "\ea2b"; }
.web-icon-sendgrid:before { content: "\ea2c"; } .web-icon-refine:before { content: "\ea2c"; }
.web-icon-star:before { content: "\ea2d"; } .web-icon-rest:before { content: "\ea2d"; }
.web-icon-system:before { content: "\ea2e"; } .web-icon-search:before { content: "\ea2e"; }
.web-icon-textmagic:before { content: "\ea2f"; } .web-icon-sendgrid:before { content: "\ea2f"; }
.web-icon-twitter:before { content: "\ea30"; } .web-icon-star:before { content: "\ea30"; }
.web-icon-vue:before { content: "\ea31"; } .web-icon-system:before { content: "\ea31"; }
.web-icon-x:before { content: "\ea32"; } .web-icon-textmagic:before { content: "\ea32"; }
.web-icon-ycombinator:before { content: "\ea33"; } .web-icon-tiktok:before { content: "\ea33"; }
.web-icon-youtube:before { content: "\ea34"; } .web-icon-twitter:before { content: "\ea34"; }
.web-icon-vue:before { content: "\ea35"; }
.web-icon-x:before { content: "\ea36"; }
.web-icon-ycombinator:before { content: "\ea37"; }
.web-icon-youtube:before { content: "\ea38"; }
$web-icon-apple: "\ea01"; $web-icon-apple: "\ea01";
$web-icon-appwrite: "\ea02"; $web-icon-appwrite: "\ea02";
@@ -76,48 +80,52 @@ $web-icon-arrow-ext-link: "\ea04";
$web-icon-arrow-left: "\ea05"; $web-icon-arrow-left: "\ea05";
$web-icon-arrow-right: "\ea06"; $web-icon-arrow-right: "\ea06";
$web-icon-arrow-up: "\ea07"; $web-icon-arrow-up: "\ea07";
$web-icon-calendar: "\ea08"; $web-icon-bluesky: "\ea08";
$web-icon-check: "\ea09"; $web-icon-calendar: "\ea09";
$web-icon-chevron-down: "\ea0a"; $web-icon-check: "\ea0a";
$web-icon-chevron-left: "\ea0b"; $web-icon-chevron-down: "\ea0b";
$web-icon-chevron-right: "\ea0c"; $web-icon-chevron-left: "\ea0c";
$web-icon-chevron-up: "\ea0d"; $web-icon-chevron-right: "\ea0d";
$web-icon-close: "\ea0e"; $web-icon-chevron-up: "\ea0e";
$web-icon-command: "\ea0f"; $web-icon-close: "\ea0f";
$web-icon-copy: "\ea10"; $web-icon-command: "\ea10";
$web-icon-daily-dev: "\ea11"; $web-icon-copy: "\ea11";
$web-icon-dark: "\ea12"; $web-icon-daily-dev: "\ea12";
$web-icon-discord: "\ea13"; $web-icon-dark: "\ea13";
$web-icon-divider-vertical: "\ea14"; $web-icon-discord: "\ea14";
$web-icon-download: "\ea15"; $web-icon-divider-vertical: "\ea15";
$web-icon-ext-link: "\ea16"; $web-icon-download: "\ea16";
$web-icon-firebase: "\ea17"; $web-icon-ext-link: "\ea17";
$web-icon-github: "\ea18"; $web-icon-firebase: "\ea18";
$web-icon-google: "\ea19"; $web-icon-github: "\ea19";
$web-icon-hamburger-menu: "\ea1a"; $web-icon-google: "\ea1a";
$web-icon-light: "\ea1b"; $web-icon-hamburger-menu: "\ea1b";
$web-icon-linkedin: "\ea1c"; $web-icon-instagram: "\ea1c";
$web-icon-location: "\ea1d"; $web-icon-light: "\ea1d";
$web-icon-logout-left: "\ea1e"; $web-icon-linkedin: "\ea1e";
$web-icon-logout-right: "\ea1f"; $web-icon-location: "\ea1f";
$web-icon-mailgun: "\ea20"; $web-icon-logout-left: "\ea20";
$web-icon-message: "\ea21"; $web-icon-logout-right: "\ea21";
$web-icon-microsoft: "\ea22"; $web-icon-mailgun: "\ea22";
$web-icon-minus: "\ea23"; $web-icon-mcp: "\ea23";
$web-icon-nuxt: "\ea24"; $web-icon-message: "\ea24";
$web-icon-platform: "\ea25"; $web-icon-microsoft: "\ea25";
$web-icon-play: "\ea26"; $web-icon-minus: "\ea26";
$web-icon-plus: "\ea27"; $web-icon-nuxt: "\ea27";
$web-icon-product-hunt: "\ea28"; $web-icon-platform: "\ea28";
$web-icon-refine: "\ea29"; $web-icon-play: "\ea29";
$web-icon-rest: "\ea2a"; $web-icon-plus: "\ea2a";
$web-icon-search: "\ea2b"; $web-icon-product-hunt: "\ea2b";
$web-icon-sendgrid: "\ea2c"; $web-icon-refine: "\ea2c";
$web-icon-star: "\ea2d"; $web-icon-rest: "\ea2d";
$web-icon-system: "\ea2e"; $web-icon-search: "\ea2e";
$web-icon-textmagic: "\ea2f"; $web-icon-sendgrid: "\ea2f";
$web-icon-twitter: "\ea30"; $web-icon-star: "\ea30";
$web-icon-vue: "\ea31"; $web-icon-system: "\ea31";
$web-icon-x: "\ea32"; $web-icon-textmagic: "\ea32";
$web-icon-ycombinator: "\ea33"; $web-icon-tiktok: "\ea33";
$web-icon-youtube: "\ea34"; $web-icon-twitter: "\ea34";
$web-icon-vue: "\ea35";
$web-icon-x: "\ea36";
$web-icon-ycombinator: "\ea37";
$web-icon-youtube: "\ea38";

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 166 KiB

After

Width:  |  Height:  |  Size: 185 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 70 KiB

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -1,11 +1,73 @@
// @ts-expect-error missing types // @ts-expect-error missing types
import SVGFixer from 'oslllo-svg-fixer'; import SVGFixer from 'oslllo-svg-fixer';
import svgtofont from 'svgtofont'; import svgtofont from 'svgtofont';
import { resolve } from 'path'; import { basename, extname, resolve } from 'path';
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from 'fs';
const src = resolve(process.cwd(), 'src/icons/svg'); const src = resolve(process.cwd(), 'src/icons/svg');
const optimized = resolve(process.cwd(), 'src/icons/optimized'); const optimized = resolve(process.cwd(), 'src/icons/optimized');
const dist = resolve(process.cwd(), 'src/icons/output'); const dist = resolve(process.cwd(), 'src/icons/output');
const outputPath = resolve(process.cwd(), 'src/lib/components/ui/icon');
const generateIconSprite = () => {
const files = readdirSync(optimized);
const outputDir = resolve(`${outputPath}`);
const spriteOutputPath = resolve(outputDir, 'sprite.svelte');
if (!existsSync(outputDir)) {
mkdirSync(outputDir, { recursive: true });
}
let spriteContent = `<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style="display: none;">\n`;
files.forEach((file) => {
if (!file.endsWith('.svg')) return;
const filePath = resolve(optimized, file);
const fileName = basename(file, '.svg');
const svgContent = readFileSync(filePath, 'utf8');
// Extract the SVG content (everything between <svg> and </svg>)
const svgMatch = svgContent.match(/<svg[^>]*>([\s\S]*?)<\/svg>/i);
if (svgMatch && svgMatch[1]) {
const innerContent = svgMatch[1].trim();
const viewBoxMatch = svgContent.match(/viewBox=['"]([^'"]*)['"]/i);
const viewBox = viewBoxMatch ? viewBoxMatch[1] : '0 0 24 24';
// Add symbol with the extracted content
spriteContent += ` <symbol id="${fileName}" stroke="currentColor" viewBox="${viewBox}">\n ${innerContent}\n </symbol>\n`;
}
});
// Close the sprite
spriteContent += '</svg>';
// Write the sprite file
writeFileSync(spriteOutputPath, spriteContent);
console.log(`Created SVG sprite at ${spriteOutputPath}`);
return spriteOutputPath;
};
const generateIconType = () => {
try {
const files = readdirSync(optimized);
const fileNames = files
.filter((file) => extname(file) !== '')
.map((file) => basename(file, extname(file)));
const typeDefinition = `export type IconType = ${fileNames.map((name) => `"${name}"`).join(' | ')};`;
writeFileSync(`${outputPath}/types.ts`, typeDefinition);
console.log(`Type generated successfully at ${outputPath}`);
console.log(`Generated type: ${typeDefinition}`);
} catch (error) {
console.error('Error generating filename type:', error);
}
};
export const optimizeSVG = async () => { export const optimizeSVG = async () => {
const fixer = new SVGFixer(src, optimized, { const fixer = new SVGFixer(src, optimized, {
@@ -36,5 +98,7 @@ export const generateIcons = async () => {
}, },
emptyDist: true, emptyDist: true,
generateInfoData: true generateInfoData: true
}); })
.then(() => generateIconSprite())
.then(() => generateIconType());
}; };

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" viewBox="0 0 40 40" fill="none">
<path d="M9.58671 6.15399C13.5994 9.28212 17.9155 15.6246 19.5001 19.0284C21.0849 15.6249 25.4007 9.28205 29.4135 6.15399C32.3088 3.89686 37 2.15043 37 7.70768C37 8.81754 36.3872 17.0311 36.0278 18.3645C34.7785 23.0005 30.226 24.1829 26.1766 23.4672C33.255 24.7182 35.0556 28.8619 31.1669 33.0056C23.7813 40.8752 20.5517 31.031 19.7237 28.5086C19.572 28.0462 19.501 27.8298 19.5 28.0138C19.4989 27.8298 19.428 28.0462 19.2763 28.5086C18.4487 31.031 15.2191 40.8755 7.83314 33.0056C3.9443 28.8619 5.74492 24.718 12.8234 23.4672C8.77385 24.1829 4.22134 23.0005 2.97221 18.3645C2.61279 17.031 2 8.81742 2 7.70768C2 2.15043 6.6913 3.89686 9.58651 6.15399H9.58671Z" fill="#19191C"/>
</svg>

After

Width:  |  Height:  |  Size: 823 B

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" width="30" height="30" viewBox="0 0 30 30" fill="none">
<path d="M15.0029 2.7017C19.0115 2.7017 19.4862 2.71928 21.0627 2.78961C22.5278 2.85407 23.319 3.10022 23.8465 3.30533C24.5439 3.57492 25.0479 3.90311 25.5694 4.42469C26.0969 4.95214 26.4192 5.45028 26.6888 6.14769C26.8939 6.67513 27.1401 7.47216 27.2045 8.93143C27.2749 10.5138 27.2924 10.9885 27.2924 14.9912C27.2924 18.9998 27.2749 19.4745 27.2045 21.051C27.1401 22.5161 26.8939 23.3073 26.6888 23.8347C26.4192 24.5321 26.091 25.0361 25.5694 25.5577C25.042 26.0852 24.5439 26.4075 23.8465 26.6771C23.319 26.8822 22.522 27.1283 21.0627 27.1928C19.4804 27.2631 19.0057 27.2807 15.0029 27.2807C10.9943 27.2807 10.5196 27.2631 8.94315 27.1928C7.47802 27.1283 6.68685 26.8822 6.15941 26.6771C5.46201 26.4075 4.958 26.0793 4.43641 25.5577C3.90897 25.0303 3.58664 24.5321 3.31705 23.8347C3.11194 23.3073 2.86579 22.5103 2.80133 21.051C2.731 19.4686 2.71342 18.9939 2.71342 14.9912C2.71342 10.9826 2.731 10.5079 2.80133 8.93143C2.86579 7.4663 3.11194 6.67513 3.31705 6.14769C3.58664 5.45028 3.91483 4.94628 4.43641 4.42469C4.96386 3.89725 5.46201 3.57492 6.15941 3.30533C6.68685 3.10022 7.48388 2.85407 8.94315 2.78961C10.5196 2.71928 10.9943 2.7017 15.0029 2.7017ZM15.0029 0C10.9299 0 10.42 0.0175816 8.82008 0.0879078C7.22602 0.158234 6.1301 0.416097 5.1807 0.78531C4.19027 1.1721 3.35222 1.68197 2.52002 2.52002C1.68197 3.35222 1.1721 4.19027 0.78531 5.17484C0.416097 6.1301 0.158234 7.22016 0.0879078 8.81422C0.0175816 10.42 0 10.9299 0 15.0029C0 19.076 0.0175816 19.5859 0.0879078 21.1858C0.158234 22.7798 0.416097 23.8758 0.78531 24.8252C1.1721 25.8156 1.68197 26.6536 2.52002 27.4858C3.35222 28.318 4.19027 28.8338 5.17484 29.2147C6.1301 29.5839 7.22016 29.8418 8.81422 29.9121C10.4141 29.9824 10.924 30 14.9971 30C19.0701 30 19.58 29.9824 21.1799 29.9121C22.774 29.8418 23.8699 29.5839 24.8193 29.2147C25.8039 28.8338 26.6419 28.318 27.4741 27.4858C28.3063 26.6536 28.822 25.8156 29.203 24.831C29.5722 23.8758 29.83 22.7857 29.9004 21.1916C29.9707 19.5917 29.9883 19.0819 29.9883 15.0088C29.9883 10.9357 29.9707 10.4259 29.9004 8.82594C29.83 7.23188 29.5722 6.13596 29.203 5.18656C28.8338 4.19027 28.3239 3.35222 27.4858 2.52002C26.6536 1.68783 25.8156 1.1721 24.831 0.79117C23.8758 0.421957 22.7857 0.164095 21.1916 0.0937683C19.5859 0.0175816 19.076 0 15.0029 0Z" fill="#19191C"/>
<path d="M15.0029 7.29633C10.7482 7.29633 7.29636 10.7482 7.29636 15.0029C7.29636 19.2576 10.7482 22.7095 15.0029 22.7095C19.2577 22.7095 22.7095 19.2576 22.7095 15.0029C22.7095 10.7482 19.2577 7.29633 15.0029 7.29633ZM15.0029 20.0019C12.2426 20.0019 10.0039 17.7632 10.0039 15.0029C10.0039 12.2426 12.2426 10.0039 15.0029 10.0039C17.7632 10.0039 20.002 12.2426 20.002 15.0029C20.002 17.7632 17.7632 20.0019 15.0029 20.0019Z" fill="#19191C"/>
<path d="M24.8134 6.99156C24.8134 7.98785 24.0047 8.79075 23.0143 8.79075C22.018 8.79075 21.2151 7.98199 21.2151 6.99156C21.2151 5.99528 22.0238 5.19238 23.0143 5.19238C24.0047 5.19238 24.8134 6.00114 24.8134 6.99156Z" fill="#19191C"/>
</svg>

After

Width:  |  Height:  |  Size: 3.0 KiB

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

@@ -0,0 +1,5 @@
<svg width="156" height="174" viewBox="0 0 156 174" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6 81.8531L73.8823 13.9709C83.255 4.59826 98.451 4.59826 107.823 13.9709C117.196 23.3434 117.196 38.5394 107.823 47.912L56.5581 99.1773" stroke="black" stroke-width="12" stroke-linecap="round"/>
<path d="M57.2656 98.4706L107.823 47.9123C117.196 38.5397 132.392 38.5397 141.765 47.9123L142.118 48.2658C151.491 57.6384 151.491 72.8344 142.118 82.2069L80.7251 143.601C77.6009 146.725 77.6009 151.79 80.7251 154.914L93.3313 167.521" stroke="black" stroke-width="12" stroke-linecap="round"/>
<path d="M90.8533 30.9414L40.6485 81.146C31.276 90.5183 31.276 105.714 40.6485 115.087C50.0211 124.459 65.2171 124.459 74.5897 115.087L124.794 64.8825" stroke="black" stroke-width="12" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 813 B

4
src/icons/svg/tiktok.svg Normal file
View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" viewBox="0 0 40 40" fill="none">
<path d="M25.8811 5H20.9485V25.4347C20.9485 27.8696 19.0515 29.8696 16.6906 29.8696C14.3297 29.8696 12.4325 27.8696 12.4325 25.4347C12.4325 23.0435 14.2875 21.0869 16.5641 21V15.8696C11.5472 15.9565 7.5 20.1739 7.5 25.4347C7.5 30.7392 11.6315 35 16.7327 35C21.8339 35 25.9654 30.6957 25.9654 25.4347V14.9565C27.8204 16.3478 30.0969 17.1739 32.5 17.2174V12.087C28.7901 11.9565 25.8811 8.82608 25.8811 5Z" fill="#19191C"/>
</svg>

After

Width:  |  Height:  |  Size: 566 B

View File

@@ -16,5 +16,5 @@
<slot /> <slot />
</video> </video>
{:else} {:else}
<img loading="lazy" {src} {alt} class={className} /> <img loading="lazy" {src} {alt} class={className} data-active="" />
{/if} {/if}

View File

@@ -1,5 +1,6 @@
import { Analytics, type AnalyticsPlugin } from 'analytics'; import { Analytics, type AnalyticsPlugin } from 'analytics';
import Plausible from 'plausible-tracker'; import Plausible from 'plausible-tracker';
import posthogEvent from 'posthog-js';
import { get } from 'svelte/store'; import { get } from 'svelte/store';
import { page } from '$app/stores'; import { page } from '$app/stores';
@@ -54,7 +55,12 @@ const analytics = Analytics({
plugins: [plausible('appwrite.io')] plugins: [plausible('appwrite.io')]
}); });
export const trackEvent = async (name: string, data: object = {}) => { export type TrackEventArgs = {
plausible?: { name: string; data?: object };
posthog?: { name: string };
};
export const trackEvent = async (platforms: TrackEventArgs) => {
if (!isTrackingAllowed()) { if (!isTrackingAllowed()) {
return; return;
} }
@@ -63,23 +69,16 @@ export const trackEvent = async (name: string, data: object = {}) => {
const path = currentPage.route.id ?? ''; const path = currentPage.route.id ?? '';
if (ENV.DEV || ENV.PREVIEW) { if (ENV.DEV || ENV.PREVIEW) {
console.log(`[Analytics] Event ${name} ${path}`, data); console.log(`[Analytics] Event`, platforms.plausible, platforms.posthog);
} else { } else {
await analytics.track(name, { ...data, path }); if (platforms.plausible) {
await analytics.track(platforms.plausible.name, { ...platforms.plausible.data, path });
}
if (platforms.posthog) {
posthogEvent.capture(platforms.posthog.name);
}
} }
}; };
export function isTrackingAllowed() { export const isTrackingAllowed = () => !ENV.TEST;
if (ENV.TEST) {
return;
}
if (window.navigator?.doNotTrack) {
if (navigator.doNotTrack === '1' || navigator.doNotTrack === 'yes') {
return false;
} else {
return true;
}
} else {
return true;
}
}

View File

@@ -10,7 +10,7 @@
class="true-body" class="true-body"
style:width={`${$bodyRect?.width ?? 0}px`} style:width={`${$bodyRect?.width ?? 0}px`}
style:height={`${$bodyRect?.height ?? 0}px`} style:height={`${$bodyRect?.height ?? 0}px`}
/> ></div>
<div class="body" use:rect={bodyRect}> <div class="body" use:rect={bodyRect}>
<slot /> <slot />
</div> </div>

View File

@@ -5,16 +5,16 @@
<div class="code-console"> <div class="code-console">
<div class="header"> <div class="header">
<div class="ellipse" /> <div class="ellipse"></div>
<div class="ellipse-2" /> <div class="ellipse-2"></div>
<div class="ellipse-3" /> <div class="ellipse-3"></div>
</div> </div>
<div class="block"> <div class="block">
<AutoBox> <AutoBox>
<slot {Code} /> <slot {Code} />
</AutoBox> </AutoBox>
</div> </div>
<div id="code-bottom" /> <div id="code-bottom"></div>
</div> </div>
<style lang="scss"> <style lang="scss">

View File

@@ -2,7 +2,7 @@
import { toScale, type Scale } from '$lib/utils/toScale'; import { toScale, type Scale } from '$lib/utils/toScale';
import { spring, type AnimationListOptions, type SpringOptions } from 'motion'; import { spring, type AnimationListOptions, type SpringOptions } from 'motion';
import { animation, createScrollHandler, scroll, type Animation } from '.'; import { animation, createScrollHandler, scroll, type Animation } from '.';
import { GITHUB_REPO_LINK, GITHUB_STARS } from '$lib/constants'; import { SOCIAL_STATS } from '$lib/constants';
const springOptions: SpringOptions = { const springOptions: SpringOptions = {
stiffness: 58.78, stiffness: 58.78,
@@ -180,7 +180,7 @@
<div class="cards-wrapper"> <div class="cards-wrapper">
<a <a
href="/discord" href={SOCIAL_STATS.DISCORD.LINK}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
class="web-card is-white web-u-min-block-size-320 oss-card flex flex-col" class="web-card is-white web-u-min-block-size-320 oss-card flex flex-col"
@@ -191,30 +191,32 @@
class="web-icon-discord web-u-font-size-40" class="web-icon-discord web-u-font-size-40"
aria-hidden="true" aria-hidden="true"
aria-label="Discord" aria-label="Discord"
/> ></span>
</div>
<div class="text-title font-aeonik-pro mt-auto">
{SOCIAL_STATS.DISCORD.STAT} Discord Members
</div> </div>
<div class="text-title font-aeonik-pro mt-auto">17k+ Discord Members</div>
</a> </a>
<a <a
class="web-card is-white web-u-min-block-size-320 oss-card flex flex-col" class="web-card is-white web-u-min-block-size-320 oss-card flex flex-col"
id="oss-github" id="oss-github"
href={GITHUB_REPO_LINK} href={SOCIAL_STATS.GITHUB.LINK}
> >
<div class="flex flex-col justify-between gap-8"> <div class="flex flex-col justify-between gap-8">
<span <span
class="web-icon-github web-u-font-size-40" class="web-icon-github web-u-font-size-40"
aria-hidden="true" aria-hidden="true"
aria-label="GitHub" aria-label="GitHub"
/> ></span>
</div> </div>
<div class="text-title font-aeonik-pro mt-auto"> <div class="text-title font-aeonik-pro mt-auto">
{GITHUB_STARS}+ GitHub Stars {SOCIAL_STATS.GITHUB.STAT} GitHub Stars
</div> </div>
</a> </a>
<a <a
href="https://twitter.com/appwrite" href={SOCIAL_STATS.TWITTER.LINK}
class="web-card is-white web-u-min-block-size-320 oss-card flex flex-col" class="web-card is-white web-u-min-block-size-320 oss-card flex flex-col"
id="oss-twitter" id="oss-twitter"
> >
@@ -223,13 +225,15 @@
class="web-icon-x web-u-font-size-40" class="web-icon-x web-u-font-size-40"
aria-hidden="true" aria-hidden="true"
aria-label="Twitter" aria-label="Twitter"
/> ></span>
</div>
<div class="text-title font-aeonik-pro mt-auto">
{SOCIAL_STATS.TWITTER.STAT} Twitter Followers
</div> </div>
<div class="text-title font-aeonik-pro mt-auto">128k+ Twitter Followers</div>
</a> </a>
<a <a
href="https://www.youtube.com/@Appwrite" href={SOCIAL_STATS.YOUTUBE.LINK}
class="web-card is-white web-u-min-block-size-320 oss-card flex flex-col" class="web-card is-white web-u-min-block-size-320 oss-card flex flex-col"
id="oss-youtube" id="oss-youtube"
> >
@@ -238,24 +242,28 @@
class="web-icon-youtube web-u-font-size-40" class="web-icon-youtube web-u-font-size-40"
aria-hidden="true" aria-hidden="true"
aria-label="YouTube" aria-label="YouTube"
/> ></span>
</div>
<div class="text-title font-aeonik-pro mt-auto">
{SOCIAL_STATS.YOUTUBE.STAT} Youtube Subscribers
</div> </div>
<div class="text-title font-aeonik-pro mt-auto">7k+ Youtube Subscribers</div>
</a> </a>
<a <a
class="web-card is-white web-u-min-block-size-320 oss-card flex flex-col" class="web-card is-white web-u-min-block-size-320 oss-card flex flex-col"
id="oss-commits" id="oss-commits"
href={GITHUB_REPO_LINK} href={SOCIAL_STATS.GITHUB.LINK}
> >
<div class="flex flex-col justify-between gap-8"> <div class="flex flex-col justify-between gap-8">
<span <span
class="web-icon-github web-u-font-size-40" class="web-icon-github web-u-font-size-40"
aria-hidden="true" aria-hidden="true"
aria-label="GitHub" aria-label="GitHub"
/> ></span>
</div>
<div class="text-title font-aeonik-pro mt-auto">
{SOCIAL_STATS.GITHUB.EXTRA?.COMMITS} Code Commits
</div> </div>
<div class="text-title font-aeonik-pro mt-auto">21k+ Code Commits</div>
</a> </a>
</div> </div>
</div> </div>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 210 KiB

After

Width:  |  Height:  |  Size: 111 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 183 KiB

After

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 197 KiB

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 300 KiB

After

Width:  |  Height:  |  Size: 120 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 98 KiB

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 265 KiB

After

Width:  |  Height:  |  Size: 130 KiB

View File

@@ -5,6 +5,7 @@
import StorageShot from './(assets)/storage-shot.png?enhanced'; import StorageShot from './(assets)/storage-shot.png?enhanced';
import RealtimeShot from './(assets)/realtime-shot.png?enhanced'; import RealtimeShot from './(assets)/realtime-shot.png?enhanced';
import MessagingShot from './(assets)/messaging-shot.png?enhanced'; import MessagingShot from './(assets)/messaging-shot.png?enhanced';
import type { EnhancedImgAttributes } from '@sveltejs/enhanced-img';
export const elId = writable(0); export const elId = writable(0);
@@ -33,7 +34,7 @@
subtitle: string; subtitle: string;
description: string; description: string;
features: string[]; features: string[];
shot?: string; shot?: EnhancedImgAttributes['src'];
}; };
export const infos: { [K in Product]?: ProductInfo } = { export const infos: { [K in Product]?: ProductInfo } = {
auth: { auth: {
@@ -158,7 +159,6 @@
import { postController } from './post'; import { postController } from './post';
import Post from './post/post.svelte'; import Post from './post/post.svelte';
import { anyify } from '$lib/utils/anyify'; import { anyify } from '$lib/utils/anyify';
import Badge from '$lib/components/ui/Badge.svelte';
/* Basic Animation setup */ /* Basic Animation setup */
let scrollInfo = { let scrollInfo = {
@@ -238,7 +238,7 @@
> >
{#if scrollInfo.percentage > -0.1} {#if scrollInfo.percentage > -0.1}
<span <span
class="web-badges text-micro uppercase !text-white" class="web-badges text-micro !text-white uppercase"
transition:slide={{ axis: 'x' }}>Products_</span transition:slide={{ axis: 'x' }}>Products_</span
> >
@@ -508,10 +508,6 @@
display: flex; display: flex;
align-items: center; align-items: center;
gap: 0.75rem; gap: 0.75rem;
.web-label {
margin-block-start: 0.25rem;
}
} }
h4 { h4 {

View File

@@ -5,7 +5,7 @@
<div class="outside"> <div class="outside">
<div class="wrapper"> <div class="wrapper">
<span class="web-badges text-micro uppercase !text-white">Products_</span> <span class="web-badges text-micro !text-white uppercase">Products_</span>
<h2 class="text-display font-aeonik-pro text-primary mt-4"> <h2 class="text-display font-aeonik-pro text-primary mt-4">
Your backend, minus the hassle Your backend, minus the hassle
@@ -60,7 +60,7 @@
</p> </p>
</div> </div>
</div> </div>
<div class="img-overlay" /> <div class="img-overlay"></div>
</div> </div>
<style lang="scss"> <style lang="scss">
@@ -108,10 +108,6 @@
display: flex; display: flex;
align-items: center; align-items: center;
gap: 0.75rem; gap: 0.75rem;
.web-label {
margin-block-start: 0.25rem;
}
} }
h4 { h4 {

View File

@@ -22,7 +22,7 @@
<div class="wrapper"> <div class="wrapper">
<button use:melt={$root} class="anim-checkbox"> <button use:melt={$root} class="anim-checkbox">
{#if $isChecked} {#if $isChecked}
<span class="web-icon-check" /> <span class="web-icon-check"></span>
{/if} {/if}
</button> </button>
</div> </div>

View File

@@ -14,12 +14,12 @@
{#each objectKeys($state.controls) as provider, i} {#each objectKeys($state.controls) as provider, i}
{@const isLast = i === objectKeys($state.controls).length - 1} {@const isLast = i === objectKeys($state.controls).length - 1}
<div> <div>
<span class={getIcon(provider)} /> <span class={getIcon(provider)}></span>
<span>{provider}</span> <span>{provider}</span>
<Switch bind:checked={$state.controls[provider]} /> <Switch bind:checked={$state.controls[provider]} />
</div> </div>
{#if !isLast} {#if !isLast}
<div class="sep" /> <div class="sep"></div>
{/if} {/if}
{/each} {/each}
</div> </div>

View File

@@ -47,7 +47,7 @@
animate:flip={{ duration: 250 }} animate:flip={{ duration: 250 }}
> >
<div class="inner"> <div class="inner">
<span class="web-icon-{provider.toLowerCase()}" /> <span class="web-icon-{provider.toLowerCase()}"></span>
<span>{provider}</span> <span>{provider}</span>
</div> </div>
</button> </button>

View File

@@ -14,7 +14,7 @@
{#each $state.tasks.slice(0, $state.tableSlice) as task (task.id)} {#each $state.tasks.slice(0, $state.tableSlice) as task (task.id)}
<div class="row" transition:slide={{ duration: 150 }} animate:flip={{ duration: 150 }}> <div class="row" transition:slide={{ duration: 150 }} animate:flip={{ duration: 150 }}>
<div class="copy-button"> <div class="copy-button">
<span class="web-icon-copy" /> <span class="web-icon-copy"></span>
<span>{task.id}</span> <span>{task.id}</span>
</div> </div>
<span class="truncated">{task.title}</span> <span class="truncated">{task.title}</span>

View File

@@ -9,7 +9,7 @@
<div data-theme-ignore class="inner-phone light"> <div data-theme-ignore class="inner-phone light">
<div class="header"> <div class="header">
<p class="title">Your tasks</p> <p class="title">Your tasks</p>
<span class="icon-menu" aria-label="menu" /> <span class="icon-menu" aria-label="menu"></span>
</div> </div>
<div class="date">Today</div> <div class="date">Today</div>
@@ -22,9 +22,9 @@
{/each} {/each}
</div> </div>
<button class="add-btn"> <div class="add-btn">
<span class="web-icon-plus" /> <span class="web-icon-plus"></span>
</button> </div>
</div> </div>
<style lang="scss"> <style lang="scss">

View File

@@ -25,14 +25,14 @@ return res.json({ success: true });`.trim();
<div use:portal={{ target: '#code-bottom' }} class="bottom"> <div use:portal={{ target: '#code-bottom' }} class="bottom">
{#if $state.submit !== 'idle'} {#if $state.submit !== 'idle'}
<span class="web-icon-github" in:fade /> <span class="web-icon-github" in:fade></span>
{/if} {/if}
{#if $state.submit === 'loading'} {#if $state.submit === 'loading'}
<span in:fade>Pushing to GitHub...</span> <span in:fade>Pushing to GitHub...</span>
<div class="loader is-small" in:fade /> <div class="loader is-small" in:fade></div>
{:else if $state.submit === 'success'} {:else if $state.submit === 'success'}
<span>Deployed to Appwrite Cloud</span> <span>Deployed to Appwrite Cloud</span>
<span class="web-icon-check" /> <span class="web-icon-check"></span>
{/if} {/if}
</div> </div>

View File

@@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import { flip } from '$lib/utils/flip'; import { flip } from '$lib/utils/flip';
import { crossfade, scale, slide } from 'svelte/transition'; import { scale, slide } from 'svelte/transition';
import { functionsController } from '.'; import { functionsController } from '.';
const { state } = functionsController; const { state } = functionsController;
@@ -33,7 +33,7 @@
<div data-theme-ignore class="inner-phone light"> <div data-theme-ignore class="inner-phone light">
<div class="header"> <div class="header">
<p class="title">Upgrade plan</p> <p class="title">Upgrade plan</p>
<span class="icon-menu" aria-label="menu" /> <span class="icon-menu" aria-label="menu"></span>
</div> </div>
<div class="plan"> <div class="plan">

View File

@@ -15,7 +15,7 @@
{#each $state.messages.slice(0, $state.tableSlice) as task (task.id)} {#each $state.messages.slice(0, $state.tableSlice) as task (task.id)}
<div class="row" transition:slide={{ duration: 150 }} animate:flip={{ duration: 150 }}> <div class="row" transition:slide={{ duration: 150 }} animate:flip={{ duration: 150 }}>
<div class="copy-button"> <div class="copy-button">
<span class="web-icon-copy" /> <span class="web-icon-copy"></span>
<span>{task.id}</span> <span>{task.id}</span>
</div> </div>
<div class="icon-button"> <div class="icon-button">
@@ -27,9 +27,9 @@
<div class="status-indicator"> <div class="status-indicator">
{#if task.status === 'sending'} {#if task.status === 'sending'}
<div class="loader is-small" in:fade /> <div class="loader is-small" in:fade></div>
{:else} {:else}
<span class="web-icon-check" /> <span class="web-icon-check"></span>
{/if} {/if}
</div> </div>
</div> </div>

View File

@@ -8,7 +8,7 @@
{#if $state.submit === 'success'} {#if $state.submit === 'success'}
<div class="push-notification" in:fly={{ y: -20 }}> <div class="push-notification" in:fly={{ y: -20 }}>
<div class="icon" /> <div class="icon"></div>
<div class="content"> <div class="content">
<div class="header"> <div class="header">
<h3 class="title">New task assigned to you</h3> <h3 class="title">New task assigned to you</h3>
@@ -22,7 +22,7 @@
<div data-theme-ignore class="inner-phone light"> <div data-theme-ignore class="inner-phone light">
<div class="header"> <div class="header">
<p class="title">Your tasks</p> <p class="title">Your tasks</p>
<span class="icon-menu" aria-label="menu" /> <span class="icon-menu" aria-label="menu"></span>
</div> </div>
<div class="date">Today</div> <div class="date">Today</div>
@@ -34,9 +34,9 @@
</div> </div>
{/each} {/each}
</div> </div>
<button class="add-btn"> <div class="add-btn">
<span class="web-icon-plus" /> <span class="web-icon-plus"></span>
</button> </div>
</div> </div>
<style lang="scss"> <style lang="scss">

View File

@@ -1,4 +1,4 @@
import { safeAnimate, sleep } from '$lib/animations'; import { safeAnimate } from '$lib/animations';
import { createResettable } from '$lib/utils/resettable'; import { createResettable } from '$lib/utils/resettable';
import { animate } from 'motion'; import { animate } from 'motion';
import { getElSelector } from '../Products.svelte'; import { getElSelector } from '../Products.svelte';

View File

@@ -17,7 +17,7 @@
<div class="gradient-box auth" id="post-auth-{$elId}"> <div class="gradient-box auth" id="post-auth-{$elId}">
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<p class="icon-user-group" /> <p class="icon-user-group"></p>
<p class="f-eyebrow">Authentication</p> <p class="f-eyebrow">Authentication</p>
</div> </div>
<p class="f-display mbs-16"> <p class="f-display mbs-16">
@@ -31,7 +31,7 @@
<div class="gradient-box storage" id="post-storage-{$elId}"> <div class="gradient-box storage" id="post-storage-{$elId}">
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<p class="icon-folder" /> <p class="icon-folder"></p>
<p class="f-eyebrow">Storage</p> <p class="f-eyebrow">Storage</p>
</div> </div>
<p class="f-display mbs-16"> <p class="f-display mbs-16">
@@ -55,7 +55,7 @@
<div class="gradient-box functions" id="post-functions-{$elId}"> <div class="gradient-box functions" id="post-functions-{$elId}">
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<p class="icon-lightning-bolt" /> <p class="icon-lightning-bolt"></p>
<p class="f-eyebrow">Functions</p> <p class="f-eyebrow">Functions</p>
</div> </div>
<p class="f-display mbs-16"> <p class="f-display mbs-16">
@@ -68,7 +68,7 @@
<div class="gradient-box databases" id="post-databases-{$elId}"> <div class="gradient-box databases" id="post-databases-{$elId}">
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<p class="icon-database" /> <p class="icon-database"></p>
<p class="f-eyebrow">Databases</p> <p class="f-eyebrow">Databases</p>
</div> </div>
<p class="f-display mbs-16"> <p class="f-display mbs-16">

View File

@@ -55,13 +55,13 @@
</div> </div>
{/each} {/each}
</div> </div>
<div class="vertical-sep" /> <div class="vertical-sep"></div>
<span class="icon-menu" /> <span class="icon-menu"></span>
</div> </div>
</div> </div>
<div class="row"> <div class="row">
<div class="search"> <div class="search">
<span class="web-icon-search" /> <span class="web-icon-search"></span>
<span class="text"> Search </span> <span class="text"> Search </span>
</div> </div>
<div class="flow gap-8"> <div class="flow gap-8">
@@ -81,11 +81,11 @@
<div class="title"> <div class="title">
<span class="text capitalize">{col}</span> <span class="text capitalize">{col}</span>
<span class="tgl-inline-tag">{tasks.length}</span> <span class="tgl-inline-tag">{tasks.length}</span>
<span class="icon-dots-horizontal" /> <span class="icon-dots-horizontal"></span>
</div> </div>
<div class="flow-v mbs-8 gap-12"> <div class="flow-v mbs-8 gap-12">
<button class="dashed-btn" id="add-{col}-{$elId}"> <button class="dashed-btn" id="add-{col}-{$elId}">
<span class="icon-plus" /> <span class="icon-plus"></span>
<span class="text">New Task</span> <span class="text">New Task</span>
</button> </button>
{#each tasks as task (task.title)} {#each tasks as task (task.title)}
@@ -112,7 +112,7 @@
</div> </div>
</div> </div>
{#if !isLast} {#if !isLast}
<div class="vertical-sep" /> <div class="vertical-sep"></div>
{/if} {/if}
{/each} {/each}
</div> </div>

View File

@@ -14,7 +14,7 @@
<div data-theme-ignore class="inner-phone light"> <div data-theme-ignore class="inner-phone light">
<div class="header"> <div class="header">
<p class="title">Your tasks</p> <p class="title">Your tasks</p>
<span class="icon-menu" aria-label="menu" /> <span class="icon-menu" aria-label="menu"></span>
</div> </div>
<div class="date">Today</div> <div class="date">Today</div>
@@ -27,9 +27,9 @@
{/each} {/each}
</div> </div>
<button class="add-btn"> <div class="add-btn">
<span class="web-icon-plus" /> <span class="web-icon-plus"></span>
</button> </div>
<div class="overlay" id="overlay-{$elId}"> <div class="overlay" id="overlay-{$elId}">
<div class="drawer" id="drawer-{$elId}"> <div class="drawer" id="drawer-{$elId}">
@@ -50,7 +50,7 @@
<div class="drop-zone"> <div class="drop-zone">
<span id="upload-text-{$elId}"> Drop media here </span> <span id="upload-text-{$elId}"> Drop media here </span>
<div class="loading-overlay" id="upload-loading-{$elId}"> <div class="loading-overlay" id="upload-loading-{$elId}">
<div class="loader" /> <div class="loader"></div>
</div> </div>
</div> </div>
<img id="upload-img-{$elId}" src="/images/animations/storage-2.png" alt="" /> <img id="upload-img-{$elId}" src="/images/animations/storage-2.png" alt="" />

View File

@@ -231,6 +231,20 @@ export function write(text: string, cb: (v: string) => void, duration = 500) {
}); });
} }
export function unwrite(text: string, cb: (v: string) => void, duration = 500) {
const step = duration / text.length;
let i = text.length;
return new Promise((resolve) => {
const interval = setInterval(() => {
cb(text.slice(0, --i));
if (i === 0) {
clearInterval(interval);
resolve(undefined);
}
}, step);
});
}
export function sleep(duration: number) { export function sleep(duration: number) {
return new Promise((resolve) => { return new Promise((resolve) => {
setTimeout(resolve, duration); setTimeout(resolve, duration);

View File

@@ -30,7 +30,7 @@
style:--y={`${y}px`} style:--y={`${y}px`}
style:--percentage={`${easedPercentage * 100}%`} style:--percentage={`${easedPercentage * 100}%`}
> >
<div class="absolute -top-[8px] left-1/2" /> <div class="absolute -top-[8px] left-1/2"></div>
</div> </div>
<style lang="scss"> <style lang="scss">

View File

@@ -1,5 +1,5 @@
import { PUBLIC_APPWRITE_ENDPOINT, 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, Storage } from '@appwrite.io/console';
export const client = new Client(); export const client = new Client();
@@ -7,3 +7,4 @@ client.setEndpoint(PUBLIC_APPWRITE_ENDPOINT).setProject(PUBLIC_APPWRITE_PROJECT_
export const databases = new Databases(client); export const databases = new Databases(client);
export const functions = new Functions(client); export const functions = new Functions(client);
export const storage = new Storage(client);

View File

@@ -8,12 +8,12 @@
<summary <summary
class="collapsible-button flex cursor-pointer list-none appearance-none items-center justify-between marker:hidden [&::-webkit-details-marker]:hidden" class="collapsible-button flex cursor-pointer list-none appearance-none items-center justify-between marker:hidden [&::-webkit-details-marker]:hidden"
> >
<span class="text">{title}</span> <span class="text-primary text-sub-body font-medium">{title}</span>
<div class="icon text-primary transition-transform group-[&[open]]:rotate-180"> <div class="icon text-primary transition-transform group-[&[open]]:rotate-180">
<span class="icon-cheveron-down" aria-hidden="true" /> <span class="icon-cheveron-down" aria-hidden="true"></span>
</div> </div>
</summary> </summary>
<div class="collapsible-content flex flex-col"> <div class="collapsible-content text-secondary text-sub-body flex flex-col">
<slot /> <slot />
</div> </div>
</details> </details>

View File

@@ -17,7 +17,7 @@
aria-label="close discord message" aria-label="close discord message"
on:click={hideTopBanner} on:click={hideTopBanner}
> >
<span class="web-icon-close" aria-hidden="true" /> <span class="web-icon-close" aria-hidden="true"></span>
</button> </button>
{/if} {/if}
</div> </div>

View File

@@ -0,0 +1,82 @@
<script lang="ts">
import { trackEvent } from '$lib/actions/analytics';
import { createDialog, melt } from '@melt-ui/svelte';
import { fade, scale } from 'svelte/transition';
import { Button, Icon } from '$lib/components/ui';
const {
elements: { portalled, trigger, content, overlay },
states: { open }
} = createDialog({
forceVisible: true
});
</script>
<Button
class="cursor-pointer shadow-[0_2px_40px_rgba(0,0,0,0.5)] transition-opacity hover:opacity-90 active:scale-95"
action={trigger}
onclick={() => {
trackEvent({
plausible: { name: 'Appwrite in 100 seconds' },
posthog: { name: 'intro-video-btn_hero_click' }
});
}}
>
Appwrite in 100 seconds
<Icon name="play" />
</Button>
{#if $open}
<div use:melt={$portalled}>
<div use:melt={$overlay} class="overlay" transition:fade={{ duration: 150 }}></div>
<div
class="web-media content"
use:melt={$content}
transition:scale={{ duration: 250, start: 0.95 }}
>
<iframe
src="https://www.youtube-nocookie.com/embed/L07xPMyL8sY?si=Odrwj1tHzlm12Fi2&controls=0"
title="YouTube video player"
frameborder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
allowfullscreen
></iframe>
</div>
</div>
{/if}
<style lang="scss">
.overlay {
position: fixed;
inset: 0;
background-color: rgba(0, 0, 0, 0.5);
transition: 200ms ease;
}
.content {
position: fixed;
left: 50%;
top: 50%;
translate: -50% -50%;
display: block;
object-fit: contain;
max-height: 75vh;
width: calc(80%);
aspect-ratio: 16 / 9;
z-index: 1000;
transform: scale(0.975);
transition: 200ms ease;
iframe {
display: block;
inline-size: 100%;
block-size: 100%;
}
}
</style>

View File

@@ -1,30 +1,22 @@
<script lang="ts"> <script lang="ts">
import { getAppwriteDashboardUrl } from '$lib/utils/dashboard';
import { Button } from '$lib/components/ui';
export let heading: string = 'Start building with Appwrite today'; export let heading: string = 'Start building with Appwrite today';
export let url: string = 'https://cloud.appwrite.io';
export let label: string = 'Get started'; export let label: string = 'Get started';
</script> </script>
<div class="call-to-action"> <div
<div class="details"> class="bg relative mt-12 -mb-16 flex min-h-[12rem] items-center justify-center overflow-hidden border-t border-[hsl(var(--web-color-subtle))] py-12"
<h2 class="text-label">{heading}</h2> >
<a href={url} class="web-button">{label}</a> <div class="flex max-w-3xs flex-col items-center justify-center gap-5 text-center">
<h2 class="text-label text-primary font-aeonik-pro">{heading}</h2>
<Button href={getAppwriteDashboardUrl()}>{label}</Button>
</div> </div>
</div> </div>
<style lang="scss"> <style lang="scss">
.call-to-action { .bg {
border-top: 1px solid hsl(var(--web-color-subtle));
padding: 48px 0;
min-height: 180px;
margin-top: 48px;
display: flex;
margin-bottom: -24px;
flex-direction: column;
align-items: center;
justify-content: center;
position: relative;
overflow: hidden;
&::before { &::before {
content: ''; content: '';
position: absolute; position: absolute;
@@ -36,22 +28,6 @@
rgba(253, 54, 110, 0.09), rgba(253, 54, 110, 0.09),
transparent 85% transparent 85%
); );
//filter: blur(10px);
}
.details {
gap: 20px;
max-width: 250px;
color: #fff;
text-align: center;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
h2 {
margin: 0;
}
} }
} }
</style> </style>

View File

@@ -1,8 +1,16 @@
<script lang="ts"> <script lang="ts">
import type { Snippet } from 'svelte';
let carousel: HTMLElement; let carousel: HTMLElement;
export let size: 'default' | 'medium' | 'big' = 'default'; interface Props {
export let gap = 32; size?: 'default' | 'medium' | 'big';
gap?: number;
header?: Snippet;
children: Snippet;
}
const { size = 'default', gap = 32, header, children }: Props = $props();
let scroll = 0; let scroll = 0;
function calculateScrollAmount(prev = false) { function calculateScrollAmount(prev = false) {
@@ -32,8 +40,8 @@
}); });
} }
let isEnd = false; let isEnd = $state(false);
let isStart = true; let isStart = $state(true);
function handleScroll() { function handleScroll() {
isStart = carousel.scrollLeft <= 0; isStart = carousel.scrollLeft <= 0;
@@ -43,23 +51,25 @@
<div> <div>
<div class="mt-2 flex flex-wrap items-center"> <div class="mt-2 flex flex-wrap items-center">
<slot name="header" /> {#if header}
{@render header()}
{/if}
<div class="nav ml-auto flex items-end gap-3"> <div class="nav ml-auto flex items-end gap-3">
<button <button
class="web-icon-button" class="web-icon-button cursor-pointer"
aria-label="Move carousel backward" aria-label="Move carousel backward"
disabled={isStart} disabled={isStart}
on:click={prev} onclick={prev}
> >
<span class="web-icon-arrow-left" aria-hidden="true" /> <span class="web-icon-arrow-left" aria-hidden="true"></span>
</button> </button>
<button <button
class="web-icon-button" class="web-icon-button cursor-pointer"
aria-label="Move carousel forward" aria-label="Move carousel forward"
disabled={isEnd} disabled={isEnd}
on:click={next} onclick={next}
> >
<span class="web-icon-arrow-right" aria-hidden="true" /> <span class="web-icon-arrow-right" aria-hidden="true"></span>
</button> </button>
</div> </div>
</div> </div>
@@ -71,9 +81,9 @@
class:is-big={size === 'big'} class:is-big={size === 'big'}
style:gap="{gap}px" style:gap="{gap}px"
bind:this={carousel} bind:this={carousel}
on:scroll={handleScroll} onscroll={handleScroll}
> >
<slot /> {@render children()}
</ul> </ul>
</div> </div>
</div> </div>

View File

@@ -0,0 +1,95 @@
<ul class="web-chat-list">
<li class="web-chat-item is-user-a">
<div class="web-chat-message">
<div class="web-user-box">
<img
class="web-user-box-image"
src="/images/community/avatars/walter.avif"
height="40"
width="40"
alt="Avatar of Walter"
/>
<div class="web-user-box-name flex gap-2">
<span class="text-sub-body font-medium">Walter O'Brien</span>
<time class="text-caption web-u-color-text-tertiary">8:32 AM</time>
</div>
<div class="web-user-box-content text-caption text-primary">
Hello devs! I am getting a CORS error when sending a request to the backend. Can
you help me?
</div>
</div>
</div>
</li>
<li class="web-chat-item is-user-b">
<div class="web-chat-message reply">
<div class="web-user-box">
<img
class="web-user-box-image"
src="/images/avatars/steven.avif"
width="48"
height="48"
alt="Avatar of Steven"
/>
<div class="web-user-box-name flex gap-2">
<span class="text-sub-body font-medium">Steven</span>
<time class="text-caption web-u-color-text-tertiary">8:38 AM</time>
</div>
<div class="web-user-box-content text-caption text-primary">
Hey Walter! Is this the message you get
<a class="web-link is-pink" href="/blog/post/cors-error" target="_blank"
>"Access blocked by CORS policy"</a
>?
</div>
</div>
</div>
</li>
<li class="web-chat-item is-user-a">
<div class="web-chat-message">
<div class="web-user-box">
<img
class="web-user-box-image"
src="/images/community/avatars/walter.avif"
height="40"
width="40"
alt="Avatar of Walter"
/>
<div class="web-user-box-name flex gap-2">
<span class="text-sub-body font-medium">Walter O'Brien</span>
<time class="text-caption web-u-color-text-tertiary">9:05 AM</time>
</div>
<div class="web-user-box-content text-caption text-primary">Yes!</div>
</div>
</div>
</li>
<li class="web-chat-item is-user-b">
<div class="web-chat-message reply">
<div class="web-user-box">
<img
class="web-user-box-image"
src="/images/avatars/steven.avif"
width="48"
height="48"
alt="Avatar of Steven"
/>
<div class="web-user-box-name flex gap-2">
<span class="text-sub-body font-medium">Steven</span>
<time class="text-caption web-u-color-text-tertiary">9:08 AM</time>
</div>
<div class="web-user-box-content text-caption text-primary">
You should be able to debug this with a few steps. Just follow this blog:
<a class="web-link is-pink" href="/blog/post/cors-error" target="_blank"
>https://appwrite.io/blog/post/cors-error</a
>. Let me know if this helps 🙂
</div>
</div>
</div>
</li>
</ul>
<style>
@media (max-width: 768px) {
.web-chat-list {
gap: 2rem;
}
}
</style>

View File

@@ -1,5 +1,10 @@
<script lang="ts"> <script lang="ts">
import { page } from '$app/stores'; import { onMount } from 'svelte';
import { page } from '$app/state';
import { fade } from 'svelte/transition';
import { loggedIn, user } from '$lib/utils/console';
import { PUBLIC_GROWTH_ENDPOINT } from '$env/static/public';
import { Button } from '$lib/components/ui';
export let date: string | undefined = undefined; export let date: string | undefined = undefined;
let showFeedback = false; let showFeedback = false;
@@ -13,7 +18,10 @@
async function handleSubmit() { async function handleSubmit() {
submitting = true; submitting = true;
error = undefined; error = undefined;
const response = await fetch('https://growth.appwrite.io/v1/feedback/docs', {
const userId = loggedIn && $user?.$id ? $user.$id : undefined;
const response = await fetch(`${PUBLIC_GROWTH_ENDPOINT}/feedback/docs`, {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json' 'Content-Type': 'application/json'
@@ -21,8 +29,11 @@
body: JSON.stringify({ body: JSON.stringify({
email, email,
type: feedbackType, type: feedbackType,
route: $page.route.id, route: page.route.id,
comment comment,
metaFields: {
userId
}
}) })
}); });
submitting = false; submitting = false;
@@ -32,6 +43,7 @@
} }
comment = email = ''; comment = email = '';
submitted = true; submitted = true;
setTimeout(() => (showFeedback = false), 500);
} }
function reset() { function reset() {
@@ -44,6 +56,12 @@
$: if (!showFeedback) { $: if (!showFeedback) {
reset(); reset();
} }
onMount(() => {
if (loggedIn && $user?.email) {
email = $user?.email;
}
});
</script> </script>
<section class="web-content-footer"> <section class="web-content-footer">
@@ -58,23 +76,23 @@
<button <button
class="web-radio-button" class="web-radio-button"
aria-label="helpful" aria-label="helpful"
on:click={() => { onclick={() => {
showFeedback = feedbackType === 'positive' ? false : true; showFeedback = feedbackType !== 'positive';
feedbackType = 'positive'; feedbackType = 'positive';
}} }}
> >
<span class="icon-thumb-up" /> <span class="icon-thumb-up"></span>
</button> </button>
<button <button
class="web-radio-button" class="web-radio-button"
aria-label="unhelpful" aria-label="unhelpful"
on:click={() => { onclick={() => {
showFeedback = feedbackType === 'negative' ? false : true; showFeedback = feedbackType !== 'negative';
feedbackType = 'negative'; feedbackType = 'negative';
}} }}
> >
<!-- TODO: fix the icon name on pink --> <!-- TODO: fix the icon name on pink -->
<span class="icon-thumb-dowm" /> <span class="icon-thumb-dowm"></span>
</button> </button>
</div> </div>
</div> </div>
@@ -85,12 +103,12 @@
{/if} {/if}
<li> <li>
<a <a
href={`https://github.com/appwrite/website/tree/main/src/routes${$page.route.id}`} href={`https://github.com/appwrite/website/tree/main/src/routes${page.route.id}`}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
class="web-link flex items-baseline gap-1" class="web-link flex items-baseline gap-1"
> >
<span class="icon-pencil-alt contents" aria-hidden="true" /> <span class="icon-pencil-alt contents" aria-hidden="true"></span>
<span>Update on GitHub</span> <span>Update on GitHub</span>
</a> </a>
</li> </li>
@@ -100,9 +118,13 @@
</header> </header>
{#if showFeedback} {#if showFeedback}
<form <form
on:submit|preventDefault={handleSubmit} onsubmit={(e) => {
e.preventDefault();
handleSubmit();
}}
class="web-card is-normal" class="web-card is-normal"
style="--card-padding:1rem" style="--card-padding:1rem"
out:fade={{ duration: 450 }}
> >
<div class="flex flex-col gap-2"> <div class="flex flex-col gap-2">
<label for="message"> <label for="message">
@@ -115,7 +137,7 @@
id="message" id="message"
placeholder="Write your message" placeholder="Write your message"
bind:value={comment} bind:value={comment}
/> ></textarea>
<label for="message" class="mt-2"> <label for="message" class="mt-2">
<span class="text-primary">Email</span> <span class="text-primary">Email</span>
</label> </label>
@@ -140,12 +162,8 @@
{/if} {/if}
<div class="mt-4 flex justify-end gap-2"> <div class="mt-4 flex justify-end gap-2">
<button class="web-button is-text" on:click={() => (showFeedback = false)}> <Button variant="text" onclick={() => (showFeedback = false)}>Cancel</Button>
<span>Cancel</span> <Button type="submit" disabled={submitting || !email}>Submit</Button>
</button>
<button type="submit" class="web-button" disabled={submitting || !email}>
<span>Submit</span>
</button>
</div> </div>
</form> </form>
{/if} {/if}

View File

@@ -32,11 +32,11 @@
{ label: 'Solid', href: '/docs/quick-starts/solid' } { label: 'Solid', href: '/docs/quick-starts/solid' }
], ],
Products: [ Products: [
{ label: 'Auth', href: '/docs/products/auth' }, { label: 'Auth', href: '/products/auth' },
{ label: 'Databases', href: '/docs/products/databases' }, { label: 'Databases', href: '/docs/products/databases' },
{ label: 'Functions', href: '/docs/products/functions' }, { label: 'Functions', href: '/products/functions' },
{ label: 'Messaging', href: '/products/messaging' }, { label: 'Messaging', href: '/products/messaging' },
{ label: 'Storage', href: '/docs/products/storage' }, { label: 'Storage', href: '/products/storage' },
{ label: 'Realtime', href: '/docs/apis/realtime' } { label: 'Realtime', href: '/docs/apis/realtime' }
], ],
Learn: [ Learn: [
@@ -69,7 +69,8 @@
Programs: [ Programs: [
{ label: 'Heroes', href: '/heroes' }, { label: 'Heroes', href: '/heroes' },
{ label: 'Startups', href: '/startups' }, { label: 'Startups', href: '/startups' },
{ label: 'Students', href: '/students' } { label: 'Education', href: '/education' },
{ label: 'Partners', href: '/partners' }
], ],
About: [ About: [
{ label: 'Company', href: '/company' }, { label: 'Company', href: '/company' },
@@ -128,7 +129,7 @@
class="web-icon-chevron-down web-u-transition" class="web-icon-chevron-down web-u-transition"
class:web-u-rotate-180={$isSelected(title)} class:web-u-rotate-180={$isSelected(title)}
style:font-size="1rem" style:font-size="1rem"
/> ></span>
</button> </button>
</h5> </h5>
{#if $isSelected(title)} {#if $isSelected(title)}

View File

@@ -1,5 +1,5 @@
<script lang="ts"> <script lang="ts">
import { page } from '$app/stores'; import { page } from '$app/state';
import { onMount } from 'svelte'; import { onMount } from 'svelte';
let mounted = false; let mounted = false;
@@ -28,7 +28,7 @@
const randomDelay = () => Math.floor(Math.random() * 750); const randomDelay = () => Math.floor(Math.random() * 750);
</script> </script>
<div class="banner" class:hidden={$page.url.pathname.includes('init')}> <div class="banner" class:hidden={page.url.pathname.includes('init')}>
<div class="content text-primary"> <div class="content text-primary">
<div class="headings"> <div class="headings">
<span style:font-weight="500" <span style:font-weight="500"
@@ -39,16 +39,16 @@
> has started > has started
</span> </span>
<span class="web-u-color-text-secondary">The start of something new</span> <span class="web-u-color-text-secondary">The start of something new</span>
<div class="shadow" /> <div class="shadow"></div>
</div> </div>
<a href="/init" rel="noopener noreferrer" class="action"> <a href="/init" rel="noopener noreferrer" class="action">
<span class="text-caption font-medium">Join now</span> <span class="text-caption font-medium">Join now</span>
<span class="web-icon-arrow-right" aria-hidden="true" /> <span class="web-icon-arrow-right" aria-hidden="true"></span>
<div class="shadow" /> <div class="shadow"></div>
</a> </a>
</div> </div>
<div class="shine" /> <div class="shine"></div>
<div class="border" /> <div class="border"></div>
<div class="lines"> <div class="lines">
{#if mounted} {#if mounted}
{#each Array.from({ length: groups.length }) as _, i} {#each Array.from({ length: groups.length }) as _, i}
@@ -57,7 +57,7 @@
<div <div
class="line" class="line"
style={`--width:${getRandomWidth(index)}px;--initial-delay:${randomDelay()}ms;left:${getRandomXValue()}px;`} style={`--width:${getRandomWidth(index)}px;--initial-delay:${randomDelay()}ms;left:${getRandomXValue()}px;`}
/> ></div>
{/each} {/each}
</div> </div>
{/each} {/each}
@@ -111,7 +111,8 @@
backdrop-filter: blur(6px); backdrop-filter: blur(6px);
background-color: hsl(var(--web-color-background) / 50%); background-color: hsl(var(--web-color-background) / 50%);
mask-composite: intersect; mask-composite: intersect;
mask-image: linear-gradient( mask-image:
linear-gradient(
to top, to top,
transparent, transparent,
rgba(0, 0, 0, 1) 25%, rgba(0, 0, 0, 1) 25%,
@@ -148,7 +149,8 @@
backdrop-filter: blur(6px); backdrop-filter: blur(6px);
background-color: hsl(var(--web-color-background) / 50%); background-color: hsl(var(--web-color-background) / 50%);
mask-composite: intersect; mask-composite: intersect;
mask-image: linear-gradient( mask-image:
linear-gradient(
to top, to top,
transparent, transparent,
rgba(0, 0, 0, 1) 25%, rgba(0, 0, 0, 1) 25%,

View File

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

View File

@@ -84,10 +84,8 @@
> >
{title} {title}
</h2> </h2>
<ul <ul class="grid grid-cols-3 gap-10 pt-20 text-center md:grid-cols-6">
class="web-u-padding-block-start-80 grid grid-cols-3 text-center md:grid-cols-6 md:gap-10" {#each logos as { src, alt, width, height }}
>
{#each logos as { src, alt, width, height }, i}
<li class="grid place-content-center"> <li class="grid place-content-center">
<img {src} {alt} {width} {height} /> <img {src} {alt} {width} {height} />
</li> </li>

View File

@@ -19,7 +19,7 @@
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
> >
<span class={social.icon} aria-hidden="true" /> <span class={social.icon} aria-hidden="true"></span>
</a> </a>
</li> </li>
{/each} {/each}
@@ -28,7 +28,7 @@
<div>Copyright © {year} Appwrite</div> <div>Copyright © {year} Appwrite</div>
<iframe <iframe
class="status w-fit max-w-[230px]" class="status w-full md:w-fit md:max-w-[230px]"
title="Appwrite Status" title="Appwrite Status"
src="https://status.appwrite.online/badge?theme=dark" src="https://status.appwrite.online/badge?theme=dark"
height="35" height="35"
@@ -36,7 +36,7 @@
scrolling="no" scrolling="no"
style:color-scheme="none" style:color-scheme="none"
style:margin-top="-4px" style:margin-top="-4px"
/> ></iframe>
<ul class="flex gap-4"> <ul class="flex gap-4">
<li><a class="web-link" href="/terms">Terms</a></li> <li><a class="web-link" href="/terms">Terms</a></li>
@@ -58,7 +58,7 @@
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
> >
<span class={social.icon} aria-hidden="true" /> <span class={social.icon} aria-hidden="true"></span>
</a> </a>
</li> </li>
{/each} {/each}
@@ -92,6 +92,13 @@
display: grid; display: grid;
} }
@media #{devices.$break1} {
.status {
height: 55px;
margin-bottom: 6px; /* balancing due to style:margin-top="-4px" & the `iframe` has some spacings too I think */
}
}
.e-main-footer { .e-main-footer {
display: flex; display: flex;
@media #{devices.$break1} { @media #{devices.$break1} {

View File

@@ -0,0 +1,79 @@
<script lang="ts" context="module">
import type { Component } from 'svelte';
export type NavLink = {
label: string;
href?: string;
showBadge?: boolean;
submenu?: Component<{ label: string }>;
mobileSubmenu?: Component<{ label: string }>;
};
</script>
<script lang="ts">
import { classNames } from '$lib/utils/classnames';
import { trackEvent } from '$lib/actions/analytics';
export let initialized = false;
export let links: NavLink[] = [];
</script>
<nav class="web-main-header-nav" aria-label="Main">
<ul class="web-main-header-nav-list flex items-center">
{#each links as link}
<li class="web-main-header-nav-item text-primary hover:text-accent">
{#if link.submenu}
<div
class="web-main-header-nav-item-button"
aria-haspopup="true"
aria-expanded="false"
aria-controls="submenu"
data-submenu-button
>
<svelte:component this={link.submenu} label={link.label} />
</div>
{:else}
<a
class={classNames(
'data-[badge]:after:animate-scale-in data-[badge]:relative data-[badge]:after:absolute data-[badge]:after:size-1.5 data-[badge]:after:translate-full data-[badge]:after:rounded-full'
)}
href={link.href}
data-initialized={initialized ? '' : undefined}
data-badge={link.showBadge ? '' : undefined}
on:click={() => {
trackEvent({
plausible: { name: `${link.label} in header` },
posthog: { name: `${link.label.toLowerCase()}_nav_click` }
});
}}
>{link.label}
</a>
{/if}
</li>
{/each}
</ul>
</nav>
<style>
[data-badge] {
position: relative;
&::after {
content: '';
position: absolute;
background-color: hsl(var(--color-accent));
border-radius: 100%;
width: 0.375rem;
height: 0.375rem;
inset-block-start: -2px;
inset-inline-end: -4px;
translate: 100%;
}
&:not([data-initialized])::after {
animation: scale-in 0.2s ease-out;
}
}
</style>

View File

@@ -1,8 +1,11 @@
<script lang="ts"> <script lang="ts">
import { afterNavigate } from '$app/navigation'; import { afterNavigate } from '$app/navigation';
import { IsLoggedIn } from '$lib/components'; import { IsLoggedIn } from '$lib/components';
import { GITHUB_REPO_LINK, GITHUB_STARS } from '$lib/constants'; import { SOCIAL_STATS } from '$lib/constants';
import type { NavLink } from '$lib/layouts/Main.svelte'; import type { NavLink } from './MainNav.svelte';
import { getAppwriteDashboardUrl } from '$lib/utils/dashboard';
import { Button, InlineTag, Icon } from '$lib/components/ui';
import { GithubStats } from '$lib/components/shared';
export let open = false; export let open = false;
export let links: NavLink[]; export let links: NavLink[];
@@ -16,36 +19,31 @@
<nav class="web-side-nav web-is-not-desktop" class:hidden={!open}> <nav class="web-side-nav web-is-not-desktop" class:hidden={!open}>
<div class="web-side-nav-wrapper ps-4 pe-4"> <div class="web-side-nav-wrapper ps-4 pe-4">
<div class="flex items-center gap-2"> <div class="flex items-center gap-2 px-4">
<a href="https://cloud.appwrite.io/register" class="web-button is-secondary flex-1"> <Button href={getAppwriteDashboardUrl('/register')} variant="secondary" class="flex-1">
Sign up Sign up
</a> </Button>
<IsLoggedIn classes="flex-1" /> <IsLoggedIn class="flex-1" />
</div> </div>
<div class="web-side-nav-scroll"> <div class="web-side-nav-scroll">
<section> <section>
<ul> <ul>
{#each links as { href, label }} {#each links as { href, label, mobileSubmenu }}
<li> <li>
<a class="web-side-nav-button" {href}> {#if mobileSubmenu}
<span class="text-caption">{label}</span> <svelte:component this={mobileSubmenu} {label} />
</a> {:else}
<a class="web-side-nav-button" {href}>
<span class="text-caption">{label}</span>
</a>
{/if}
</li> </li>
{/each} {/each}
</ul> </ul>
</section> </section>
</div> </div>
<div class="web-side-nav-mobile-footer-buttons"> <div class="web-side-nav-mobile-footer-buttons">
<a <GithubStats class="w-full! md:w-fit" />
href={GITHUB_REPO_LINK}
target="_blank"
rel="noopener noreferrer"
class="web-button is-text web-u-inline-width-100-percent-mobile"
>
<span class="web-icon-star" aria-hidden="true" />
<span class="text">Star on GitHub</span>
<span class="web-inline-tag text-sub-body">{GITHUB_STARS}</span>
</a>
</div> </div>
</div> </div>
</nav> </nav>

View File

@@ -0,0 +1,116 @@
<script lang="ts">
import { Select, Tooltip } from '$lib/components';
import { getCodeHtml, type Language } from '$lib/utils/code';
import { copy } from '$lib/utils/copy';
import { platformMap } from '$lib/utils/references';
import { writable } from 'svelte/store';
interface Props {
selected?: Language;
data?: { language: string; content: string; platform?: string }[];
width?: number | null;
height?: number | null;
}
let { selected = $bindable('js'), data = [], width = null, height = null }: Props = $props();
let snippets = $derived(writable(new Set(data.map((d) => d.language))));
let content = $derived(data.find((d) => d.language === selected)?.content ?? '');
let platform = $derived(data.find((d) => d.language === selected)?.platform ?? '');
snippets?.subscribe((n) => {
if (selected === null && n.size > 0) {
selected = Array.from(n)[0] as Language;
}
});
const CopyStatus = {
Copy: 'Copy',
Copied: 'Copied!'
} as const;
type CopyStatusType = keyof typeof CopyStatus;
type CopyStatusValue = (typeof CopyStatus)[CopyStatusType];
let copyText: CopyStatusValue = CopyStatus.Copy;
async function handleCopy() {
await copy(content);
copyText = CopyStatus.Copied;
setTimeout(() => {
copyText = CopyStatus.Copy;
}, 1000);
}
let result = $derived(
getCodeHtml({
content,
language: selected ?? 'sh',
withLineNumbers: true
})
);
let options = $derived(
Array.from($snippets).map((language) => ({
value: language,
label: platformMap[language]
}))
);
</script>
<section
class="dark web-code-snippet mx-auto lg:!max-w-[90vw]"
aria-label="code-snippet panel"
style={`width: ${width ? width / 16 + 'rem' : 'inherit'}; height: ${
height ? height / 16 + 'rem' : 'inherit'
}`}
>
<header class="web-code-snippet-header">
<div class="web-code-snippet-header-start">
<div class="flex gap-4">
{#if platform}
<div class="web-tag"><span class="text">{platform}</span></div>
{/if}
</div>
</div>
<div class="web-code-snippet-header-end">
<ul class="buttons-list flex gap-3">
{#if $snippets.entries.length}
<li class="buttons-list-item flex self-center">
<Select bind:value={selected} bind:options />
</li>
{/if}
<li class="buttons-list-item" style="padding-inline-start: 13px">
<Tooltip>
<button
onclick={handleCopy}
class="web-icon-button"
aria-label="copy code from code-snippet"
><span class="web-icon-copy" aria-hidden="true"></span></button
>
{#snippet tooltip()}
<span>
{copyText}
</span>
{/snippet}
</Tooltip>
</li>
</ul>
</div>
</header>
<div
class="web-code-snippet-content overflow-auto"
style={`height: ${height ? height / 16 + 'rem' : 'inherit'}`}
>
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
{@html result}
</div>
</section>
<style>
/* system breaks the corners */
.overflow-auto::-webkit-scrollbar {
display: none;
}
</style>

View File

@@ -1,14 +1,16 @@
<script context="module" lang="ts"> <script context="module" lang="ts">
import { PUBLIC_GROWTH_ENDPOINT } from '$env/static/public';
import { Button } from '$lib/components/ui';
export async function newsletter(name: string, email: string) { export async function newsletter(name: string, email: string) {
const response = await fetch('https://growth.appwrite.io/v1/newsletter/subscribe', { const response = await fetch(`${PUBLIC_GROWTH_ENDPOINT}/newsletter/subscribe`, {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json' 'Content-Type': 'application/json'
}, },
body: JSON.stringify({ body: JSON.stringify({
name, name,
email, email
cloud: true /* not optional on the growth endpoint. */
}) })
}); });
return response; return response;
@@ -90,9 +92,9 @@
class:max-w-[380px]={!submitted} class:max-w-[380px]={!submitted}
> >
<section class="web-gap-5 flex flex-col"> <section class="web-gap-5 flex flex-col">
<h1 class="text-title font-aeonik-pro text-primary"> <h2 class="text-title font-aeonik-pro text-primary">
Subscribe to our newsletter Subscribe to our newsletter
</h1> </h2>
<p class="text-description web-u-padding-block-end-40"> <p class="text-description web-u-padding-block-end-40">
Sign up to our company blog and get the latest insights from Sign up to our company blog and get the latest insights from
Appwrite. Learn more about engineering, product design, building Appwrite. Learn more about engineering, product design, building
@@ -165,9 +167,7 @@
bind:value={email} bind:value={email}
/> />
</div> </div>
<button type="submit" class="web-button" disabled={submitting} <Button type="submit" disabled={submitting}>Sign up</Button>
>Sign up</button
>
{#if error} {#if error}
<span class="text"> <span class="text">
Something went wrong. Please try again later. Something went wrong. Please try again later.

View File

@@ -1,25 +1,87 @@
<script lang="ts"> <script lang="ts">
import { PUBLIC_APPWRITE_DASHBOARD } from '$env/static/public';
import { trackEvent } from '$lib/actions/analytics'; import { trackEvent } from '$lib/actions/analytics';
import { getAppwriteDashboardUrl } from '$lib/utils/dashboard';
import { Button, type Variant } from '$lib/components/ui';
const plans: Array<{
name: string;
price: string;
description: string;
variable?: boolean;
tag?: string;
buttonText: string;
buttonLink: string;
buttonVariant: Variant;
eventName: string;
}> = [
{
name: 'Free',
price: '$0',
description: 'A great fit for passion projects and small applications.',
buttonText: 'Get started',
buttonLink: getAppwriteDashboardUrl('/register'),
buttonVariant: 'secondary',
eventName: 'Get started Free plan'
},
{
name: 'Pro',
price: '$15',
variable: true,
tag: 'Most Popular',
description:
'For production applications that need powerful functionality and resources to scale.',
buttonText: 'Start building',
buttonLink: getAppwriteDashboardUrl('/console?type=create&plan=tier-1'),
buttonVariant: 'primary',
eventName: 'Get started Pro plan'
},
{
name: 'Scale',
price: '$599',
variable: true,
description:
'For teams that handle more complex and large projects and need more control and support.',
buttonText: 'Start building',
buttonLink: getAppwriteDashboardUrl('/console?type=create&plan=tier-2'),
buttonVariant: 'secondary',
eventName: 'Get started Scale plan'
},
{
name: 'Enterprise',
price: 'Custom',
description: 'For enterprises that need more power and premium support.',
buttonText: 'Contact us',
buttonLink: '/contact-us/enterprise',
buttonVariant: 'secondary',
eventName: 'Get started Enterprise plan'
}
];
</script> </script>
<img src="/images/bgs/pre-footer.png" alt="" class="web-pre-footer-bg" style="z-index:-1" /> <img
src="/images/bgs/pre-footer.png"
alt=""
class="web-pre-footer-bg"
loading="lazy"
style="z-index:-1"
/>
<div class="web-u-row-gap-80 relative grid gap-8 md:grid-cols-2"> <div class="web-u-row-gap-80 relative grid gap-8 md:grid-cols-2">
<section class="web-hero flex items-center justify-center gap-y-8"> <section class="web-hero flex items-center justify-center gap-y-8">
<h2 class="text-display font-aeonik-pro text-primary max-w-[500px] text-center"> <h2 class="text-display font-aeonik-pro text-primary max-w-[500px] text-center">
Start building today Start building with Appwrite today
</h2> </h2>
<a <Button
href={PUBLIC_APPWRITE_DASHBOARD} variant="transparent"
class="web-button is-transparent web-self-center" href={getAppwriteDashboardUrl()}
on:click={() => trackEvent('Get started in pre footer')} class="self-center"
onclick={() => trackEvent({ plausible: { name: 'Get started in pre footer' } })}
> >
<span class="text">Get started</span> <span class="text">Get started</span>
</a> </Button>
</section> </section>
<section <section
class="web-card is-transparent has-border-gradient web-u-max-inline-width-584-mobile web-mx-auto-mobile web-u-inline-width-100-percent-mobile" class="web-card is-transparent has-border-gradient web-u-max-inline-width-584-mobile web-mx-auto-mobile web-u-inline-width-100-percent-mobile p-8!"
> >
<header class="web-strip-plans-header"> <header class="web-strip-plans-header">
<div class="web-strip-plans-header-wrapper web-u-row-gap-24"> <div class="web-strip-plans-header-wrapper web-u-row-gap-24">
@@ -27,84 +89,50 @@
</div> </div>
</header> </header>
<ul class="web-strip-plans"> <ul class="web-strip-plans -mt-8">
<li class="web-strip-plans-item web-strip-plans-container-query"> {#each plans as plan}
<div class="web-strip-plans-item-wrapper"> <li class="web-strip-plans-item web-strip-plans-container-query">
<div class="web-strip-plans-plan"> <div class="place-item-end grid grid-cols-1 gap-6 md:grid-cols-3">
<h4 class="title text-description">Free</h4> <div class="flex flex-col">
<div class="text-title font-aeonik-pro text-primary">$0</div> <div class="flex gap-3">
<div class="info text-caption font-medium" /> <h4 class="title text-description">{plan.name}</h4>
{#if plan.tag}<div class="web-inline-tag is-pink text-sub-body">
Most popular
</div>{/if}
</div>
<div class="mt-4 flex flex-col">
{#if plan.variable}<span>From</span>{/if}
<div class="flex items-end gap-2">
<div class="text-title font-aeonik-pro text-primary">
{plan.price}
</div>
{#if plan.variable}
<div class="info text-caption font-medium">/month</div>
{/if}
</div>
</div>
</div>
<p class="web-strip-plans-info text-caption self-end font-medium">
{plan.description}
</p>
<Button
variant={plan.buttonVariant}
href={plan.buttonLink}
class="w-full! flex-3 self-end md:w-fit"
onclick={() =>
trackEvent({
plausible: {
name: plan.eventName
}
})}
>
<span class="text" style:padding-inline="0.5rem">{plan.buttonText}</span
>
</Button>
</div> </div>
<p class="web-strip-plans-info text-caption font-medium"> </li>
For personal hobby projects and students. {/each}
</p>
<a
href={`${PUBLIC_APPWRITE_DASHBOARD}/register`}
class="web-button is-secondary is-full-width-mobile web-u-cross-child-end"
on:click={() => trackEvent('Get started Free plan')}
>
<span class="text">Get started</span>
</a>
</div>
</li>
<li class="web-strip-plans-item web-strip-plans-container-query">
<div class="web-strip-plans-item-wrapper">
<div class="web-strip-plans-plan">
<h4 class="title text-description">Pro</h4>
<div class="text-title font-aeonik-pro text-primary">$15</div>
<div class="info text-caption font-medium">per member/month</div>
</div>
<p class="web-strip-plans-info text-caption font-medium">
For pro developers and teams that need to scale their products.
</p>
<a
href={`${PUBLIC_APPWRITE_DASHBOARD}/console?type=createPro`}
class="web-button is-full-width-mobile web-u-cross-child-end"
target="_blank"
rel="noopener noreferrer"
on:click={() => trackEvent('Get started Pro plan')}
>
<!-- <span class="text">Start trial</span> -->
<span class="text">Start building</span>
</a>
</div>
</li>
<li class="web-strip-plans-item web-strip-plans-container-query">
<div class="web-strip-plans-item-wrapper">
<div class="web-strip-plans-plan">
<h4 class="title text-description">Scale</h4>
<div class="text-title font-aeonik-pro text-primary">$599</div>
<div class="info text-caption font-medium">per org/month</div>
</div>
<p class="web-strip-plans-info text-caption font-medium">
For pro developers and production projects that need the ability to scale.
</p>
<button
class="web-button is-full-width-mobile is-secondary web-u-cross-child-end"
disabled
>
<span class="text">Coming soon</span>
</button>
</div>
</li>
<li class="web-strip-plans-item web-strip-plans-container-query">
<div class="web-strip-plans-item-wrapper">
<div class="web-strip-plans-plan">
<h4 class="title web-description">Enterprise</h4>
<div class="web-title web-u-color-text-primary">Custom</div>
<div class="info web-caption-500">per org/month</div>
</div>
<p class="web-strip-plans-info web-caption-500">
For enterprises that need more power and premium support.
</p>
<a
href="/contact-us/enterprise"
class="web-button is-secondary is-full-width-mobile web-u-cross-child-end"
>
<span class="text">Contact us</span>
</a>
</div>
</li>
</ul> </ul>
</section> </section>
</div> </div>
@@ -114,10 +142,6 @@
flex-basis: 5rem !important; flex-basis: 5rem !important;
} }
.web-strip-plans .web-button {
flex: 3;
}
.web-strip-plans-item-wrapper { .web-strip-plans-item-wrapper {
gap: 2.65rem; gap: 2.65rem;
} }
@@ -152,5 +176,6 @@
height: auto; height: auto;
max-inline-size: unset; max-inline-size: unset;
max-block-size: unset; max-block-size: unset;
filter: blur(100px);
} }
</style> </style>

View File

@@ -0,0 +1,92 @@
<script lang="ts">
import { classNames } from '$lib/utils/classnames';
import { melt, createCollapsible } from '@melt-ui/svelte';
import { slide } from 'svelte/transition';
import { products, sublinks } from './ProductsSubmenu.svelte';
import { trackEvent } from '$lib/actions/analytics';
export let label: string;
const {
elements: { root, content, trigger },
states: { open }
} = createCollapsible();
</script>
<div use:melt={$root} class="relative mx-auto block md:hidden">
<div class="flex items-center justify-between">
<button
use:melt={$trigger}
class="text-caption web-side-nav-button flex items-center justify-between"
>{label}
<span
class={classNames('web-icon-chevron-down transition-transform', {
'rotate-180': $open
})}
></span></button
>
</div>
<div>
{#if $open}
<div use:melt={$content} transition:slide class="px-2 py-3">
<div class="flex flex-col gap-2">
{#each products as product}
<a
href={product.href}
class="group flex gap-3 rounded-xl p-2 text-white transition-colors outline-none focus:bg-white/8"
on:click={() =>
trackEvent({
plausible: {
name: `${product.name} in products submenu`
}
})}
>
<div
class="flex size-12 shrink-0 items-center justify-center rounded-lg border border-white/12 bg-white/6"
>
<img
src={product.icon}
alt={product.name}
class="size-6 grayscale transition-all group-focus:grayscale-0"
/>
</div>
<div class="">
<span class="text-sub-body text-primary font-medium"
>{product.name}
{#if product.beta}
<span
class="text-caption bg-accent/24 ml-1 rounded px-2 py-1 font-medium text-white"
>Coming soon</span
>
{/if}
</span>
<p class="text-caption text-secondary text-pretty">
{product.description}
</p>
</div>
</a>
{/each}
</div>
<div class="mt-8">
<span
class="font-aeonik-fono tracking-loose text-secondary block text-xs uppercase"
>Compare Appwrite<span class="text-accent">_</span></span
>
<div class="mt-3 space-y-3">
{#each sublinks as sublink}
<a
href={sublink.href}
class="text-caption text-primary flex items-center gap-2"
>
{sublink.label} <span class="web-icon-chevron-right"></span>
</a>
{/each}
</div>
</div>
</div>
{/if}
</div>
</div>

View File

@@ -0,0 +1,217 @@
<script lang="ts" context="module">
export type SubmenuItem = {
name: string;
href: string;
description: string;
icon: string;
beta?: boolean;
};
export type SubLink = {
label: string;
href: string;
};
export const products: Array<SubmenuItem> = [
{
name: 'Auth',
href: '/products/auth',
description: 'Secure login with multi-factor auth.',
icon: '/images/icons/illustrated/dark/auth.png'
},
{
name: 'Databases',
href: '/docs/products/databases',
description: 'Scalable and robust databases.',
icon: '/images/icons/illustrated/dark/databases.png'
},
{
name: 'Storage',
href: '/products/storage',
description: 'Advanced compression and encryption.',
icon: '/images/icons/illustrated/dark/storage.png'
},
{
name: 'Functions',
href: '/products/functions',
description: 'Deploy & scale serverless functions.',
icon: '/images/icons/illustrated/dark/functions.png'
},
{
name: 'Messaging',
href: '/products/messaging',
description: 'Set up a full-functioning messaging service.',
icon: '/images/icons/illustrated/dark/messaging.png'
},
{
name: 'Realtime',
href: '/docs/apis/realtime',
description: 'Subscribe and react to any event.',
icon: '/images/icons/illustrated/dark/realtime.png'
}
];
export const sublinks: Array<SubLink> = [
{
label: 'Appwrite vs. Supabase',
href: '/blog/post/appwrite-compared-to-supabase'
},
{
label: 'Appwrite vs. Firebase',
href: '/blog/post/open-source-firebase-alternative'
}
];
</script>
<script lang="ts">
import { dev } from '$app/environment';
import { trackEvent } from '$lib/actions/analytics';
import { classNames } from '$lib/utils/classnames';
import { createDropdownMenu, melt } from '@melt-ui/svelte';
const {
elements: { trigger, menu, item, overlay },
states: { open }
} = createDropdownMenu({
loop: true
});
export let label: string;
</script>
<button
class={classNames(
'text-primary focus:text-accent hover:text-accent inline-flex cursor-pointer items-center justify-between outline-none',
{
'text-accent': $open
}
)}
use:melt={$trigger}
>
{label}
<span
class={classNames('web-icon-chevron-down block transition-transform', {
'rotate-180': $open
})}
></span>
</button>
<div
use:melt={$menu}
class={classNames(
'data-[state=closed]:animate-fade-out data-[state=open]:animate-fade-in relative !left-1/2 z-10 mx-auto mt-6 hidden w-full -translate-x-1/2 flex-col items-center p-0 outline-none [max-inline-size:86.875rem] md:flex'
)}
>
<div class="is-special-padding w-full rounded-2xl border border-white/8 bg-[#232325] p-6">
<div class="grid w-full grid-cols-1 place-content-between gap-16 lg:grid-cols-12">
<div class="col-span-8 -mr-12 pr-12">
<span
class="font-aeonik-fono text-secondary tracking-loose mb-4 block text-xs uppercase"
>{label}<span class="text-accent">_</span></span
>
<div
class="grid grid-flow-col-dense grid-cols-1 gap-2 md:grid-cols-2 md:grid-rows-4"
>
{#each products as product}
<a
href={product.href}
use:melt={$item}
on:click={() =>
trackEvent({
plausible: {
name: `${product.name} in products submenu`
}
})}
class="group flex gap-3 rounded-xl p-1 text-white transition-colors outline-none focus:bg-white/8"
>
<div
class="flex size-12 shrink-0 items-center justify-center rounded-lg border border-white/12 bg-white/6"
>
<img
src={product.icon}
alt={product.name}
class="size-6 grayscale transition-all group-focus:grayscale-0"
/>
</div>
<div class="">
<span class="text-sub-body text-primary font-medium"
>{product.name}
{#if product.beta}
<span
class="text-caption bg-accent/24 ml-1 rounded px-2 py-1 font-medium text-white"
>Coming soon</span
>
{/if}
</span>
<p class="text-caption text-secondary text-pretty">
{product.description}
</p>
</div>
</a>
{/each}
</div>
</div>
<div class="col-span-4 -ml-12 border-l border-white/6 pl-12">
<div
use:melt={$item}
class="group block rounded-2xl border border-white/12 bg-white/6 p-4 outline-none focus-within:bg-white/12"
>
<header class="flex items-center justify-between">
<span
class="font-aeonik-fono tracking-loose text-secondary block text-xs uppercase"
>Customer Stories<span class="text-accent">_</span></span
>
<a
href="/blog/category/customer-stories"
class="text-primary text-caption flex items-center gap-2"
>See more <span
class="web-icon-chevron-right transition-transform group-hover:translate-x-0.5"
></span></a
>
</header>
<a
href="/blog/post/customer-story-storealert"
class="my-4 flex flex-1 gap-3 outline-none"
>
<img
src="/images/blog/customer-story-storealert/cover.png"
alt="Case study cover"
class="aspect-[3/1] max-w-[7.5rem] shrink-0 rounded-xl object-cover"
/>
<p class="text-pretty">
Empowering Shopify merchants with real-time store monitoring using
StoreAlert
</p>
</a>
</div>
<div class="mt-8">
<span
class="font-aeonik-fono tracking-loose text-secondary block text-xs uppercase"
>Compare Appwrite<span class="text-accent">_</span></span
>
<div class="mt-3 space-y-3">
{#each sublinks as sublink}
<a
href={sublink.href}
class="text-caption text-primary group flex items-center gap-1"
>
{sublink.label}
<span
class="web-icon-chevron-right transition-transform group-hover:translate-x-0.5"
></span>
</a>
{/each}
</div>
</div>
</div>
</div>
</div>
<div
use:melt={$overlay}
class="data-[state=closed]:animate-fade-out fixed inset-0 bg-black/60"
></div>
</div>

View File

@@ -15,7 +15,7 @@
const client = new MeiliSearch({ const client = new MeiliSearch({
host: 'https://search.appwrite.org', host: 'https://search.appwrite.org',
apiKey: 'd7e83e21c0daf2a471ef4c463c7872e55b91b0cd02e2d20e9c6f6f1c4cd09ed3' apiKey: '10a5fea149bfaff21ef4d7cbe7f8a09d4fab404d6c3510279a365e065f8955a7'
}); });
const index = client.index<Props>('website'); const index = client.index<Props>('website');
@@ -70,12 +70,12 @@
const recommended: Hits<Props> = [ const recommended: Hits<Props> = [
{ {
uid: 'recommended-references-account', uid: 'recommended-references-account',
url: '/docs/references/cloud/client-web/databases', url: '/docs/references/cloud/client-web/account',
h1: 'API reference', h1: 'API reference',
h2: 'Databases' h2: 'Account'
}, },
{ {
uid: 'recommended-references-teans', uid: 'recommended-references-teams',
url: '/docs/references/cloud/client-web/teams', url: '/docs/references/cloud/client-web/teams',
h1: 'API reference', h1: 'API reference',
h2: 'Teams' h2: 'Teams'
@@ -160,11 +160,12 @@
on:click={handleExit} on:click={handleExit}
> >
<div class="web-input-text-search-wrapper web-u-margin-inline-20 w-full max-w-[680px]"> <div class="web-input-text-search-wrapper web-u-margin-inline-20 w-full max-w-[680px]">
<span class="web-icon-search z-[5]" aria-hidden="true" style="inset-block-start:0.9rem" /> <span class="web-icon-search z-[5]" aria-hidden="true" style="inset-block-start:0.9rem"
<div id="searchbox" /> ></span>
<div id="searchbox"></div>
<input <input
class="web-input-button relative z-1 !rounded-b-none !pl-10" class="web-input-button bg-greyscale-800/75! relative z-1 !rounded-b-none !pl-10"
type="text" type="text"
id="search" id="search"
bind:value bind:value
@@ -222,7 +223,7 @@
</div> </div>
{#if hit.p} {#if hit.p}
<div <div
class="web-u-color-text-secondary w-full overflow-hidden text-ellipsis whitespace-nowrap text-left" class="web-u-color-text-secondary w-full overflow-hidden text-left text-ellipsis whitespace-nowrap"
> >
{hit.p} {hit.p}
</div> </div>

View File

@@ -8,6 +8,7 @@
</script> </script>
<script lang="ts"> <script lang="ts">
import { classNames } from '$lib/utils/classnames';
import { createSelect, melt, type CreateSelectProps } from '@melt-ui/svelte'; import { createSelect, melt, type CreateSelectProps } from '@melt-ui/svelte';
import { createEventDispatcher } from 'svelte'; import { createEventDispatcher } from 'svelte';
import { fly, type FlyParams } from 'svelte/transition'; import { fly, type FlyParams } from 'svelte/transition';
@@ -21,11 +22,16 @@
export let id: string | undefined = undefined; export let id: string | undefined = undefined;
export let preventScroll = false; export let preventScroll = false;
export let placement: NonNullable<CreateSelectProps['positioning']>['placement'] = 'bottom'; export let placement: NonNullable<CreateSelectProps['positioning']>['placement'] = 'bottom';
let className: string = '';
export { className as class };
const dispatch = createEventDispatcher<{ const dispatch = createEventDispatcher<{
change: unknown; change: unknown;
}>(); }>();
export let initialLabel: string = 'Select an option';
const { const {
elements: { trigger, menu, option: optionEl, group: groupEl, groupLabel }, elements: { trigger, menu, option: optionEl, group: groupEl, groupLabel },
states: { open, selected, selectedLabel } states: { open, selected, selectedLabel }
@@ -83,7 +89,7 @@
</script> </script>
<button <button
class="web-select is-colored" class={classNames('web-select is-colored', className)}
{id} {id}
class:web-is-not-mobile={nativeMobile} class:web-is-not-mobile={nativeMobile}
use:melt={$trigger} use:melt={$trigger}
@@ -91,11 +97,11 @@
> >
<div class="physical-select"> <div class="physical-select">
{#if selectedOption?.icon} {#if selectedOption?.icon}
<span class={selectedOption.icon} aria-hidden="true" /> <span class={selectedOption.icon} aria-hidden="true"></span>
{/if} {/if}
<span>{$selectedLabel}</span> <span>{$selectedLabel || initialLabel}</span>
</div> </div>
<span class="icon-cheveron-{$open ? 'up' : 'down'}" aria-hidden="true" /> <span class="icon-cheveron-{$open ? 'up' : 'down'}" aria-hidden="true"></span>
</button> </button>
{#if $open} {#if $open}
@@ -113,9 +119,9 @@
{#each group.options as option} {#each group.options as option}
<button class="web-select-option" use:melt={$optionEl(option)}> <button class="web-select-option" use:melt={$optionEl(option)}>
{#if option.icon} {#if option.icon}
<span class={option.icon} aria-hidden="true" /> <span class={option.icon} aria-hidden="true"></span>
{/if} {/if}
<span style:text-transform="capitalize">{option.label}</span> <span>{option.label}</span>
</button> </button>
{/each} {/each}
</div> </div>
@@ -128,7 +134,7 @@
{#each group.options as option} {#each group.options as option}
<button class="web-select-option" use:melt={$optionEl(option)}> <button class="web-select-option" use:melt={$optionEl(option)}>
{#if option.icon} {#if option.icon}
<span class={option.icon} aria-hidden="true" /> <span class={option.icon} aria-hidden="true"></span>
{/if} {/if}
<span style:text-transform="capitalize">{option.label}</span> <span style:text-transform="capitalize">{option.label}</span>
</button> </button>
@@ -144,7 +150,7 @@
style:display={nativeMobile ? undefined : 'none'} style:display={nativeMobile ? undefined : 'none'}
> >
{#if selectedOption?.icon} {#if selectedOption?.icon}
<span class={selectedOption.icon} aria-hidden="true" /> <span class={selectedOption.icon} aria-hidden="true"></span>
{/if} {/if}
<select {id} bind:value> <select {id} bind:value>
{#each groups as group} {#each groups as group}
@@ -166,7 +172,7 @@
{/if} {/if}
{/each} {/each}
</select> </select>
<span class="icon-cheveron-{$open ? 'up' : 'down'}" aria-hidden="true" /> <span class="icon-cheveron-{$open ? 'up' : 'down'}" aria-hidden="true"></span>
</div> </div>
<style lang="scss"> <style lang="scss">

View File

@@ -18,7 +18,7 @@
<div class="melt-switch"> <div class="melt-switch">
<button use:melt={$root}> <button use:melt={$root}>
<span class="thumb" /> <span class="thumb"></span>
</button> </button>
</div> </div>

View File

@@ -75,7 +75,7 @@
<a <a
href={platform.href} href={platform.href}
class="web-icon-button web-box-icon has-border-gradient" class="web-icon-button web-box-icon has-border-gradient"
on:click={() => trackEvent(`${platform.name} clicked`)} on:click={() => trackEvent({ plausible: { name: `${platform.name} clicked` } })}
> >
<img <img
src={platform.image} src={platform.image}
@@ -85,7 +85,9 @@
/> />
</a> </a>
</li> </li>
<svelte:fragment slot="tooltip">{platform.name}</svelte:fragment> {#snippet tooltip()}
{platform.name}
{/snippet}
</Tooltip> </Tooltip>
{/each} {/each}
</ul> </ul>

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