mirror of
https://github.com/appwrite/console.git
synced 2026-06-06 19:27:48 +00:00
@@ -1,10 +0,0 @@
|
||||
src/sdk.ts
|
||||
.DS_Store
|
||||
node_modules
|
||||
/build
|
||||
/.svelte-kit
|
||||
/package
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
/playwright-report
|
||||
@@ -1,43 +0,0 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
extends: [
|
||||
'eslint:recommended',
|
||||
'plugin:@typescript-eslint/recommended',
|
||||
'plugin:svelte/recommended',
|
||||
'prettier'
|
||||
],
|
||||
parser: '@typescript-eslint/parser',
|
||||
parserOptions: {
|
||||
extraFileExtensions: ['.svelte'], // This is a required setting in `@typescript-eslint/parser` v4.24.0.
|
||||
sourceType: 'module',
|
||||
ecmaVersion: 2020
|
||||
},
|
||||
ignorePatterns: ['*.cjs'],
|
||||
overrides: [
|
||||
{
|
||||
files: ['*.svelte'],
|
||||
parser: 'svelte-eslint-parser',
|
||||
parserOptions: {
|
||||
parser: '@typescript-eslint/parser' //This is required to parse TypeScript inside Svelte files.
|
||||
}
|
||||
}
|
||||
],
|
||||
rules: {
|
||||
'no-redeclare': 'off',
|
||||
'@typescript-eslint/no-duplicate-enum-values': 'off',
|
||||
'svelte/no-at-html-tags': 'off',
|
||||
'no-unused-vars': 'off', // This rule is handled by `@typescript-eslint/no-unused-vars` so it needs to be disabled to avoid conflicts.
|
||||
'@typescript-eslint/no-unused-vars': [
|
||||
'error',
|
||||
{ argsIgnorePattern: '^_', varsIgnorePattern: '^(_|\\$\\$)' } // Ignore unused variables starting with `_` or `$$`.
|
||||
]
|
||||
},
|
||||
env: {
|
||||
browser: true,
|
||||
es2017: true,
|
||||
node: true
|
||||
},
|
||||
globals: {
|
||||
globalThis: false // false means it is not writeable
|
||||
}
|
||||
};
|
||||
@@ -24,7 +24,6 @@ dist/
|
||||
*.swp
|
||||
### Node ###
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
|
||||
+23
-23
@@ -108,9 +108,9 @@ pnpm run lint
|
||||
|
||||
Diagnostic tool that checks for the following:
|
||||
|
||||
- Unused CSS
|
||||
- Svelte A11y hints
|
||||
- TypeScript compiler errors
|
||||
- Unused CSS
|
||||
- Svelte A11y hints
|
||||
- TypeScript compiler errors
|
||||
|
||||
```bash
|
||||
pnpm run check
|
||||
@@ -130,11 +130,11 @@ doc-548-submit-a-pull-request-section-to-contribution-guide
|
||||
|
||||
When `TYPE` can be:
|
||||
|
||||
- **feat** - is a new feature
|
||||
- **doc** - documentation only changes
|
||||
- **cicd** - changes related to CI/CD system
|
||||
- **fix** - a bug fix
|
||||
- **refactor** - code change that neither fixes a bug nor adds a feature
|
||||
- **feat** - is a new feature
|
||||
- **doc** - documentation only changes
|
||||
- **cicd** - changes related to CI/CD system
|
||||
- **fix** - a bug fix
|
||||
- **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!**
|
||||
|
||||
@@ -175,12 +175,12 @@ $ git push origin [name_of_your_new_branch]
|
||||
|
||||
Before committing always make sure to run all available tools to improve the codebase:
|
||||
|
||||
- Formatter
|
||||
- `pnpm run format`
|
||||
- Tests
|
||||
- `pnpm test`
|
||||
- Diagnostics
|
||||
- `pnpm run check`
|
||||
- Formatter
|
||||
- `pnpm run format`
|
||||
- Tests
|
||||
- `pnpm test`
|
||||
- Diagnostics
|
||||
- `pnpm run check`
|
||||
|
||||
### Performance
|
||||
|
||||
@@ -188,9 +188,9 @@ Page load times are a key consideration for users of all browsers and device typ
|
||||
|
||||
There are some general things we can do in front-end development:
|
||||
|
||||
- Minimize HTTP requests
|
||||
- Minimize blocking – content should be readable before client-side processing
|
||||
- Lazy load "supplementary" content, especially images
|
||||
- Minimize HTTP requests
|
||||
- Minimize blocking – content should be readable before client-side processing
|
||||
- Lazy load "supplementary" content, especially images
|
||||
|
||||
### Don't Repeat Yourself (DRY)
|
||||
|
||||
@@ -202,12 +202,12 @@ If you stick to this principle, you will ensure that you will only ever need to
|
||||
|
||||
Separate _structure_ from _presentation_ from _behavior_ to aid maintainability and understanding.
|
||||
|
||||
- Keep CSS (presentation), JS (behavior) and HTML (structure) in the same respective Svelte component
|
||||
- Avoid writing inline CSS or Javascript in HTML
|
||||
- Avoid writing CSS or HTML in Javascript
|
||||
- Don't choose HTML elements to imply style
|
||||
- Where appropriate, use CSS or Svelte rather than Javascript for animations and transitions
|
||||
- Try to use templates when defining markup in Javascript
|
||||
- Keep CSS (presentation), JS (behavior) and HTML (structure) in the same respective Svelte component
|
||||
- Avoid writing inline CSS or Javascript in HTML
|
||||
- Avoid writing CSS or HTML in Javascript
|
||||
- Don't choose HTML elements to imply style
|
||||
- Where appropriate, use CSS or Svelte rather than Javascript for animations and transitions
|
||||
- Try to use templates when defining markup in Javascript
|
||||
|
||||
### Write code to be read
|
||||
|
||||
|
||||
+1
-1
@@ -37,7 +37,7 @@ ENV SENTRY_AUTH_TOKEN=$SENTRY_AUTH_TOKEN
|
||||
ENV SENTRY_RELEASE=$SENTRY_RELEASE
|
||||
ENV NODE_OPTIONS=--max_old_space_size=8192
|
||||
|
||||
RUN pnpm run sync && pnpm run build
|
||||
RUN pnpm run build
|
||||
|
||||
FROM nginx:1.26.3-alpine
|
||||
|
||||
|
||||
@@ -10,8 +10,8 @@
|
||||
|
||||
Appwrite Console has been built with the following frameworks:
|
||||
|
||||
- [Svelte](https://svelte.dev/)
|
||||
- [Svelte Kit](https://kit.svelte.dev/)
|
||||
- [Svelte](https://svelte.dev/)
|
||||
- [Svelte Kit](https://kit.svelte.dev/)
|
||||
|
||||
## Developer Experience
|
||||
|
||||
|
||||
@@ -10,7 +10,6 @@ export function registerUserStep(page: Page): Promise<Metadata> {
|
||||
return test.step('register user', async () => {
|
||||
const seed = crypto.randomUUID();
|
||||
await page.goto('./register');
|
||||
// await page.getByRole('button', { name: 'only required' }).click();
|
||||
const inputs = {
|
||||
name: page.locator('id=name'),
|
||||
email: page.locator('id=email'),
|
||||
@@ -25,9 +24,9 @@ export function registerUserStep(page: Page): Promise<Metadata> {
|
||||
await inputs.name.fill(values.name);
|
||||
await inputs.email.fill(values.email);
|
||||
await inputs.password.fill(values.password);
|
||||
await inputs.terms.check();
|
||||
await inputs.terms.check({ force: true });
|
||||
await page.getByRole('button', { name: 'Sign up', exact: true }).click();
|
||||
await page.waitForURL('./onboarding');
|
||||
await page.waitForURL('./onboarding/create-project');
|
||||
|
||||
return values;
|
||||
});
|
||||
@@ -9,10 +9,6 @@ type Metadata = {
|
||||
export async function createFreeProject(page: Page): Promise<Metadata> {
|
||||
const organizationId = await test.step('create organization', async () => {
|
||||
await page.goto('./');
|
||||
await page.waitForURL('./onboarding');
|
||||
await page.locator('id=name').fill('test org');
|
||||
await page.locator('id=plan').selectOption('tier-0');
|
||||
await page.getByRole('button', { name: 'get started' }).click();
|
||||
await page.waitForURL(/\/organization-[^/]+/);
|
||||
return getOrganizationIdFromUrl(page.url());
|
||||
});
|
||||
@@ -20,12 +16,11 @@ export async function createFreeProject(page: Page): Promise<Metadata> {
|
||||
const projectId = await test.step('create project', async () => {
|
||||
await page.waitForURL(/\/organization-[^/]+/);
|
||||
await page.getByRole('button', { name: 'create project' }).first().click();
|
||||
await page.locator('id=name').fill('test project');
|
||||
await page.getByRole('button', { name: 'next' }).click();
|
||||
await page.locator('label').filter({ hasText: 'Frankfurt' }).click();
|
||||
await page.getByRole('button', { name: 'create' }).click();
|
||||
await page.waitForURL(/\/project-(?:[a-z0-9]+-)?([^/]+)\/overview\/platforms/);
|
||||
expect(page.url()).toContain('/console/project-');
|
||||
const dialog = page.locator('dialog[open]');
|
||||
await dialog.getByPlaceholder('Project name').fill('test project');
|
||||
await dialog.getByRole('button', { name: 'create' }).click();
|
||||
await page.waitForURL(/\/project-fra-[^/]+/);
|
||||
expect(page.url()).toContain('/console/project-fra-');
|
||||
|
||||
return getProjectIdFromUrl(page.url());
|
||||
});
|
||||
@@ -27,12 +27,10 @@ export async function enterCreditCard(page: Page) {
|
||||
|
||||
export async function createProProject(page: Page): Promise<Metadata> {
|
||||
const organizationId = await test.step('create organization', async () => {
|
||||
await page.goto('./');
|
||||
await page.waitForURL('./onboarding');
|
||||
await page.goto('./create-organization');
|
||||
await page.locator('id=name').fill('test org');
|
||||
await page.locator('id=plan').selectOption('tier-1');
|
||||
await page.getByLabel('pro').check();
|
||||
await page.getByRole('button', { name: 'get started' }).click();
|
||||
await page.waitForURL(/\/create-organization.*/);
|
||||
await page.getByRole('button', { name: 'add' }).first().click();
|
||||
await enterCreditCard(page);
|
||||
// skip members
|
||||
@@ -45,12 +43,11 @@ export async function createProProject(page: Page): Promise<Metadata> {
|
||||
const projectId = await test.step('create project', async () => {
|
||||
await page.waitForURL(/\/organization-[^/]+/);
|
||||
await page.getByRole('button', { name: 'create project' }).first().click();
|
||||
await page.getByPlaceholder('project name').fill('test project');
|
||||
await page.getByRole('button', { name: 'next' }).click();
|
||||
await page.locator('label').filter({ hasText: 'frankfurt' }).click();
|
||||
await page.getByRole('button', { name: 'create' }).click();
|
||||
await page.waitForURL(/\/project-(?:[a-z0-9]+-)?([^/]+)\/overview\/platforms/);
|
||||
expect(page.url()).toContain('/project-');
|
||||
const dialog = page.locator('dialog[open]');
|
||||
await dialog.getByPlaceholder('Project name').fill('test project');
|
||||
await dialog.getByRole('button', { name: 'create' }).click();
|
||||
await page.waitForURL(/\/project-fra-[^/]+/);
|
||||
expect(page.url()).toContain('/console/project-fra-');
|
||||
|
||||
return getProjectIdFromUrl(page.url());
|
||||
});
|
||||
@@ -0,0 +1,51 @@
|
||||
import prettier from 'eslint-config-prettier';
|
||||
import js from '@eslint/js';
|
||||
import { includeIgnoreFile } from '@eslint/compat';
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
+53
-36
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"name": "@appwrite/console",
|
||||
"type": "module",
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
},
|
||||
@@ -7,74 +8,90 @@
|
||||
"dev": "vite dev",
|
||||
"build": "node build.js",
|
||||
"preview": "vite preview",
|
||||
"sync": "svelte-kit sync",
|
||||
"check": "svelte-check --tsconfig ./tsconfig.json --fail-on-warnings --threshold warning",
|
||||
"prepare": "svelte-kit sync || echo ''",
|
||||
"clean": "rm -rf node_modules && rm -rf .svelte_kit && pnpm i --force",
|
||||
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
||||
"check:watch": "svelte-check --tsconfig ./tsconfig.json --watch",
|
||||
"lint": "prettier --check . && eslint .",
|
||||
"format": "prettier --write .",
|
||||
"lint": "prettier --check . && eslint .",
|
||||
"test": "TZ=EST vitest run",
|
||||
"test:ui": "TZ=EST vitest --ui",
|
||||
"test:watch": "TZ=EST vitest watch",
|
||||
"e2e": "playwright test tests/e2e",
|
||||
"e2e:ui": "playwright test tests/e2e --ui"
|
||||
"e2e": "playwright test",
|
||||
"e2e:ui": "playwright test --ui"
|
||||
},
|
||||
"dependencies": {
|
||||
"@appwrite.io/console": "https://pkg.pr.new/appwrite-labs/cloud/@appwrite.io/console@c985738",
|
||||
"@ai-sdk/svelte": "^1.1.24",
|
||||
"@appwrite.io/console": "https://pkg.pr.new/appwrite-labs/cloud/@appwrite.io/console@1959",
|
||||
"@appwrite.io/pink": "0.25.0",
|
||||
"@appwrite.io/pink-icons": "0.25.0",
|
||||
"@appwrite.io/pink-icons-svelte": "https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-icons-svelte@d68e5e4",
|
||||
"@appwrite.io/pink-legacy": "^1.0.3",
|
||||
"@appwrite.io/pink-svelte": "https://pkg.pr.new/appwrite/pink/@appwrite.io/pink-svelte@b7fa532",
|
||||
"@popperjs/core": "^2.11.8",
|
||||
"@sentry/sveltekit": "^8.38.0",
|
||||
"@stripe/stripe-js": "^3.5.0",
|
||||
"@ai-sdk/svelte": "^1.1.24",
|
||||
"analytics": "^0.8.14",
|
||||
"ai": "^2.2.37",
|
||||
"analytics": "^0.8.16",
|
||||
"cron-parser": "^4.9.0",
|
||||
"dayjs": "^1.11.13",
|
||||
"deep-equal": "^2.2.3",
|
||||
"echarts": "^5.5.1",
|
||||
"envfile": "^7.1.0",
|
||||
"nanoid": "^5.0.8",
|
||||
"echarts": "^5.6.0",
|
||||
"ignore": "^6.0.2",
|
||||
"nanoid": "^5.1.5",
|
||||
"nanotar": "^0.1.1",
|
||||
"plausible-tracker": "^0.3.9",
|
||||
"pretty-bytes": "^6.1.1",
|
||||
"prismjs": "^1.29.0",
|
||||
"prismjs": "^1.30.0",
|
||||
"remarkable": "^2.0.1",
|
||||
"svelte-confetti": "^1.4.0",
|
||||
"tippy.js": "^6.3.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/compat": "^1.2.7",
|
||||
"@eslint/js": "^9.24.0",
|
||||
"@melt-ui/pp": "^0.3.2",
|
||||
"@melt-ui/svelte": "^0.83.0",
|
||||
"@playwright/test": "^1.49.0",
|
||||
"@sveltejs/adapter-static": "^3.0.6",
|
||||
"@sveltejs/kit": "^2.8.1",
|
||||
"@sveltejs/vite-plugin-svelte": "^3.1.2",
|
||||
"@melt-ui/svelte": "^0.86.5",
|
||||
"@playwright/test": "^1.51.1",
|
||||
"@sveltejs/adapter-static": "^3.0.8",
|
||||
"@sveltejs/kit": "^2.20.2",
|
||||
"@sveltejs/vite-plugin-svelte": "^5.0.3",
|
||||
"@testing-library/dom": "^10.4.0",
|
||||
"@testing-library/jest-dom": "^6.6.3",
|
||||
"@testing-library/svelte": "^5.2.4",
|
||||
"@testing-library/user-event": "^14.5.2",
|
||||
"@testing-library/user-event": "^14.6.1",
|
||||
"@types/deep-equal": "^1.0.4",
|
||||
"@types/prismjs": "^1.26.5",
|
||||
"@types/remarkable": "^2.0.8",
|
||||
"@typescript-eslint/eslint-plugin": "^7.18.0",
|
||||
"@typescript-eslint/parser": "^7.18.0",
|
||||
"@vitest/ui": "^1.6.0",
|
||||
"eslint": "^8.57.1",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-plugin-svelte": "^2.46.0",
|
||||
"jsdom": "^22.1.0",
|
||||
"@typescript-eslint/eslint-plugin": "^8.28.0",
|
||||
"@typescript-eslint/parser": "^8.28.0",
|
||||
"@vitest/ui": "^3.0.9",
|
||||
"eslint": "^9.23.0",
|
||||
"eslint-config-prettier": "^10.1.0",
|
||||
"eslint-plugin-svelte": "^3.3.3",
|
||||
"globals": "^16.0.0",
|
||||
"jsdom": "^26.0.0",
|
||||
"kleur": "^4.1.5",
|
||||
"prettier": "^3.3.3",
|
||||
"prettier-plugin-svelte": "^3.2.8",
|
||||
"sass": "^1.81.0",
|
||||
"svelte": "^4.2.19",
|
||||
"svelte-check": "^3.8.6",
|
||||
"svelte-jester": "^2.3.2",
|
||||
"prettier": "^3.5.3",
|
||||
"prettier-plugin-svelte": "^3.3.3",
|
||||
"sass": "^1.86.0",
|
||||
"svelte": "^5.25.3",
|
||||
"svelte-check": "^4.1.5",
|
||||
"svelte-preprocess": "^6.0.3",
|
||||
"svelte-sequential-preprocessor": "^2.0.2",
|
||||
"tslib": "^2.8.1",
|
||||
"typescript": "^5.6.3",
|
||||
"vite": "^5.4.11",
|
||||
"vitest": "^1.6.1"
|
||||
"typescript": "^5.8.2",
|
||||
"typescript-eslint": "^8.30.1",
|
||||
"vite": "^6.2.3",
|
||||
"vitest": "^3.0.0"
|
||||
},
|
||||
"type": "module",
|
||||
"packageManager": "pnpm@9.7.0+sha512.dc09430156b427f5ecfc79888899e1c39d2d690f004be70e05230b72cb173d96839587545d09429b55ac3c429c801b4dc3c0e002f653830a420fa2dd4e3cf9cf"
|
||||
"pnpm": {
|
||||
"onlyBuiltDependencies": [
|
||||
"@parcel/watcher",
|
||||
"@sentry/cli",
|
||||
"esbuild",
|
||||
"svelte-preprocess"
|
||||
]
|
||||
},
|
||||
"packageManager": "pnpm@10.7.0+sha512.6b865ad4b62a1d9842b61d674a393903b871d9244954f652b8842c2b553c72176b278f64c463e52d40fff8aba385c235c8c9ecf5cc7de4fd78b8bb6d49633ab6"
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ const config: PlaywrightTestConfig = {
|
||||
reportSlowTests: null,
|
||||
reporter: [['html', { open: 'never' }]],
|
||||
retries: 3,
|
||||
testDir: 'e2e',
|
||||
use: {
|
||||
baseURL: 'http://localhost:4173/console/',
|
||||
trace: 'on-first-retry'
|
||||
|
||||
Generated
+3305
-3848
File diff suppressed because it is too large
Load Diff
-124
@@ -5,133 +5,9 @@
|
||||
<meta
|
||||
name="description"
|
||||
content="Appwrite is an open-source platform for building applications at any scale, using your preferred programming languages and tools." />
|
||||
|
||||
<link rel="icon" type="image/svg+xml" href="/console/logos/appwrite-icon.svg" />
|
||||
<link rel="mask-icon" type="image/png" href="/console/logos/appwrite-icon.png" />
|
||||
|
||||
<link
|
||||
rel="preload"
|
||||
href="/console/fonts/inter/inter-v8-latin-600.woff2"
|
||||
as="font"
|
||||
type="font/woff2"
|
||||
crossorigin />
|
||||
<link
|
||||
rel="preload"
|
||||
href="/console/fonts/inter/inter-v8-latin-regular.woff2"
|
||||
as="font"
|
||||
type="font/woff2"
|
||||
crossorigin />
|
||||
<link
|
||||
rel="preload"
|
||||
href="/console/fonts/poppins/poppins-v19-latin-500.woff2"
|
||||
as="font"
|
||||
type="font/woff2"
|
||||
crossorigin />
|
||||
<link
|
||||
rel="preload"
|
||||
href="/console/fonts/poppins/poppins-v19-latin-600.woff2"
|
||||
as="font"
|
||||
type="font/woff2"
|
||||
crossorigin />
|
||||
<link
|
||||
rel="preload"
|
||||
href="/console/fonts/poppins/poppins-v19-latin-700.woff2"
|
||||
as="font"
|
||||
type="font/woff2"
|
||||
crossorigin />
|
||||
<link
|
||||
rel="preload"
|
||||
href="/console/fonts/source-code-pro/source-code-pro-v20-latin-regular.woff2"
|
||||
as="font"
|
||||
type="font/woff2"
|
||||
crossorigin />
|
||||
<link
|
||||
rel="preload"
|
||||
href="https://fonts.appwrite.io/aeonik-pro/AeonikPro-Air.woff2"
|
||||
as="font"
|
||||
type="font/woff2"
|
||||
crossorigin />
|
||||
<link
|
||||
rel="preload"
|
||||
href="https://fonts.appwrite.io/aeonik-pro/AeonikPro-AirItalic.woff2"
|
||||
as="font"
|
||||
type="font/woff2"
|
||||
crossorigin />
|
||||
<link
|
||||
rel="preload"
|
||||
href="https://fonts.appwrite.io/aeonik-pro/AeonikPro-Thin.woff2"
|
||||
as="font"
|
||||
type="font/woff2"
|
||||
crossorigin />
|
||||
<link
|
||||
rel="preload"
|
||||
href="https://fonts.appwrite.io/aeonik-pro/AeonikPro-ThinItalic.woff2"
|
||||
as="font"
|
||||
type="font/woff2"
|
||||
crossorigin />
|
||||
<link
|
||||
rel="preload"
|
||||
href="https://fonts.appwrite.io/aeonik-pro/AeonikPro-Light.woff2"
|
||||
as="font"
|
||||
type="font/woff2"
|
||||
crossorigin />
|
||||
<link
|
||||
rel="preload"
|
||||
href="https://fonts.appwrite.io/aeonik-pro/AeonikPro-LightItalic.woff2"
|
||||
as="font"
|
||||
type="font/woff2"
|
||||
crossorigin />
|
||||
<link
|
||||
rel="preload"
|
||||
href="https://fonts.appwrite.io/aeonik-pro/AeonikPro-Regular.woff2"
|
||||
as="font"
|
||||
type="font/woff2"
|
||||
crossorigin />
|
||||
<link
|
||||
rel="preload"
|
||||
href="https://fonts.appwrite.io/aeonik-pro/AeonikPro-RegularItalic.woff2"
|
||||
as="font"
|
||||
type="font/woff2"
|
||||
crossorigin />
|
||||
<link
|
||||
rel="preload"
|
||||
href="https://fonts.appwrite.io/aeonik-pro/AeonikPro-Medium.woff2"
|
||||
as="font"
|
||||
type="font/woff2"
|
||||
crossorigin />
|
||||
<link
|
||||
rel="preload"
|
||||
href="https://fonts.appwrite.io/aeonik-pro/AeonikPro-MediumItalic.woff2"
|
||||
as="font"
|
||||
type="font/woff2"
|
||||
crossorigin />
|
||||
<link
|
||||
rel="preload"
|
||||
href="https://fonts.appwrite.io/aeonik-pro/AeonikPro-Bold.woff2"
|
||||
as="font"
|
||||
type="font/woff2"
|
||||
crossorigin />
|
||||
<link
|
||||
rel="preload"
|
||||
href="https://fonts.appwrite.io/aeonik-pro/AeonikPro-BoldItalic.woff2"
|
||||
as="font"
|
||||
type="font/woff2"
|
||||
crossorigin />
|
||||
<link
|
||||
rel="preload"
|
||||
href="https://fonts.appwrite.io/aeonik-pro/AeonikPro-Black.woff2"
|
||||
as="font"
|
||||
type="font/woff2"
|
||||
crossorigin />
|
||||
<link
|
||||
rel="preload"
|
||||
href="https://fonts.appwrite.io/aeonik-pro/AeonikPro-BlackItalic.woff2"
|
||||
as="font"
|
||||
type="font/woff2"
|
||||
crossorigin />
|
||||
<link rel="preload" as="style" type="text/css" href="/console/fonts/main.css" />
|
||||
<link rel="stylesheet" href="/console/css/loading.css" />
|
||||
<link rel="stylesheet" href="/console/fonts/main.css" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
%sveltekit.head%
|
||||
</head>
|
||||
|
||||
@@ -13,6 +13,7 @@ Sentry.init({
|
||||
|
||||
export const handleError: HandleClientError = Sentry.handleErrorWithSentry(
|
||||
async ({ error, message, status }) => {
|
||||
console.error(error);
|
||||
if (error instanceof AppwriteException) {
|
||||
status = error.code === 0 ? undefined : error.code;
|
||||
message = error.message;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import Analytics, { type AnalyticsPlugin } from 'analytics';
|
||||
import Plausible from 'plausible-tracker';
|
||||
import { get } from 'svelte/store';
|
||||
import { page } from '$app/stores';
|
||||
import { page } from '$app/state';
|
||||
import { user } from '$lib/stores/user';
|
||||
import { ENV, MODE, VARS, isCloud } from '$lib/system';
|
||||
import { AppwriteException } from '@appwrite.io/console';
|
||||
@@ -56,13 +56,12 @@ export function trackEvent(name: string, data: object = null): void {
|
||||
return;
|
||||
}
|
||||
|
||||
const currentPage = get(page);
|
||||
const path = currentPage.route.id;
|
||||
const path = page.route.id;
|
||||
|
||||
if (currentPage.params?.project) {
|
||||
if (page.params?.project) {
|
||||
data = {
|
||||
...data,
|
||||
project: currentPage.params.project
|
||||
project: page.params.project
|
||||
};
|
||||
}
|
||||
|
||||
@@ -141,6 +140,64 @@ export function isTrackingAllowed() {
|
||||
}
|
||||
}
|
||||
|
||||
export enum Click {
|
||||
BackupCopyIdClick = 'click_backup_copy_id',
|
||||
BackupDeleteClick = 'click_backup_delete',
|
||||
BackupRestoreClick = 'click_backup_restore',
|
||||
BreadcrumbClick = 'click_breadcrumb',
|
||||
ConnectRepositoryClick = 'click_connect_repository',
|
||||
CreditsRedeemClick = 'click_credits_redeem',
|
||||
CloudSignupClick = 'click_cloud_signup',
|
||||
DatabaseAttributeDelete = 'click_attribute_delete',
|
||||
DatabaseIndexDelete = 'click_index_delete',
|
||||
DatabaseCollectionDelete = 'click_collection_delete',
|
||||
DatabaseDatabaseDelete = 'click_database_delete',
|
||||
DatabaseImportCsv = 'click_database_import_csv',
|
||||
DomainCreateClick = 'click_domain_create',
|
||||
DomainDeleteClick = 'click_domain_delete',
|
||||
DomainRetryDomainVerificationClick = 'click_domain_retry_domain_verification',
|
||||
FeedbackSubmitClick = 'click_leave_feedback',
|
||||
FilterApplyClick = 'click_apply_filter',
|
||||
FunctionsRedeployClick = 'click_function_redeploy',
|
||||
FunctionsDeploymentDeleteClick = 'click_deployment_delete',
|
||||
FunctionsDeploymentCancelClick = 'click_deployment_cancel',
|
||||
KeyCreateClick = 'click_key_create',
|
||||
DevKeyCreateClick = 'click_dev_key_create',
|
||||
MenuDropDownClick = 'click_menu_dropdown',
|
||||
MenuOverviewClick = 'click_menu_overview',
|
||||
ModalCloseClick = 'click_close_modal',
|
||||
MessagingScheduleClick = 'click_messaging_schedule',
|
||||
MessagingTopicCreateClick = 'click_messaging_topic_create',
|
||||
MessagingTargetCreateClick = 'click_messaging_target_create',
|
||||
MembershipDeleteClick = 'click_delete_membership',
|
||||
PlatformCreateClick = 'click_platform_create',
|
||||
OrganizationClickCreate = 'click_create_organization',
|
||||
OrganizationClickUpgrade = 'click_organization_upgrade',
|
||||
OnboardingSetupDatabaseClick = 'click_onboarding_setup_database',
|
||||
OnboardingApiReferencesClick = 'click_onboarding_api_references',
|
||||
OnboardingTutorialsClick = 'click_onboarding_tutorials',
|
||||
OnboardingStorageQuickstartClick = 'click_onboarding_storage_quickstart',
|
||||
OnboardingFunctionsQuickstartClick = 'click_onboarding_functions_quickstart',
|
||||
OnboardingAuthEmailPasswordClick = 'click_onboarding_auth_email_password',
|
||||
OnboardingAuthOauth2Click = 'click_onboarding_auth_oauth2',
|
||||
OnboardingAuthAllMethodsClick = 'click_onboarding_auth_all_methods',
|
||||
OnboardingDiscordClick = 'click_onboarding_discord',
|
||||
StorageBucketDeleteClick = 'click_bucket_delete',
|
||||
SettingsWebhookUpdateSignatureClick = 'click_webhook_update_signature',
|
||||
SettingsWebhookDeleteClick = 'click_webhook_delete',
|
||||
SettingsInstallProviderClick = 'click_install_provider',
|
||||
SettingsStartMigrationClick = 'click_start_migration',
|
||||
SubmitFormClick = 'click_submit_form',
|
||||
ShowCustomIdClick = 'click_show_custom_id',
|
||||
SupportOpenClick = 'click_open_support_menu',
|
||||
PromoClick = 'click_promo',
|
||||
PolicyDeleteClick = 'click_policy_delete',
|
||||
VariablesCreateClick = 'click_variable_create',
|
||||
VariablesUpdateClick = 'click_variable_update',
|
||||
VariablesImportClick = 'click_variable_import',
|
||||
WebsiteOpenClick = 'click_open_website'
|
||||
}
|
||||
|
||||
export enum Submit {
|
||||
DownloadDPA = 'submit_download_dpa',
|
||||
Error = 'submit_error',
|
||||
@@ -161,6 +218,9 @@ export enum Submit {
|
||||
AccountRecoveryCodesCreate = 'submit_account_recovery_codes_create',
|
||||
AccountRecoveryCodesUpdate = 'submit_account_recovery_codes_update',
|
||||
AccountDeleteIdentity = 'submit_account_delete_identity',
|
||||
FeedbackSubmit = 'submit_leave_feedback',
|
||||
FilterClear = 'submit_clear_filter',
|
||||
FilterApply = 'submit_filter_apply',
|
||||
UserCreate = 'submit_user_create',
|
||||
UserDelete = 'submit_user_delete',
|
||||
UserUpdateEmail = 'submit_user_update_email',
|
||||
@@ -168,6 +228,7 @@ export enum Submit {
|
||||
UserUpdateName = 'submit_user_update_name',
|
||||
UserUpdatePassword = 'submit_user_update_password',
|
||||
UserUpdatePhone = 'submit_user_update_phone',
|
||||
UserUpdateMfa = 'submit_user_update_mfa',
|
||||
UserUpdatePreferences = 'submit_user_update_preferences',
|
||||
UserUpdateStatus = 'submit_user_update_status',
|
||||
UserUpdateVerificationEmail = 'submit_user_update_verification_email',
|
||||
@@ -189,9 +250,12 @@ export enum Submit {
|
||||
MemberDelete = 'submit_member_delete',
|
||||
MembershipUpdate = 'submit_membership_update',
|
||||
MembershipUpdateStatus = 'submit_membership_update_status',
|
||||
MessagingTargetUpdate = 'submit_messaging_target_update',
|
||||
MessagingUpdateHtmlMode = 'submit_update_html_mode',
|
||||
ProviderUpdate = 'submit_provider_update',
|
||||
TeamCreate = 'submit_team_create',
|
||||
TeamDelete = 'submit_team_delete',
|
||||
TeamUpdatePreferences = 'submit_team_update_preferences',
|
||||
TeamUpdateName = 'submit_team_update_name',
|
||||
AuthLimitUpdate = 'submit_auth_limit_update',
|
||||
AuthStatusUpdate = 'submit_auth_status_update',
|
||||
@@ -208,6 +272,7 @@ export enum Submit {
|
||||
DatabaseCreate = 'submit_database_create',
|
||||
DatabaseDelete = 'submit_database_delete',
|
||||
DatabaseUpdateName = 'submit_database_update_name',
|
||||
DatabaseImportCsv = 'submit_database_import_csv',
|
||||
AttributeCreate = 'submit_attribute_create',
|
||||
AttributeUpdate = 'submit_attribute_update',
|
||||
AttributeDelete = 'submit_attribute_delete',
|
||||
@@ -234,6 +299,8 @@ export enum Submit {
|
||||
FunctionUpdateTimeout = 'submit_function_update_timeout',
|
||||
FunctionUpdateEvents = 'submit_function_update_events',
|
||||
FunctionUpdateScopes = 'submit_function_key_update_scopes',
|
||||
FunctionUpdateRuntime = 'submit_function_update_runtime',
|
||||
FunctionUpdateBuildCommand = 'submit_function_update_build_command',
|
||||
FunctionConnectRepo = 'submit_function_connect_repo',
|
||||
FunctionDisconnectRepo = 'submit_function_disconnect_repo',
|
||||
FunctionRedeploy = 'submit_function_redeploy',
|
||||
@@ -247,17 +314,26 @@ export enum Submit {
|
||||
VariableDelete = 'submit_variable_delete',
|
||||
VariableUpdate = 'submit_variable_update',
|
||||
VariableEditor = 'submit_variable_editor',
|
||||
|
||||
KeyCreate = 'submit_key_create',
|
||||
KeyDelete = 'submit_key_delete',
|
||||
KeyUpdateName = 'submit_key_update_name',
|
||||
KeyUpdateScopes = 'submit_key_update_scopes',
|
||||
KeyUpdateExpire = 'submit_key_update_expire',
|
||||
|
||||
DevKeyCreate = 'submit_dev_key_create',
|
||||
DevKeyDelete = 'submit_dev_key_delete',
|
||||
DevKeyUpdateName = 'submit_dev_key_update_name',
|
||||
DevKeyUpdateExpire = 'submit_dev_key_update_expire',
|
||||
|
||||
PlatformCreate = 'submit_platform_create',
|
||||
PlatformDelete = 'submit_platform_delete',
|
||||
PlatformUpdate = 'submit_platform_update',
|
||||
|
||||
DomainCreate = 'submit_domain_create',
|
||||
DomainDelete = 'submit_domain_delete',
|
||||
DomainUpdateVerification = 'submit_domain_update_verification',
|
||||
CertificateDelete = 'submit_certificate_delete',
|
||||
WebhookCreate = 'submit_webhook_create',
|
||||
WebhookDelete = 'submit_webhook_delete',
|
||||
WebhookUpdateSignature = 'submit_webhook_update_signature',
|
||||
@@ -279,6 +355,9 @@ export enum Submit {
|
||||
FileCreate = 'submit_file_create',
|
||||
FileDelete = 'submit_file_delete',
|
||||
FileUpdatePermissions = 'submit_file_update_permissions',
|
||||
FileTokenCreate = 'submit_file_token',
|
||||
FileTokenDelete = 'submit_file_delete',
|
||||
FileTokenUpdate = 'submit_file_update_expiry',
|
||||
BudgetCapUpdate = 'submit_budget_cap_update',
|
||||
BudgetAlertsUpdate = 'submit_budget_alert_conditions_update',
|
||||
CreditRedeem = 'submit_credit_redeem',
|
||||
@@ -327,5 +406,26 @@ export enum Submit {
|
||||
MessagingTopicSubscriberDelete = 'submit_messaging_topic_subscriber_delete',
|
||||
ApplyQuickFilter = 'submit_apply_quick_filter',
|
||||
RequestBAA = 'submit_request_baa',
|
||||
RequestSoc2 = 'submit_request_soc2'
|
||||
RequestSoc2 = 'submit_request_soc2',
|
||||
SiteCreate = 'submit_site_create',
|
||||
SiteDelete = 'submit_site_delete',
|
||||
SiteUpdateName = 'submit_site_update_name',
|
||||
SiteUpdatePermissions = 'submit_site_update_permissions',
|
||||
SiteUpdateSchedule = 'submit_site_update_schedule',
|
||||
SiteUpdateConfiguration = 'submit_site_update_configuration',
|
||||
SiteUpdateLogging = 'submit_site_update_logging',
|
||||
SiteUpdateTimeout = 'submit_site_update_timeout',
|
||||
SiteUpdateEvents = 'submit_site_update_events',
|
||||
SiteUpdateScopes = 'submit_site_key_update_scopes',
|
||||
SiteUpdateBuildSettings = 'submit_site_update_build_settings',
|
||||
SiteUpdateSinglePageApplication = 'submit_site_update_single_page_application',
|
||||
SiteConnectRepo = 'submit_site_connect_repo',
|
||||
SiteRedeploy = 'submit_site_redeploy',
|
||||
SiteDisconnectRepo = 'submit_site_disconnect_repo',
|
||||
SiteActivateDeployment = 'submit_site_activate_deployment',
|
||||
RecordCreate = 'submit_dns_record_create',
|
||||
RecordUpdate = 'submit_dns_record_update',
|
||||
RecordDelete = 'submit_dns_record_delete',
|
||||
SearchClear = 'submit_clear_search',
|
||||
FrameworkDetect = 'submit_framework_detect'
|
||||
}
|
||||
|
||||
@@ -22,7 +22,13 @@ export const timer: Action<HTMLElement, Props> = (node, props) => {
|
||||
|
||||
function step() {
|
||||
const diffInSeconds = Math.floor((new Date().getTime() - startDate.getTime()) / 1000);
|
||||
node.textContent = calculateTime(diffInSeconds);
|
||||
const minutes = Math.floor(diffInSeconds / 60);
|
||||
if (minutes > 0) {
|
||||
const seconds = diffInSeconds % 60;
|
||||
node.textContent = `${minutes}m ${seconds}s`;
|
||||
} else {
|
||||
node.textContent = calculateTime(diffInSeconds);
|
||||
}
|
||||
frame = window.requestAnimationFrame(step);
|
||||
}
|
||||
|
||||
|
||||
+117
-84
@@ -1,5 +1,121 @@
|
||||
{
|
||||
"backgroundColor": "rgba(255,255,255,0)",
|
||||
"backgroundColor": "var(--bgcolor-neutral-primary)",
|
||||
"categoryAxis": {
|
||||
"axisLine": {
|
||||
"lineStyle": {
|
||||
"color": "var(--fgcolor-neutral-tertiary)"
|
||||
},
|
||||
"show": true
|
||||
},
|
||||
"axisTick": {
|
||||
"lineStyle": {
|
||||
"color": "var(--fgcolor-neutral-tertiary)"
|
||||
},
|
||||
"show": true
|
||||
},
|
||||
"axisLabel": {
|
||||
"color": "var(--fgcolor-neutral-tertiary)",
|
||||
"show": true
|
||||
},
|
||||
"splitLine": {
|
||||
"show": false
|
||||
},
|
||||
"splitArea": {
|
||||
"show": false
|
||||
}
|
||||
},
|
||||
"valueAxis": {
|
||||
"axisLabel": {
|
||||
"color": "var(--fgcolor-neutral-tertiary)",
|
||||
"show": true
|
||||
},
|
||||
"splitLine": {
|
||||
"lineStyle": {
|
||||
"color": ["var(--border-neutral)"]
|
||||
},
|
||||
"show": true
|
||||
},
|
||||
"axisLine": {
|
||||
"show": false
|
||||
},
|
||||
"axisTick": {
|
||||
"show": false
|
||||
},
|
||||
"splitArea": {
|
||||
"show": false
|
||||
}
|
||||
},
|
||||
"logAxis": {
|
||||
"axisLine": {
|
||||
"lineStyle": {
|
||||
"color": "var(--fgcolor-neutral-tertiary)"
|
||||
},
|
||||
"show": true
|
||||
},
|
||||
"axisTick": {
|
||||
"lineStyle": {
|
||||
"color": "var(--fgcolor-neutral-tertiary)"
|
||||
},
|
||||
"show": true
|
||||
},
|
||||
"axisLabel": {
|
||||
"color": "var(--fgcolor-neutral-tertiary)",
|
||||
"show": true
|
||||
},
|
||||
"splitLine": {
|
||||
"lineStyle": {
|
||||
"color": ["var(--border-neutral)"]
|
||||
},
|
||||
"show": true
|
||||
},
|
||||
"splitArea": {
|
||||
"show": false
|
||||
}
|
||||
},
|
||||
"timeAxis": {
|
||||
"axisLine": {
|
||||
"lineStyle": {
|
||||
"color": "var(--fgcolor-neutral-tertiary)"
|
||||
},
|
||||
"show": true
|
||||
},
|
||||
"axisTick": {
|
||||
"lineStyle": {
|
||||
"color": "var(--fgcolor-neutral-tertiary)"
|
||||
},
|
||||
"show": true
|
||||
},
|
||||
"axisLabel": {
|
||||
"color": "var(--fgcolor-neutral-tertiary)",
|
||||
"show": true
|
||||
},
|
||||
"splitLine": {
|
||||
"lineStyle": {
|
||||
"color": ["var(--border-neutral)"]
|
||||
},
|
||||
"show": true
|
||||
},
|
||||
"splitArea": {
|
||||
"show": false
|
||||
}
|
||||
},
|
||||
"tooltip": {
|
||||
"backgroundColor": "var(--bgcolor-neutral-primary)",
|
||||
"borderColor": "var(--border-neutral)",
|
||||
"textStyle": {
|
||||
"color": "var(--fgcolor-neutral-secondary)"
|
||||
},
|
||||
"axisPointer": {
|
||||
"lineStyle": {
|
||||
"color": "var(--border-neutral-strong)",
|
||||
"width": 1
|
||||
},
|
||||
"crossStyle": {
|
||||
"color": "var(--border-neutral-strong)",
|
||||
"width": 1
|
||||
}
|
||||
}
|
||||
},
|
||||
"textStyle": {},
|
||||
"line": {
|
||||
"itemStyle": {
|
||||
@@ -12,89 +128,6 @@
|
||||
"symbol": "emptyCircle",
|
||||
"smooth": true
|
||||
},
|
||||
"bar": {
|
||||
"itemStyle": {
|
||||
"barBorderColor": "#ccc"
|
||||
}
|
||||
},
|
||||
"categoryAxis": {
|
||||
"axisLine": {
|
||||
"show": true
|
||||
},
|
||||
"axisTick": {
|
||||
"show": true
|
||||
},
|
||||
"axisLabel": {
|
||||
"show": true
|
||||
},
|
||||
"splitLine": {
|
||||
"show": false
|
||||
},
|
||||
"splitArea": {
|
||||
"show": false
|
||||
}
|
||||
},
|
||||
"valueAxis": {
|
||||
"axisLine": {
|
||||
"show": false
|
||||
},
|
||||
"axisTick": {
|
||||
"show": false
|
||||
},
|
||||
"axisLabel": {
|
||||
"show": true
|
||||
},
|
||||
"splitLine": {
|
||||
"show": true
|
||||
},
|
||||
"splitArea": {
|
||||
"show": false
|
||||
}
|
||||
},
|
||||
"logAxis": {
|
||||
"axisLine": {
|
||||
"show": true
|
||||
},
|
||||
"axisTick": {
|
||||
"show": true
|
||||
},
|
||||
"axisLabel": {
|
||||
"show": true
|
||||
},
|
||||
"splitLine": {
|
||||
"show": true
|
||||
},
|
||||
"splitArea": {
|
||||
"show": false
|
||||
}
|
||||
},
|
||||
"timeAxis": {
|
||||
"axisLine": {
|
||||
"show": true
|
||||
},
|
||||
"axisTick": {
|
||||
"show": true
|
||||
},
|
||||
"axisLabel": {
|
||||
"show": true
|
||||
},
|
||||
"splitLine": {
|
||||
"show": true
|
||||
},
|
||||
"splitArea": {
|
||||
"show": false
|
||||
}
|
||||
},
|
||||
"tooltip": {
|
||||
"axisPointer": {
|
||||
"lineStyle": {
|
||||
"width": 1
|
||||
},
|
||||
"crossStyle": {
|
||||
"width": 1
|
||||
}
|
||||
}
|
||||
},
|
||||
"timeline": {
|
||||
"lineStyle": {
|
||||
"width": 1
|
||||
|
||||
@@ -4,15 +4,13 @@
|
||||
import { app } from '$lib/stores/app';
|
||||
import { dailyFormat, hourlyFormat, defaultConfig } from './config';
|
||||
import base from './base.json';
|
||||
import light from './light.json';
|
||||
import dark from './dark.json';
|
||||
import type { ECharts } from 'echarts/core';
|
||||
import type { BarSeriesOption, LineSeriesOption } from 'echarts/charts';
|
||||
import type { EChartsOption } from 'echarts';
|
||||
import { wizard } from '$lib/stores/wizard';
|
||||
|
||||
registerTheme('light', { ...base, ...light });
|
||||
registerTheme('dark', { ...base, ...dark });
|
||||
registerTheme('light', base);
|
||||
registerTheme('dark', base);
|
||||
|
||||
export let options: EChartsOption;
|
||||
export let series: (BarSeriesOption | LineSeriesOption)[];
|
||||
@@ -90,7 +88,7 @@
|
||||
|
||||
<svelte:window on:resize={onResize} />
|
||||
|
||||
<div class="echart" bind:this={container} />
|
||||
<div class="echart" bind:this={container}></div>
|
||||
|
||||
<style>
|
||||
.echart {
|
||||
|
||||
@@ -120,10 +120,10 @@
|
||||
"color": "#444444"
|
||||
}
|
||||
},
|
||||
"backgroundColor": "#373B4D",
|
||||
"borderColor": "#373B4D",
|
||||
"backgroundColor": "var(--bgcolor-neutral-primary)",
|
||||
"borderColor": "var(--border-neutral)",
|
||||
"textStyle": {
|
||||
"color": "#F2F2F8"
|
||||
"color": "var(--fgcolor-neutral-secondary)"
|
||||
}
|
||||
},
|
||||
"timeline": {
|
||||
|
||||
+21
-88
@@ -1,149 +1,82 @@
|
||||
{
|
||||
"backgroundColor": "rgba(255,255,255,0)",
|
||||
"title": {
|
||||
"textStyle": {
|
||||
"color": "#373b4d"
|
||||
},
|
||||
"subtextStyle": {
|
||||
"color": "#616b7c"
|
||||
}
|
||||
},
|
||||
"bar": {
|
||||
"itemStyle": {
|
||||
"barBorderColor": "#ccc"
|
||||
}
|
||||
},
|
||||
"backgroundColor": "var(--bgcolor-neutral-primary)",
|
||||
"categoryAxis": {
|
||||
"axisLine": {
|
||||
"lineStyle": {
|
||||
"color": "#6E7079"
|
||||
"color": "var(--fgcolor-neutral-tertiary)"
|
||||
}
|
||||
},
|
||||
"axisTick": {
|
||||
"lineStyle": {
|
||||
"color": "#6E7079"
|
||||
"color": "var(--fgcolor-neutral-tertiary)"
|
||||
}
|
||||
},
|
||||
"axisLabel": {
|
||||
"color": "#6E7079"
|
||||
"color": "var(--fgcolor-neutral-tertiary)"
|
||||
}
|
||||
},
|
||||
"valueAxis": {
|
||||
"axisLabel": {
|
||||
"color": "#6E7079"
|
||||
"color": "var(--fgcolor-neutral-tertiary)"
|
||||
},
|
||||
"splitLine": {
|
||||
"lineStyle": {
|
||||
"color": ["#E8E9F0"]
|
||||
"color": ["var(--border-neutral)"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"logAxis": {
|
||||
"axisLine": {
|
||||
"lineStyle": {
|
||||
"color": "#6E7079"
|
||||
"color": "var(--fgcolor-neutral-tertiary)"
|
||||
}
|
||||
},
|
||||
"axisTick": {
|
||||
"lineStyle": {
|
||||
"color": "#6E7079"
|
||||
"color": "var(--fgcolor-neutral-tertiary)"
|
||||
}
|
||||
},
|
||||
"axisLabel": {
|
||||
"color": "#6E7079"
|
||||
"color": "var(--fgcolor-neutral-tertiary)"
|
||||
},
|
||||
"splitLine": {
|
||||
"lineStyle": {
|
||||
"color": ["#E0E6F1"]
|
||||
"color": ["var(--border-neutral)"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"timeAxis": {
|
||||
"axisLine": {
|
||||
"lineStyle": {
|
||||
"color": "#6E7079"
|
||||
"color": "var(--fgcolor-neutral-tertiary)"
|
||||
}
|
||||
},
|
||||
"axisTick": {
|
||||
"lineStyle": {
|
||||
"color": "#6E7079"
|
||||
"color": "var(--fgcolor-neutral-tertiary)"
|
||||
}
|
||||
},
|
||||
"axisLabel": {
|
||||
"color": "#6E7079"
|
||||
"color": "var(--fgcolor-neutral-tertiary)"
|
||||
},
|
||||
"splitLine": {
|
||||
"lineStyle": {
|
||||
"color": ["#E0E6F1"]
|
||||
"color": ["var(--border-neutral)"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"toolbox": {
|
||||
"iconStyle": {
|
||||
"borderColor": "#999"
|
||||
},
|
||||
"emphasis": {
|
||||
"iconStyle": {
|
||||
"borderColor": "#666"
|
||||
}
|
||||
}
|
||||
},
|
||||
"legend": {
|
||||
"textStyle": {
|
||||
"color": "#333"
|
||||
}
|
||||
},
|
||||
"tooltip": {
|
||||
"backgroundColor": "var(--bgcolor-neutral-primary)",
|
||||
"borderColor": "var(--border-neutral)",
|
||||
"textStyle": {
|
||||
"color": "var(--fgcolor-neutral-secondary)"
|
||||
},
|
||||
"axisPointer": {
|
||||
"lineStyle": {
|
||||
"color": "#ccc"
|
||||
"color": "var(--border-neutral-strong)"
|
||||
},
|
||||
"crossStyle": {
|
||||
"color": "#ccc"
|
||||
}
|
||||
}
|
||||
},
|
||||
"timeline": {
|
||||
"lineStyle": {
|
||||
"color": "#DAE1F5"
|
||||
},
|
||||
"itemStyle": {
|
||||
"color": "#A4B1D7"
|
||||
},
|
||||
"controlStyle": {
|
||||
"color": "#A4B1D7",
|
||||
"borderColor": "#A4B1D7"
|
||||
},
|
||||
"checkpointStyle": {
|
||||
"color": "#316bf3",
|
||||
"borderColor": "#fff"
|
||||
},
|
||||
"label": {
|
||||
"color": "#A4B1D7"
|
||||
},
|
||||
"emphasis": {
|
||||
"itemStyle": {
|
||||
"color": "#FFF"
|
||||
},
|
||||
"controlStyle": {
|
||||
"color": "#A4B1D7",
|
||||
"borderColor": "#A4B1D7"
|
||||
},
|
||||
"label": {
|
||||
"color": "#A4B1D7"
|
||||
}
|
||||
}
|
||||
},
|
||||
"visualMap": {
|
||||
"color": ["#bf444c", "#d88273", "#f6efa6"]
|
||||
},
|
||||
"markPoint": {
|
||||
"label": {
|
||||
"color": "#eee"
|
||||
},
|
||||
"emphasis": {
|
||||
"label": {
|
||||
"color": "#eee"
|
||||
"color": "var(--border-neutral-strong)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -99,7 +99,7 @@
|
||||
return ['INPUT', 'TEXTAREA', 'SELECT'].includes((event.target as HTMLElement).tagName);
|
||||
}
|
||||
|
||||
const handleKeydown = (e) => {
|
||||
const handleKeydown = (e: KeyboardEvent) => {
|
||||
if (!$subPanels.length) {
|
||||
if (isInputEvent(e)) return;
|
||||
keys = [...keys, e.key].slice(-10);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { debounce } from '$lib/helpers/debounce';
|
||||
import { isMac } from '$lib/helpers/platform';
|
||||
import { wizard } from '$lib/stores/wizard';
|
||||
import { onMount } from 'svelte';
|
||||
import { type ComponentType, onMount } from 'svelte';
|
||||
import { derived, writable } from 'svelte/store';
|
||||
import { nanoid } from 'nanoid/non-secure';
|
||||
import { trackEvent } from '$lib/actions/analytics';
|
||||
@@ -37,7 +37,8 @@ const groups = [
|
||||
'buckets',
|
||||
'files',
|
||||
'misc',
|
||||
'settings'
|
||||
'settings',
|
||||
'sites'
|
||||
] as const;
|
||||
|
||||
export type CommandGroup = (typeof groups)[number];
|
||||
@@ -48,7 +49,7 @@ type BaseCommand = {
|
||||
disabled?: boolean;
|
||||
forceEnable?: boolean;
|
||||
group?: CommandGroup;
|
||||
icon?: string;
|
||||
icon?: ComponentType;
|
||||
image?: string;
|
||||
rank?: number;
|
||||
nested?: boolean;
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
<script lang="ts">
|
||||
import { Remarkable } from 'remarkable';
|
||||
import Template from './template.svelte';
|
||||
import { Alert, Keyboard, Layout } from '@appwrite.io/pink-svelte';
|
||||
|
||||
const markdownInstance = new Remarkable();
|
||||
|
||||
import { Alert, AvatarInitials, Code, LoadingDots, SvgIcon } from '$lib/components';
|
||||
import { AvatarInitials, Code, LoadingDots, SvgIcon } from '$lib/components';
|
||||
import { user } from '$lib/stores/user';
|
||||
import { useCompletion } from '@ai-sdk/svelte';
|
||||
import { subPanels } from '../subPanels';
|
||||
@@ -158,14 +159,13 @@
|
||||
</div>
|
||||
|
||||
<div slot="option" let:option class="u-flex u-cross-center u-gap-8">
|
||||
<i class="icon-question-mark-circle" />
|
||||
<i class="icon-question-mark-circle"></i>
|
||||
<span>{option.label}</span>
|
||||
</div>
|
||||
|
||||
{#if !$preferences.hideAiDisclaimer}
|
||||
<div style="padding: 1rem; padding-block-end: 0;">
|
||||
<Alert
|
||||
type="default"
|
||||
<Alert.Inline
|
||||
dismissible
|
||||
on:dismiss={() => {
|
||||
$preferences.hideAiDisclaimer = true;
|
||||
@@ -173,7 +173,7 @@
|
||||
<span slot="title">
|
||||
We collect user responses to refine our experimental AI feature.
|
||||
</span>
|
||||
</Alert>
|
||||
</Alert.Inline>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -218,57 +218,54 @@
|
||||
|
||||
{#if $error}
|
||||
<div style="padding: 1rem; padding-block-end: 0;">
|
||||
<Alert type="error">
|
||||
<span slot="title">Something went wrong</span>
|
||||
<p>
|
||||
An unexpected error occurred while handling your request. Please try again
|
||||
later.
|
||||
</p>
|
||||
</Alert>
|
||||
<Alert.Inline status="error" title="Something went wrong">
|
||||
An unexpected error occurred while handling your request. Please try again later.
|
||||
</Alert.Inline>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="footer" slot="footer">
|
||||
<div class="u-flex u-cross-center u-gap-4">
|
||||
<AvatarInitials size={32} name={$user.name || $user.email} />
|
||||
<form
|
||||
class="input-text-wrapper u-width-full-line"
|
||||
style="--amount-of-buttons: 1;"
|
||||
on:submit|preventDefault={(e) => {
|
||||
handleSubmit(e);
|
||||
}}>
|
||||
<!-- svelte-ignore a11y-autofocus -->
|
||||
<input
|
||||
type="text"
|
||||
class="input-text"
|
||||
placeholder="Ask a question..."
|
||||
autofocus
|
||||
bind:value={$input}
|
||||
disabled={$isLoading} />
|
||||
<div class="options-list">
|
||||
<button
|
||||
class="options-list-button"
|
||||
aria-label="ask AI"
|
||||
type="submit"
|
||||
disabled={!$input.trim() || $isLoading}>
|
||||
<span class="icon-arrow-sm-right" aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="u-flex u-main-end u-cross-center u-gap-16 u-margin-block-start-16">
|
||||
<div class="u-flex u-cross-center u-gap-4">
|
||||
<kbd class="kbd">Enter</kbd>
|
||||
<span>to search</span>
|
||||
</div>
|
||||
<div class="sep" />
|
||||
<div class="u-flex u-cross-center u-gap-4">
|
||||
<kbd class="kbd">Esc</kbd>
|
||||
<span>to {$subPanels.length === 1 ? 'close' : 'go back'}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Layout.Stack slot="footer">
|
||||
<Layout.Stack gap="l">
|
||||
<Layout.Stack direction="row" gap="s">
|
||||
<AvatarInitials size="s" name={$user.name} />
|
||||
<form
|
||||
class="input-text-wrapper u-width-full-line"
|
||||
style="--amount-of-buttons: 1;"
|
||||
on:submit|preventDefault={(e) => {
|
||||
handleSubmit(e);
|
||||
}}>
|
||||
<!-- svelte-ignore a11y-autofocus -->
|
||||
<input
|
||||
type="text"
|
||||
class="input-text"
|
||||
placeholder="Ask a question..."
|
||||
autofocus
|
||||
bind:value={$input}
|
||||
disabled={$isLoading} />
|
||||
<div class="options-list">
|
||||
<button
|
||||
class="options-list-button"
|
||||
aria-label="ask AI"
|
||||
type="submit"
|
||||
disabled={!$input.trim() || $isLoading}>
|
||||
<span class="icon-arrow-sm-right" aria-hidden="true"></span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</Layout.Stack>
|
||||
<Layout.Stack direction="row" justifyContent="space-between" gap="xxl">
|
||||
<Layout.Stack direction="row" alignItems="center" gap="xxs">
|
||||
<Keyboard key="Enter" autoWidth={true} /> <span>to search</span></Layout.Stack>
|
||||
<Layout.Stack
|
||||
direction="row"
|
||||
justifyContent="flex-end"
|
||||
alignItems="center"
|
||||
gap="xxs">
|
||||
<Keyboard key="Esc" autoWidth={true} />
|
||||
<span>to {$subPanels.length === 1 ? 'close' : 'go back'}</span></Layout.Stack>
|
||||
</Layout.Stack>
|
||||
</Layout.Stack>
|
||||
</Layout.Stack>
|
||||
</Template>
|
||||
|
||||
<style lang="scss">
|
||||
@@ -280,14 +277,6 @@
|
||||
--logo-bg: #f2f2f8;
|
||||
}
|
||||
|
||||
:global(.theme-dark) .footer {
|
||||
--sep-clr: hsl(var(--color-neutral-150));
|
||||
}
|
||||
|
||||
:global(.theme-light) .footer {
|
||||
--sep-clr: hsl(var(--color-neutral-30));
|
||||
}
|
||||
|
||||
.content {
|
||||
overflow: auto;
|
||||
padding: 1rem;
|
||||
@@ -323,14 +312,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
.footer {
|
||||
.sep {
|
||||
width: 1px;
|
||||
height: 1.5rem;
|
||||
background-color: var(--sep-clr);
|
||||
}
|
||||
}
|
||||
|
||||
.experimental {
|
||||
display: flex;
|
||||
padding: 0.09375rem 0.25rem;
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
|
||||
<Template options={filteredOptions} bind:search>
|
||||
<div class="u-flex u-cross-center u-gap-8" slot="option" let:option>
|
||||
<i class="icon-{option.icon}" />
|
||||
<i class="icon-{option.icon}"></i>
|
||||
<span>{option.label}</span>
|
||||
</div>
|
||||
</Template>
|
||||
|
||||
@@ -67,7 +67,7 @@
|
||||
|
||||
<Template options={filteredOptions} bind:search>
|
||||
<div class="u-flex u-cross-center u-gap-8" slot="option" let:option>
|
||||
<i class="icon-{option.icon}" />
|
||||
<i class="icon-{option.icon}"></i>
|
||||
<span>{option.label}</span>
|
||||
</div>
|
||||
</Template>
|
||||
|
||||
@@ -4,13 +4,14 @@
|
||||
addPlatform
|
||||
} from '$routes/(console)/project-[region]-[project]/overview/platforms/+page.svelte';
|
||||
import Template from './template.svelte';
|
||||
import { IconAndroid, IconApple, IconCode, IconFlutter } from '@appwrite.io/pink-icons-svelte';
|
||||
|
||||
let search = '';
|
||||
|
||||
let platforms = [
|
||||
{
|
||||
label: 'Web',
|
||||
icon: 'code',
|
||||
icon: IconCode,
|
||||
group: 'platforms',
|
||||
callback: () => {
|
||||
addPlatform(Platform.Web);
|
||||
@@ -18,7 +19,7 @@
|
||||
},
|
||||
{
|
||||
label: 'Flutter',
|
||||
icon: 'flutter',
|
||||
icon: IconFlutter,
|
||||
group: 'platforms',
|
||||
callback: () => {
|
||||
addPlatform(Platform.Flutter);
|
||||
@@ -26,7 +27,7 @@
|
||||
},
|
||||
{
|
||||
label: 'Android',
|
||||
icon: 'android',
|
||||
icon: IconAndroid,
|
||||
group: 'platforms',
|
||||
callback: () => {
|
||||
addPlatform(Platform.Android);
|
||||
@@ -34,7 +35,7 @@
|
||||
},
|
||||
{
|
||||
label: 'Apple',
|
||||
icon: 'apple',
|
||||
icon: IconApple,
|
||||
group: 'platforms',
|
||||
callback: () => {
|
||||
addPlatform(Platform.Apple);
|
||||
@@ -49,7 +50,7 @@
|
||||
|
||||
<Template options={filteredPlatforms} bind:search>
|
||||
<div class="u-flex u-cross-center u-gap-8" slot="option" let:option>
|
||||
<i class="icon-{option.icon}" />
|
||||
<i class="icon-{option.icon}"></i>
|
||||
<span>{option.label}</span>
|
||||
</div>
|
||||
</Template>
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
import { isMac } from '$lib/helpers/platform';
|
||||
import { commands, searchers, type Command, isKeyedCommand } from '../commands';
|
||||
import Template from './template.svelte';
|
||||
import { Icon, Keyboard, Layout } from '@appwrite.io/pink-svelte';
|
||||
import { IconArrowSmRight } from '@appwrite.io/pink-icons-svelte';
|
||||
|
||||
let search = '';
|
||||
|
||||
@@ -53,43 +55,56 @@
|
||||
};
|
||||
</script>
|
||||
|
||||
<Template options={results} bind:search searchPlaceholder="Search for commands or content...">
|
||||
<div slot="option" class="u-flex u-main-space-between content" let:option={command}>
|
||||
<div class="u-flex u-gap-8 u-cross-center">
|
||||
<Template options={results} bind:search searchPlaceholder="Search...">
|
||||
<Layout.Stack
|
||||
slot="option"
|
||||
direction="row"
|
||||
justifyContent="space-between"
|
||||
alignItems="center"
|
||||
let:option={command}>
|
||||
<Layout.Stack direction="row" alignItems="center" gap="s">
|
||||
{#if command.image}
|
||||
<img
|
||||
src={`${base}/icons/${$app.themeInUse}/color/${command.image}.svg`}
|
||||
alt={command.label} />
|
||||
{:else if command.icon}
|
||||
<Icon icon={command.icon} size="s" color="--fgcolor-neutral-tertiary" />
|
||||
{:else}
|
||||
<i class="icon-{command.icon ?? 'arrow-sm-right'}" />
|
||||
<Icon icon={IconArrowSmRight} size="s" color="--fgcolor-neutral-tertiary" />
|
||||
{/if}
|
||||
<span>
|
||||
{command.label}
|
||||
</span>
|
||||
</div>
|
||||
<div class="u-flex u-gap-4 u-cross-center">
|
||||
</Layout.Stack>
|
||||
<Layout.Stack direction="row" justifyContent="flex-end" alignItems="center" gap="s">
|
||||
{#if hasCtrl(command)}
|
||||
<kbd class="kbd"> {isMac() ? '⌘' : 'Ctrl'} </kbd>
|
||||
<Keyboard autoWidth={!isMac()} key={isMac() ? '⌘' : 'Ctrl'} />
|
||||
{/if}
|
||||
{#if hasShift(command)}
|
||||
<kbd class="kbd"> {isMac() ? '⇧' : 'Shift'} </kbd>
|
||||
<Keyboard autoWidth={!isMac()} key={isMac() ? '⇧' : 'Shift'} />
|
||||
{/if}
|
||||
{#if hasAlt(command)}
|
||||
<kbd class="kbd"> {isMac() ? '⌥' : 'Alt'} </kbd>
|
||||
<Keyboard autoWidth={!isMac()} key={isMac() ? '⌥' : 'Alt'} />
|
||||
{/if}
|
||||
{#if isKeyedCommand(command)}
|
||||
{#each command.keys as key, i}
|
||||
{@const hasNext = command.keys.length - 1 !== i}
|
||||
|
||||
<kbd class="kbd">
|
||||
{key.toUpperCase()}
|
||||
</kbd>
|
||||
<Keyboard key={key.toUpperCase()} />
|
||||
{#if hasNext}
|
||||
<span class="u-margin-inline-4" style:opacity={0.5}>then</span>
|
||||
<span class="then">then</span>
|
||||
{/if}
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</Layout.Stack>
|
||||
</Layout.Stack>
|
||||
<svelte:fragment slot="no-options">No commands found</svelte:fragment>
|
||||
</Template>
|
||||
|
||||
<style>
|
||||
.then {
|
||||
color: var(--fgcolor-neutral-tertiary, #97979b);
|
||||
font-size: var(--font-size-s, 14px);
|
||||
font-weight: 400;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -8,6 +8,9 @@
|
||||
import { getCommandCenterCtx } from '../commandCenter.svelte';
|
||||
|
||||
import { clearSubPanels, popSubPanel, subPanels } from '../subPanels';
|
||||
import { IconArrowSmRight } from '@appwrite.io/pink-icons-svelte';
|
||||
import { Icon, Keyboard, Layout } from '@appwrite.io/pink-svelte';
|
||||
import { Submit, trackEvent } from '$lib/actions/analytics';
|
||||
|
||||
/* eslint no-undef: "off" */
|
||||
type Option = $$Generic<Omit<Command, 'group'> & { group?: string }>;
|
||||
@@ -20,6 +23,7 @@
|
||||
let selected = 0;
|
||||
let usingKeyboard = false;
|
||||
let contentEl: HTMLElement;
|
||||
let didSearch = false;
|
||||
|
||||
async function triggerOption(option: Option) {
|
||||
const prevPanels = $subPanels.length;
|
||||
@@ -41,6 +45,14 @@
|
||||
if (!open) return;
|
||||
usingKeyboard = true;
|
||||
|
||||
if (search.length > 0) {
|
||||
didSearch = true;
|
||||
}
|
||||
|
||||
if (search === '' && didSearch) {
|
||||
trackEvent(Submit.SearchClear);
|
||||
}
|
||||
|
||||
let canceled = false;
|
||||
dispatch('keydown', {
|
||||
originalEvent: event,
|
||||
@@ -250,7 +262,7 @@
|
||||
{@const isLast = i === breadcrumbs.length - 1}
|
||||
<button class="crumb" on:click={() => handleCrumbClick(i)}>
|
||||
<span>{crumb}</span>
|
||||
<i class="icon-x" />
|
||||
<i class="icon-x"></i>
|
||||
</button>
|
||||
{#if !isLast}
|
||||
<span style="opacity: 50%">/</span>
|
||||
@@ -284,7 +296,7 @@
|
||||
class:first-nested={isFirstNested(i)}
|
||||
class:last-nested={isLastNested(i)}>
|
||||
{#if isSelected}
|
||||
<div class="bg" />
|
||||
<div class="bg"></div>
|
||||
{/if}
|
||||
<button
|
||||
class="option"
|
||||
@@ -293,12 +305,22 @@
|
||||
on:mouseleave={getOptionBlurHandler()}
|
||||
on:focus={getOptionFocusHandler(item)}>
|
||||
<slot name="option" option={castOption(item)}>
|
||||
<div class="u-flex u-gap-8 u-cross-center">
|
||||
<i class="icon-{item.icon ?? 'arrow-sm-right'}" />
|
||||
<Layout.Stack direction="column" gap="s">
|
||||
{#if item.icon}
|
||||
<Icon
|
||||
icon={item.icon}
|
||||
size="s"
|
||||
color="--fgcolor-neutral-tertiary" />
|
||||
{:else}
|
||||
<Icon
|
||||
icon={IconArrowSmRight}
|
||||
size="s"
|
||||
color="--fgcolor-neutral-tertiary" />
|
||||
{/if}
|
||||
<span>
|
||||
{item.label}
|
||||
</span>
|
||||
</div>
|
||||
</Layout.Stack>
|
||||
</slot>
|
||||
</button>
|
||||
</li>
|
||||
@@ -316,15 +338,17 @@
|
||||
|
||||
<div class="footer">
|
||||
<slot name="footer">
|
||||
<div class=" u-flex u-flex u-cross-center u-main-space-between">
|
||||
<div class="u-flex u-cross-center u-gap-4">
|
||||
<kbd class="kbd">Enter</kbd> <span>to select</span>
|
||||
</div>
|
||||
<div class="u-flex u-cross-center u-gap-4">
|
||||
<kbd class="kbd">Esc</kbd>
|
||||
<span>to {$subPanels.length > 1 ? 'go back' : 'close'}</span>
|
||||
</div>
|
||||
</div>
|
||||
<Layout.Stack direction="row" justifyContent="space-between"
|
||||
><Layout.Stack direction="row" alignItems="center" gap="xxs">
|
||||
<Keyboard key="Enter" autoWidth={true} /> <span>to select</span></Layout.Stack>
|
||||
<Layout.Stack
|
||||
direction="row"
|
||||
justifyContent="flex-end"
|
||||
alignItems="center"
|
||||
gap="xxs">
|
||||
<Keyboard key="Esc" autoWidth={true} />
|
||||
<span>to {$subPanels.length > 1 ? 'go back' : 'close'}</span></Layout.Stack>
|
||||
</Layout.Stack>
|
||||
</slot>
|
||||
</div>
|
||||
</div>
|
||||
@@ -363,45 +387,16 @@
|
||||
animation: scale-up 150ms cubic-bezier(0.5, 1, 0.89, 1);
|
||||
}
|
||||
|
||||
// Theme
|
||||
:global(.theme-light) .card {
|
||||
--cmd-center-bg: hsl(var(--color-neutral-0));
|
||||
--cmd-center-border: hsl(var(--color-neutral-10));
|
||||
--cmd-center-shadow: 0px 16px 32px 0px rgba(55, 59, 77, 0.04);
|
||||
|
||||
--kbd-bg: hsl(var(--color-neutral-15));
|
||||
--kbd-color: hsl(var(--color-neutral-60));
|
||||
|
||||
--crumb-bg: hsl(var(--color-neutral-10));
|
||||
--crumb-color: hsl(var(--color-neutral-100));
|
||||
|
||||
--result-bg: hsl(var(--color-neutral-10));
|
||||
--footer-bg: linear-gradient(180deg, #fff 0%, #e8e9f0 100%);
|
||||
|
||||
--icon-color: hsl(var(--color-neutral-50));
|
||||
--label-color: hsl(var(--color-neutral-60));
|
||||
}
|
||||
|
||||
:global(.theme-dark) .card {
|
||||
--cmd-center-bg: hsl(var(--color-neutral-90));
|
||||
--cmd-center-border: hsl(var(--color-neutral-80));
|
||||
--cmd-center-shadow: 0px 16px 32px 0px #14141f;
|
||||
|
||||
--kbd-bg: hsl(var(--color-neutral-80));
|
||||
--kbd-color: hsl(var(--color-neutral-15));
|
||||
|
||||
--crumb-bg: hsl(var(--color-neutral-150));
|
||||
--crumb-color: hsl(var(--color-neutral-30));
|
||||
|
||||
--result-bg: hsl(var(--color-neutral-85));
|
||||
--footer-bg: linear-gradient(
|
||||
180deg,
|
||||
hsl(var(--color-neutral-100)) 0%,
|
||||
hsl(var(--color-neutral-85)) 100%
|
||||
);
|
||||
|
||||
--icon-color: hsl(var(--color-neutral-70));
|
||||
--label-color: hsl(var(--color-neutral-15));
|
||||
.card {
|
||||
--cmd-center-bg: var(--bgcolor-neutral-primary);
|
||||
--footer-bg: var(--bgcolor-neutral-primary);
|
||||
--cmd-center-border: var(--border-neutral);
|
||||
--result-bg: var(--overlay-neutral-hover);
|
||||
--kbd-bg: var(--overlay-on-neutral);
|
||||
--kbd-color: var(--fgcolor-neutral-secondary);
|
||||
--icon-color: var(--fgcolor-neutral-tertiary);
|
||||
--label-color: var(--fgcolor-neutral-secondary);
|
||||
--crumb-color: var(--fgcolor-neutral-secondary);
|
||||
}
|
||||
|
||||
// Elements
|
||||
@@ -420,11 +415,14 @@
|
||||
max-height: min(calc(100vh - var(--top) - 4rem), var(--max-height, 32rem));
|
||||
overflow: hidden;
|
||||
padding: 0;
|
||||
box-shadow:
|
||||
0 56px 32px 0 rgba(0, 0, 0, 0.02),
|
||||
0 6px 14px 0 rgba(0, 0, 0, 0.04),
|
||||
0 24px 25px 0 rgba(0, 0, 0, 0.03);
|
||||
|
||||
border-radius: 0.5rem;
|
||||
border: 1px solid var(--cmd-center-border);
|
||||
background: var(--cmd-center-bg);
|
||||
box-shadow: var(--cmd-center-shadow);
|
||||
backdrop-filter: blur(6px);
|
||||
|
||||
&.fullheight {
|
||||
@@ -434,7 +432,7 @@
|
||||
:global(.kbd) {
|
||||
color: var(--kbd-color);
|
||||
background-color: var(--kbd-bg);
|
||||
padding-inline: 0.25rem;
|
||||
padding-inline: var(--space-2, 4px);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -444,7 +442,7 @@
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
|
||||
border-bottom: 1px solid hsl(var(--color-border));
|
||||
border-bottom: 1px solid var(--border-neutral, #ededf0);
|
||||
font-size: 16px;
|
||||
padding: 1rem;
|
||||
|
||||
@@ -465,7 +463,6 @@
|
||||
gap: 0.25rem;
|
||||
|
||||
border-radius: 0.25rem;
|
||||
background: var(--crumb-bg);
|
||||
color: var(--crumb-color);
|
||||
text-align: center;
|
||||
font-family: Inter;
|
||||
@@ -493,13 +490,14 @@
|
||||
padding: 1rem;
|
||||
|
||||
.group {
|
||||
color: hsl(var(--color-neutral-70));
|
||||
color: var(--fgcolor-neutral-secondary, #56565c);
|
||||
margin-inline-start: 0.25rem;
|
||||
margin-block-end: 0.25rem;
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
|
||||
font-size: 10px !important;
|
||||
font-size: var(--font-size-xs, 12px);
|
||||
font-weight: 500;
|
||||
|
||||
&:not(:first-child) {
|
||||
margin-block-start: 1rem;
|
||||
@@ -560,7 +558,7 @@
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: -8px;
|
||||
border-left: 1px solid hsl(var(--color-border));
|
||||
border-left: 1px solid var(--border-neutral, #ededf0);
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
@@ -570,8 +568,8 @@
|
||||
|
||||
.footer {
|
||||
background: var(--footer-bg);
|
||||
border-top: 1px solid hsl(var(--color-border));
|
||||
border-top: 1px solid var(--border-neutral, #ededf0);
|
||||
|
||||
padding: 0.5rem 1rem;
|
||||
padding: 0.75rem 1rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -7,7 +7,14 @@ import type { Command, Searcher } from '../commands';
|
||||
import { addSubPanel } from '../subPanels';
|
||||
import { FilesPanel } from '../panels';
|
||||
import { base } from '$app/paths';
|
||||
import { page } from '$app/stores';
|
||||
import {
|
||||
IconFolder,
|
||||
IconKey,
|
||||
IconLockClosed,
|
||||
IconPuzzle,
|
||||
IconSearch
|
||||
} from '@appwrite.io/pink-icons-svelte';
|
||||
import { page } from '$app/state';
|
||||
|
||||
const getBucketCommand = (bucket: Models.Bucket, region: string, projectId: string) => {
|
||||
return {
|
||||
@@ -16,16 +23,15 @@ const getBucketCommand = (bucket: Models.Bucket, region: string, projectId: stri
|
||||
goto(`${base}/project-${region}-${projectId}/storage/bucket-${bucket.$id}`);
|
||||
},
|
||||
group: 'buckets',
|
||||
icon: 'folder'
|
||||
icon: IconFolder
|
||||
} satisfies Command;
|
||||
};
|
||||
|
||||
export const bucketSearcher = (async (query: string) => {
|
||||
const $page = get(page);
|
||||
const $project = get(project);
|
||||
const region = $page.params.region;
|
||||
const region = page.params.region;
|
||||
const { buckets } = await sdk
|
||||
.forProject($page.params.region, $page.params.project)
|
||||
.forProject(page.params.region, page.params.project)
|
||||
.storage.listBuckets([Query.orderDesc('$createdAt')]);
|
||||
|
||||
const filtered = buckets.filter((bucket) => bucket.name.includes(query));
|
||||
@@ -44,7 +50,7 @@ export const bucketSearcher = (async (query: string) => {
|
||||
},
|
||||
group: 'buckets',
|
||||
nested: true,
|
||||
icon: 'search',
|
||||
icon: IconSearch,
|
||||
keepOpen: true
|
||||
},
|
||||
{
|
||||
@@ -57,7 +63,7 @@ export const bucketSearcher = (async (query: string) => {
|
||||
},
|
||||
group: 'buckets',
|
||||
nested: true,
|
||||
icon: 'key'
|
||||
icon: IconKey
|
||||
},
|
||||
{
|
||||
label: 'Extensions',
|
||||
@@ -68,7 +74,7 @@ export const bucketSearcher = (async (query: string) => {
|
||||
},
|
||||
group: 'buckets',
|
||||
nested: true,
|
||||
icon: 'puzzle'
|
||||
icon: IconPuzzle
|
||||
},
|
||||
{
|
||||
label: 'File Security',
|
||||
@@ -80,7 +86,7 @@ export const bucketSearcher = (async (query: string) => {
|
||||
},
|
||||
group: 'buckets',
|
||||
nested: true,
|
||||
icon: 'lock-closed'
|
||||
icon: IconLockClosed
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
@@ -4,13 +4,12 @@ import { get } from 'svelte/store';
|
||||
import type { Searcher } from '../commands';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { base } from '$app/paths';
|
||||
import { page } from '$app/stores';
|
||||
import { page } from '$app/state';
|
||||
|
||||
export const collectionsSearcher = (async (query: string) => {
|
||||
const databaseId = get(database).$id;
|
||||
const $page = get(page);
|
||||
const { collections } = await sdk
|
||||
.forProject($page.params.region, $page.params.project)
|
||||
.forProject(page.params.region, page.params.project)
|
||||
.databases.listCollections(databaseId);
|
||||
|
||||
return collections
|
||||
@@ -22,7 +21,7 @@ export const collectionsSearcher = (async (query: string) => {
|
||||
label: col.name,
|
||||
callback: () => {
|
||||
goto(
|
||||
`${base}/project-${$page.params.region}-${$page.params.project}/databases/database-${databaseId}/collection-${col.$id}`
|
||||
`${base}/project-${page.params.region}-${page.params.project}/databases/database-${databaseId}/collection-${col.$id}`
|
||||
);
|
||||
}
|
||||
}) as const
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
import { goto } from '$app/navigation';
|
||||
import { get } from 'svelte/store';
|
||||
import type { Searcher } from '../commands';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { base } from '$app/paths';
|
||||
import { page } from '$app/stores';
|
||||
import { IconDatabase } from '@appwrite.io/pink-icons-svelte';
|
||||
import { page } from '$app/state';
|
||||
|
||||
export const dbSearcher = (async (query: string) => {
|
||||
const $page = get(page);
|
||||
const { databases } = await sdk
|
||||
.forProject($page.params.region, $page.params.project)
|
||||
.forProject(page.params.region, page.params.project)
|
||||
.databases.list();
|
||||
|
||||
return databases
|
||||
@@ -20,10 +19,10 @@ export const dbSearcher = (async (query: string) => {
|
||||
label: db.name,
|
||||
callback: () => {
|
||||
goto(
|
||||
`${base}/project-${$page.params.region}-${$page.params.project}/databases/database-${db.$id}`
|
||||
`${base}/project-${page.params.region}-${page.params.project}/databases/database-${db.$id}`
|
||||
);
|
||||
},
|
||||
icon: 'database'
|
||||
icon: IconDatabase
|
||||
}) as const
|
||||
);
|
||||
}) satisfies Searcher;
|
||||
|
||||
@@ -6,15 +6,15 @@ import { Query } from '@appwrite.io/console';
|
||||
import { goto } from '$app/navigation';
|
||||
import { project } from '$routes/(console)/project-[region]-[project]/store';
|
||||
import { base } from '$app/paths';
|
||||
import { page } from '$app/stores';
|
||||
import { IconDocument } from '@appwrite.io/pink-icons-svelte';
|
||||
import { page } from '$app/state';
|
||||
|
||||
export const fileSearcher = (async (query: string) => {
|
||||
const $bucket = get(bucket);
|
||||
const $project = get(project);
|
||||
const $page = get(page);
|
||||
|
||||
const { files } = await sdk
|
||||
.forProject($page.params.region, $page.params.project)
|
||||
.forProject(page.params.region, page.params.project)
|
||||
.storage.listFiles($bucket.$id, [Query.orderDesc('')], query || undefined);
|
||||
|
||||
return files.map((file) => ({
|
||||
@@ -24,7 +24,7 @@ export const fileSearcher = (async (query: string) => {
|
||||
`${base}/project-${$project.region}-${$project.$id}/storage/bucket-${$bucket.$id}/file-${file.$id}`
|
||||
);
|
||||
},
|
||||
icon: 'document',
|
||||
icon: IconDocument,
|
||||
group: 'files'
|
||||
}));
|
||||
}) satisfies Searcher;
|
||||
|
||||
@@ -4,9 +4,10 @@ import { project } from '$routes/(console)/project-[region]-[project]/store';
|
||||
import { get } from 'svelte/store';
|
||||
import type { Searcher } from '../commands';
|
||||
import type { Models } from '@appwrite.io/console';
|
||||
import { page } from '$app/stores';
|
||||
import { page } from '$app/state';
|
||||
import { showCreateDeployment } from '$routes/(console)/project-[region]-[project]/functions/function-[function]/store';
|
||||
import { base } from '$app/paths';
|
||||
import { IconLightningBolt, IconPlus } from '@appwrite.io/pink-icons-svelte';
|
||||
|
||||
const getFunctionCommand = (fn: Models.Function, region: string, projectId: string) => {
|
||||
return {
|
||||
@@ -15,15 +16,14 @@ const getFunctionCommand = (fn: Models.Function, region: string, projectId: stri
|
||||
goto(`${base}/project-${region}-${projectId}/functions/function-${fn.$id}`);
|
||||
},
|
||||
group: 'functions',
|
||||
icon: 'lightning-bolt'
|
||||
icon: IconLightningBolt
|
||||
} as const;
|
||||
};
|
||||
|
||||
export const functionsSearcher = (async (query: string) => {
|
||||
const $page = get(page);
|
||||
const projectId = get(project).$id;
|
||||
const { functions } = await sdk
|
||||
.forProject($page.params.region, $page.params.project)
|
||||
.forProject(page.params.region, page.params.project)
|
||||
.functions.list();
|
||||
|
||||
const filtered = functions.filter((fn) => fn.name.toLowerCase().includes(query.toLowerCase()));
|
||||
@@ -31,28 +31,27 @@ export const functionsSearcher = (async (query: string) => {
|
||||
if (filtered.length === 1) {
|
||||
const func = filtered[0];
|
||||
return [
|
||||
getFunctionCommand(func, $page.params.region, projectId),
|
||||
getFunctionCommand(func, page.params.region, projectId),
|
||||
{
|
||||
label: 'Create deployment',
|
||||
nested: true,
|
||||
async callback() {
|
||||
const $page = get(page);
|
||||
if (!$page.url.pathname.endsWith(func.$id)) {
|
||||
if (!page.url.pathname.endsWith(func.$id)) {
|
||||
await goto(
|
||||
`${base}/project-${$page.params.region}-${projectId}/functions/function-${func.$id}`
|
||||
`${base}/project-${page.params.region}-${projectId}/functions/function-${func.$id}`
|
||||
);
|
||||
}
|
||||
showCreateDeployment.set(true);
|
||||
},
|
||||
group: 'functions',
|
||||
icon: 'plus'
|
||||
icon: IconPlus
|
||||
},
|
||||
{
|
||||
label: 'Go to deployments',
|
||||
nested: true,
|
||||
callback() {
|
||||
goto(
|
||||
`${base}/project-${$page.params.region}-${projectId}/functions/function-${func.$id}`
|
||||
`${base}/project-${page.params.region}-${projectId}/functions/function-${func.$id}`
|
||||
);
|
||||
},
|
||||
group: 'functions'
|
||||
@@ -62,7 +61,7 @@ export const functionsSearcher = (async (query: string) => {
|
||||
nested: true,
|
||||
callback() {
|
||||
goto(
|
||||
`${base}/project-${$page.params.region}-${projectId}/functions/function-${func.$id}/usage`
|
||||
`${base}/project-${page.params.region}-${projectId}/functions/function-${func.$id}/usage`
|
||||
);
|
||||
},
|
||||
group: 'functions'
|
||||
@@ -72,7 +71,7 @@ export const functionsSearcher = (async (query: string) => {
|
||||
nested: true,
|
||||
callback() {
|
||||
goto(
|
||||
`${base}/project-${$page.params.region}-${projectId}/functions/function-${func.$id}/executions`
|
||||
`${base}/project-${page.params.region}-${projectId}/functions/function-${func.$id}/executions`
|
||||
);
|
||||
},
|
||||
group: 'functions'
|
||||
@@ -82,7 +81,7 @@ export const functionsSearcher = (async (query: string) => {
|
||||
nested: true,
|
||||
callback() {
|
||||
goto(
|
||||
`${base}/project-${$page.params.region}-${projectId}/functions/function-${func.$id}/settings`
|
||||
`${base}/project-${page.params.region}-${projectId}/functions/function-${func.$id}/settings`
|
||||
);
|
||||
},
|
||||
group: 'functions'
|
||||
@@ -90,5 +89,5 @@ export const functionsSearcher = (async (query: string) => {
|
||||
];
|
||||
}
|
||||
|
||||
return filtered.map((fn) => getFunctionCommand(fn, $page.params.region, projectId));
|
||||
return filtered.map((fn) => getFunctionCommand(fn, page.params.region, projectId));
|
||||
}) satisfies Searcher;
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
import { goto } from '$app/navigation';
|
||||
import { get } from 'svelte/store';
|
||||
import { type Searcher } from '../commands';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { MessagingProviderType, type Models } from '@appwrite.io/console';
|
||||
import { base } from '$app/paths';
|
||||
import { page } from '$app/stores';
|
||||
import { IconAnnotation, IconDeviceMobile, IconMail } from '@appwrite.io/pink-icons-svelte';
|
||||
import { page } from '$app/state';
|
||||
|
||||
const getLabel = (message) => {
|
||||
const getLabel = (message: Models.Message) => {
|
||||
switch (message.providerType) {
|
||||
case MessagingProviderType.Push:
|
||||
return message.data.title;
|
||||
return message.data['title'];
|
||||
case MessagingProviderType.Sms:
|
||||
return message.data.content;
|
||||
return message.data['content'];
|
||||
case MessagingProviderType.Email:
|
||||
return message.data.subject;
|
||||
return message.data['subject'];
|
||||
default:
|
||||
return 'null';
|
||||
}
|
||||
@@ -22,20 +22,19 @@ const getLabel = (message) => {
|
||||
const getIcon = (message: Models.Message) => {
|
||||
switch (message.providerType) {
|
||||
case MessagingProviderType.Push:
|
||||
return 'device-mobile';
|
||||
return IconDeviceMobile;
|
||||
case MessagingProviderType.Sms:
|
||||
return 'annotation';
|
||||
return IconAnnotation;
|
||||
case MessagingProviderType.Email:
|
||||
return 'mail';
|
||||
return IconMail;
|
||||
default:
|
||||
return 'send';
|
||||
throw new Error('Unsupported provider type');
|
||||
}
|
||||
};
|
||||
|
||||
export const messagesSearcher = (async (query: string) => {
|
||||
const $page = get(page);
|
||||
const { messages } = await sdk
|
||||
.forProject($page.params.region, $page.params.project)
|
||||
.forProject(page.params.region, page.params.project)
|
||||
.messaging.listMessages([], query || undefined);
|
||||
|
||||
return messages
|
||||
@@ -47,7 +46,7 @@ export const messagesSearcher = (async (query: string) => {
|
||||
label: getLabel(message),
|
||||
callback: () => {
|
||||
goto(
|
||||
`${base}/project-${$page.params.region}-${$page.params.project}/messaging/message-${message.$id}`
|
||||
`${base}/project-${page.params.region}-${page.params.project}/messaging/message-${message.$id}`
|
||||
);
|
||||
},
|
||||
icon: getIcon(message)
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import { goto } from '$app/navigation';
|
||||
import { get } from 'svelte/store';
|
||||
import type { Searcher } from '../commands';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { getProviderDisplayNameAndIcon } from '$routes/(console)/project-[region]-[project]/messaging/provider.svelte';
|
||||
import { base } from '$app/paths';
|
||||
import { page } from '$app/stores';
|
||||
import { page } from '$app/state';
|
||||
|
||||
const getIcon = (provider: string) => {
|
||||
const { icon } = getProviderDisplayNameAndIcon(provider);
|
||||
@@ -12,9 +11,8 @@ const getIcon = (provider: string) => {
|
||||
};
|
||||
|
||||
export const providersSearcher = (async (query: string) => {
|
||||
const $page = get(page);
|
||||
const { providers } = await sdk
|
||||
.forProject($page.params.region, $page.params.project)
|
||||
.forProject(page.params.region, page.params.project)
|
||||
.messaging.listProviders([], query || undefined);
|
||||
|
||||
return providers
|
||||
@@ -26,7 +24,7 @@ export const providersSearcher = (async (query: string) => {
|
||||
label: provider.name,
|
||||
callback: () => {
|
||||
goto(
|
||||
`${base}/project-${$page.params.region}-${$page.params.project}/messaging/providers/provider-${provider.$id}`
|
||||
`${base}/project-${page.params.region}-${page.params.project}/messaging/providers/provider-${provider.$id}`
|
||||
);
|
||||
},
|
||||
image: getIcon(provider.provider)
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { goto } from '$app/navigation';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { get } from 'svelte/store';
|
||||
import type { Command, Searcher } from '../commands';
|
||||
import type { Models } from '@appwrite.io/console';
|
||||
import { base } from '$app/paths';
|
||||
import { page } from '$app/stores';
|
||||
import { IconUserCircle } from '@appwrite.io/pink-icons-svelte';
|
||||
import { page } from '$app/state';
|
||||
|
||||
const getTeamCommand = (team: Models.Team<Models.Preferences>, region: string, projectId: string) =>
|
||||
({
|
||||
@@ -13,23 +13,22 @@ const getTeamCommand = (team: Models.Team<Models.Preferences>, region: string, p
|
||||
goto(`${base}/project-${region}-${projectId}/auth/teams/team-${team.$id}`);
|
||||
},
|
||||
group: 'teams',
|
||||
icon: 'user-circle'
|
||||
icon: IconUserCircle
|
||||
}) satisfies Command;
|
||||
|
||||
export const teamSearcher = (async (query: string) => {
|
||||
const $page = get(page);
|
||||
const { teams } = await sdk
|
||||
.forProject($page.params.region, $page.params.project)
|
||||
.forProject(page.params.region, page.params.project)
|
||||
.teams.list([], query);
|
||||
|
||||
if (teams.length === 1) {
|
||||
return [
|
||||
getTeamCommand(teams[0], $page.params.region, $page.params.project),
|
||||
getTeamCommand(teams[0], page.params.region, page.params.project),
|
||||
{
|
||||
label: 'Go to members',
|
||||
callback: () => {
|
||||
goto(
|
||||
`${base}/project-${$page.params.region}-${$page.params.project}/auth/teams/team-${teams[0].$id}/members`
|
||||
`${base}/project-${page.params.region}-${page.params.project}/auth/teams/team-${teams[0].$id}/members`
|
||||
);
|
||||
},
|
||||
group: 'teams',
|
||||
@@ -40,7 +39,7 @@ export const teamSearcher = (async (query: string) => {
|
||||
label: 'Go to activity',
|
||||
callback: () => {
|
||||
goto(
|
||||
`${base}/project-${$page.params.region}-${$page.params.project}/auth/teams/team-${teams[0].$id}/activity`
|
||||
`${base}/project-${page.params.region}-${page.params.project}/auth/teams/team-${teams[0].$id}/activity`
|
||||
);
|
||||
},
|
||||
group: 'teams',
|
||||
@@ -48,5 +47,5 @@ export const teamSearcher = (async (query: string) => {
|
||||
}
|
||||
];
|
||||
}
|
||||
return teams.map((team) => getTeamCommand(team, $page.params.region, $page.params.project));
|
||||
return teams.map((team) => getTeamCommand(team, page.params.region, page.params.project));
|
||||
}) satisfies Searcher;
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
import { goto } from '$app/navigation';
|
||||
import { get } from 'svelte/store';
|
||||
import type { Searcher } from '../commands';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { base } from '$app/paths';
|
||||
import { page } from '$app/stores';
|
||||
import { IconChevronRight } from '@appwrite.io/pink-icons-svelte';
|
||||
import { page } from '$app/state';
|
||||
|
||||
export const topicsSearcher = (async (query: string) => {
|
||||
const $page = get(page);
|
||||
const { topics } = await sdk
|
||||
.forProject($page.params.region, $page.params.project)
|
||||
.forProject(page.params.region, page.params.project)
|
||||
.messaging.listTopics([], query || undefined);
|
||||
|
||||
return topics
|
||||
@@ -20,10 +19,10 @@ export const topicsSearcher = (async (query: string) => {
|
||||
label: topic.name,
|
||||
callback: () => {
|
||||
goto(
|
||||
`${base}/project-${$page.params.region}-${$page.params.project}/messaging/topics/topic-${topic.$id}`
|
||||
`${base}/project-${page.params.region}-${page.params.project}/messaging/topics/topic-${topic.$id}`
|
||||
);
|
||||
},
|
||||
icon: 'send'
|
||||
icon: IconChevronRight // TODO: @itznotabug - 'send' no replacement yet.
|
||||
}) as const
|
||||
);
|
||||
}) satisfies Searcher;
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { goto } from '$app/navigation';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { get } from 'svelte/store';
|
||||
import type { Command, Searcher } from '../commands';
|
||||
import type { Models } from '@appwrite.io/console';
|
||||
import { promptDeleteUser } from '$routes/(console)/project-[region]-[project]/auth/user-[user]/dangerZone.svelte';
|
||||
import { base } from '$app/paths';
|
||||
import { page } from '$app/stores';
|
||||
import { IconTrash, IconUserCircle } from '@appwrite.io/pink-icons-svelte';
|
||||
import { page } from '$app/state';
|
||||
|
||||
const getUserCommand = (user: Models.User<Models.Preferences>, region: string, projectId: string) =>
|
||||
({
|
||||
@@ -14,18 +14,17 @@ const getUserCommand = (user: Models.User<Models.Preferences>, region: string, p
|
||||
goto(`${base}/project-${region}-${projectId}/auth/user-${user.$id}`);
|
||||
},
|
||||
group: 'users',
|
||||
icon: 'user-circle'
|
||||
icon: IconUserCircle
|
||||
}) satisfies Command;
|
||||
|
||||
export const userSearcher = (async (query: string) => {
|
||||
const $page = get(page);
|
||||
const { users } = await sdk
|
||||
.forProject($page.params.region, $page.params.project)
|
||||
.forProject(page.params.region, page.params.project)
|
||||
.users.list([], query || undefined);
|
||||
|
||||
if (users.length === 1) {
|
||||
return [
|
||||
getUserCommand(users[0], $page.params.region, $page.params.project),
|
||||
getUserCommand(users[0], page.params.region, page.params.project),
|
||||
{
|
||||
label: 'Delete user',
|
||||
callback: () => {
|
||||
@@ -33,13 +32,13 @@ export const userSearcher = (async (query: string) => {
|
||||
},
|
||||
group: 'users',
|
||||
nested: true,
|
||||
icon: 'trash'
|
||||
icon: IconTrash
|
||||
},
|
||||
{
|
||||
label: 'Go to activity',
|
||||
callback: () => {
|
||||
goto(
|
||||
`${base}/project-${$page.params.region}-${$page.params.project}/auth/user-${users[0].$id}/activity`
|
||||
`${base}/project-${page.params.region}-${page.params.project}/auth/user-${users[0].$id}/activity`
|
||||
);
|
||||
},
|
||||
group: 'users',
|
||||
@@ -49,7 +48,7 @@ export const userSearcher = (async (query: string) => {
|
||||
label: 'Go to sessions',
|
||||
callback: () => {
|
||||
goto(
|
||||
`${base}/project-${$page.params.region}-${$page.params.project}/auth/user-${users[0].$id}/sessions`
|
||||
`${base}/project-${page.params.region}-${page.params.project}/auth/user-${users[0].$id}/sessions`
|
||||
);
|
||||
},
|
||||
group: 'users',
|
||||
@@ -59,7 +58,7 @@ export const userSearcher = (async (query: string) => {
|
||||
label: 'Go to memberships',
|
||||
callback: () => {
|
||||
goto(
|
||||
`${base}/project-${$page.params.region}-${$page.params.project}/auth/user-${users[0].$id}/memberships`
|
||||
`${base}/project-${page.params.region}-${page.params.project}/auth/user-${users[0].$id}/memberships`
|
||||
);
|
||||
},
|
||||
group: 'users',
|
||||
@@ -67,5 +66,5 @@ export const userSearcher = (async (query: string) => {
|
||||
}
|
||||
];
|
||||
}
|
||||
return users.map((user) => getUserCommand(user, $page.params.region, $page.params.project));
|
||||
return users.map((user) => getUserCommand(user, page.params.region, page.params.project));
|
||||
}) satisfies Searcher;
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import type { SvelteComponent } from 'svelte';
|
||||
import type { Component } from 'svelte';
|
||||
import { writable } from 'svelte/store';
|
||||
|
||||
export type SubPanel = {
|
||||
name: string;
|
||||
component: typeof SvelteComponent<unknown>;
|
||||
component: Component;
|
||||
};
|
||||
|
||||
type CastSubPanel = Omit<SubPanel, 'component'> & {
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
class="button is-text is-only-icon"
|
||||
aria-label="close alert box"
|
||||
on:click={() => dispatch('dismiss')}>
|
||||
<span class="icon-x" aria-hidden="true" />
|
||||
<span class="icon-x" aria-hidden="true"></span>
|
||||
</button>
|
||||
{/if}
|
||||
<span
|
||||
@@ -39,7 +39,7 @@
|
||||
class:icon-check-circle={type === 'success'}
|
||||
class:icon-exclamation={type === 'warning'}
|
||||
class:icon-exclamation-circle={type === 'error'}
|
||||
aria-hidden="true" />
|
||||
aria-hidden="true"></span>
|
||||
<div class="alert-content" data-private>
|
||||
{#if $$slots.title}
|
||||
<h6 class="alert-title">
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
<script lang="ts">
|
||||
export let size: number;
|
||||
export let src: string;
|
||||
export let name: string;
|
||||
export let color = 'transparent';
|
||||
import { Avatar } from '@appwrite.io/pink-svelte';
|
||||
|
||||
type AvatarProps = Partial<{
|
||||
src: string;
|
||||
alt: string;
|
||||
size: 'xs' | 's' | 'm' | 'l' | 'xl';
|
||||
empty: boolean;
|
||||
}>;
|
||||
|
||||
export let size: AvatarProps['size'] = 'm';
|
||||
export let src: AvatarProps['src'] = undefined;
|
||||
export let alt: AvatarProps['alt'];
|
||||
</script>
|
||||
|
||||
<img
|
||||
width={size}
|
||||
height={size}
|
||||
class="avatar"
|
||||
style="--size: {size / 16}rem; background-color: {color};"
|
||||
{src}
|
||||
title={name}
|
||||
alt={name} />
|
||||
<Avatar {alt} {src} {size}>
|
||||
<slot />
|
||||
</Avatar>
|
||||
|
||||
@@ -1,44 +1,32 @@
|
||||
<script lang="ts">
|
||||
import { AvatarGroup, Avatar, Icon } from '@appwrite.io/pink-svelte';
|
||||
import AvatarInitials from './avatarInitials.svelte';
|
||||
import type { ComponentProps, ComponentType } from 'svelte';
|
||||
|
||||
export let avatars: string[] = [];
|
||||
export let icons: string[] = [];
|
||||
export let icons: ComponentType[] = [];
|
||||
export let total = avatars.length;
|
||||
export let size = 40;
|
||||
export let avatarSize: keyof typeof Sizes = 'medium';
|
||||
export let bordered = false;
|
||||
export let color = '';
|
||||
|
||||
let classes = '';
|
||||
export { classes as class };
|
||||
|
||||
enum Sizes {
|
||||
xsmall = 'is-size-x-small',
|
||||
small = 'is-size-small',
|
||||
medium = '',
|
||||
large = 'is-size-large',
|
||||
xlarge = 'is-size-x-large'
|
||||
}
|
||||
export let size: ComponentProps<Avatar>['size'] = 'm';
|
||||
</script>
|
||||
|
||||
<ul class="avatars-group {classes}" class:is-with-border={bordered}>
|
||||
<AvatarGroup>
|
||||
{#each avatars as name, index}
|
||||
{#if index < 2}
|
||||
<li class="avatars-group-item">
|
||||
<AvatarInitials {size} {name} />
|
||||
</li>
|
||||
<AvatarInitials {size} {name} />
|
||||
{/if}
|
||||
{/each}
|
||||
|
||||
{#each icons as icon}
|
||||
<li class="avatars-group-item">
|
||||
<span class="avatar {Sizes[avatarSize]} {color}"><span class={`icon-${icon}`} /></span>
|
||||
</li>
|
||||
<Avatar {size}>
|
||||
<Icon {icon} size="s" />
|
||||
</Avatar>
|
||||
{/each}
|
||||
|
||||
{#if total > 2}
|
||||
<li class="avatars-group-item">
|
||||
<div class="avatar {Sizes[avatarSize]} {color}">+{total - 2}</div>
|
||||
</li>
|
||||
<Avatar {size}>
|
||||
<span style:font-size="10px">
|
||||
+{total - 2}
|
||||
</span>
|
||||
</Avatar>
|
||||
{/if}
|
||||
</ul>
|
||||
</AvatarGroup>
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
<script lang="ts">
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import type { ComponentProps } from 'svelte';
|
||||
import Avatar from './avatar.svelte';
|
||||
|
||||
export let name: string;
|
||||
export let size: number;
|
||||
export let background: string | undefined = undefined;
|
||||
export let color = 'black';
|
||||
type AvatarProps = ComponentProps<Avatar>;
|
||||
|
||||
$: src = sdk.forConsole.avatars.getInitials(name, size * 2, size * 2, background);
|
||||
export let name: string;
|
||||
export let size: AvatarProps['size'] = 'm';
|
||||
export let background: string | undefined = undefined;
|
||||
|
||||
$: src = sdk.forConsole.avatars.getInitials(name, 192, 192, background).toString();
|
||||
</script>
|
||||
|
||||
<Avatar {name} {size} {src} {color} />
|
||||
<Avatar alt={name} {size} {src} />
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/stores';
|
||||
import { page } from '$app/state';
|
||||
import { BillingPlan } from '$lib/constants';
|
||||
import { Button } from '$lib/elements/forms';
|
||||
import { organization } from '$lib/stores/organization';
|
||||
@@ -8,6 +8,8 @@
|
||||
import { upgradeURL } from '$lib/stores/billing';
|
||||
import { hideNotification } from '$lib/helpers/notifications';
|
||||
import { backupsBannerId, showPolicyAlert } from '$lib/stores/database';
|
||||
import { IconX } from '@appwrite.io/pink-icons-svelte';
|
||||
import { Icon } from '@appwrite.io/pink-svelte';
|
||||
|
||||
function handleClose() {
|
||||
showPolicyAlert.set(false);
|
||||
@@ -15,7 +17,7 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if $showPolicyAlert && isCloud && $organization?.$id && $page.url.pathname.match(/\/databases\/database-[^/]+$/)}
|
||||
{#if $showPolicyAlert && isCloud && $organization?.$id && page.url.pathname.match(/\/databases\/database-[^/]+$/)}
|
||||
{@const isFreePlan = $organization?.billingPlan === BillingPlan.FREE}
|
||||
|
||||
{@const subtitle = isFreePlan
|
||||
@@ -23,7 +25,7 @@
|
||||
: 'Protect your data by quickly adding a backup policy'}
|
||||
|
||||
{@const ctaText = isFreePlan ? 'Upgrade plan' : 'Create policy'}
|
||||
{@const ctaURL = isFreePlan ? $upgradeURL : `${$page.url.pathname}/backups`}
|
||||
{@const ctaURL = isFreePlan ? $upgradeURL : `${page.url.pathname}/backups`}
|
||||
|
||||
<HeaderAlert type="warning" title="Your database has no backup policy">
|
||||
<svelte:fragment>{subtitle}</svelte:fragment>
|
||||
@@ -38,7 +40,7 @@
|
||||
</Button>
|
||||
|
||||
<Button text on:click={handleClose} event="backup_banner_close">
|
||||
<span class="icon-x" aria-hidden="true"></span>
|
||||
<Icon icon={IconX} slot="start" size="s" />
|
||||
</Button>
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
|
||||
@@ -7,11 +7,12 @@
|
||||
import { BillingPlan, Dependencies } from '$lib/constants';
|
||||
import type { BackupArchive, BackupRestoration } from '$lib/sdk/backups';
|
||||
import { goto, invalidate } from '$app/navigation';
|
||||
import { page } from '$app/stores';
|
||||
import { page } from '$app/state';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
import { base } from '$app/paths';
|
||||
import { getProjectId } from '$lib/helpers/project';
|
||||
import { toLocaleDate } from '$lib/helpers/date';
|
||||
import { Typography } from '@appwrite.io/pink-svelte';
|
||||
|
||||
const backupRestoreItems: {
|
||||
archives: Map<string, BackupArchive>;
|
||||
@@ -33,8 +34,8 @@
|
||||
|
||||
function showRestoreNotification(newDatabaseId: string, newDatabaseName: string) {
|
||||
if (newDatabaseId && newDatabaseName && lastDatabaseRestorationId !== newDatabaseId) {
|
||||
const region = $page.params.region;
|
||||
const project = $page.params.project;
|
||||
const region = page.params.region;
|
||||
const project = page.params.project;
|
||||
lastDatabaseRestorationId = newDatabaseId;
|
||||
|
||||
addNotification({
|
||||
@@ -126,7 +127,7 @@
|
||||
if (isSelfHosted || (isCloud && $organization.billingPlan === BillingPlan.FREE)) return;
|
||||
|
||||
return realtime
|
||||
.forProject($page.params.region, $page.params.project)
|
||||
.forProject(page.params.region, page.params.project)
|
||||
.subscribe('console', (response) => {
|
||||
if (!response.channels.includes(`projects.${getProjectId()}`)) return;
|
||||
|
||||
@@ -151,7 +152,9 @@
|
||||
<section class="upload-box">
|
||||
<header class="upload-box-header">
|
||||
<h4 class="upload-box-title">
|
||||
<span class="text">{titleText} ({items.size})</span>
|
||||
<Typography.Text variant="m-500">
|
||||
{titleText} ({items.size})
|
||||
</Typography.Text>
|
||||
</h4>
|
||||
<button
|
||||
class="upload-box-button"
|
||||
@@ -160,13 +163,13 @@
|
||||
on:click={() => {
|
||||
openStates[key] = !openStates[key];
|
||||
}}>
|
||||
<span class="icon-cheveron-up" aria-hidden="true" />
|
||||
<span class="icon-cheveron-up" aria-hidden="true"></span>
|
||||
</button>
|
||||
<button
|
||||
class="upload-box-button"
|
||||
aria-label="close backup restore box"
|
||||
on:click={() => handleClose(key)}>
|
||||
<span class="icon-x" aria-hidden="true" />
|
||||
<span class="icon-x" aria-hidden="true"></span>
|
||||
</button>
|
||||
</header>
|
||||
|
||||
@@ -177,18 +180,19 @@
|
||||
<section class="progress-bar u-width-full-line">
|
||||
<div
|
||||
class="progress-bar-top-line u-flex u-gap-8 u-main-space-between">
|
||||
<span class="body-text-2">
|
||||
<Typography.Text>
|
||||
{text(item.status, key)}
|
||||
</span>
|
||||
</Typography.Text>
|
||||
|
||||
<span class="backup-name">
|
||||
<Typography.Caption variant="400">
|
||||
{backupName(item, key)}
|
||||
</span>
|
||||
</Typography.Caption>
|
||||
</div>
|
||||
<div
|
||||
class="progress-bar-container"
|
||||
class:is-danger={item.status === 'failed'}
|
||||
style="--graph-size:{graphSize(item.status)}%" />
|
||||
style="--graph-size:{graphSize(item.status)}%">
|
||||
</div>
|
||||
</section>
|
||||
</li>
|
||||
{/each}
|
||||
@@ -200,7 +204,7 @@
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
<style lang="scss">
|
||||
.upload-box-title {
|
||||
font-size: 11px;
|
||||
}
|
||||
@@ -216,13 +220,17 @@
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.backup-name {
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
line-height: 130%;
|
||||
font-style: normal;
|
||||
letter-spacing: -0.12px;
|
||||
color: var(--mid-neutrals-50, #818186);
|
||||
font-family: var(--font-family-sansSerif, Inter);
|
||||
.progress-bar-container {
|
||||
height: 4px;
|
||||
|
||||
&::before {
|
||||
height: 4px;
|
||||
background-color: var(--bgcolor-neutral-invert);
|
||||
}
|
||||
|
||||
&.is-danger::before {
|
||||
height: 4px;
|
||||
background-color: var(--bgcolor-error);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { base } from '$app/paths';
|
||||
import { page } from '$app/stores';
|
||||
import { trackEvent } from '$lib/actions/analytics';
|
||||
import { page } from '$app/state';
|
||||
import { Click, trackEvent } from '$lib/actions/analytics';
|
||||
import { BillingPlan } from '$lib/constants';
|
||||
import { Button } from '$lib/elements/forms';
|
||||
import { HeaderAlert } from '$lib/layout';
|
||||
@@ -9,7 +9,7 @@
|
||||
import { organization } from '$lib/stores/organization';
|
||||
</script>
|
||||
|
||||
{#if $organization?.$id && $organization?.billingPlan === BillingPlan.FREE && $readOnly && !hideBillingHeaderRoutes.includes($page.url.pathname)}
|
||||
{#if $organization?.$id && $organization?.billingPlan === BillingPlan.FREE && $readOnly && !hideBillingHeaderRoutes.includes(page.url.pathname)}
|
||||
<HeaderAlert
|
||||
type="error"
|
||||
title={`${$organization.name} usage has reached the ${tierToPlan($organization.billingPlan).name} plan limit`}>
|
||||
@@ -26,7 +26,7 @@
|
||||
<Button
|
||||
href={$upgradeURL}
|
||||
on:click={() => {
|
||||
trackEvent('click_organization_upgrade', {
|
||||
trackEvent(Click.OrganizationClickUpgrade, {
|
||||
from: 'button',
|
||||
source: 'limit_reached_banner'
|
||||
});
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/stores';
|
||||
import { page } from '$app/state';
|
||||
import { HeaderAlert } from '$lib/layout';
|
||||
import { hideBillingHeaderRoutes } from '$lib/stores/billing';
|
||||
import { organization } from '$lib/stores/organization';
|
||||
</script>
|
||||
|
||||
{#if $organization?.markedForDeletion && !hideBillingHeaderRoutes.includes($page.url.pathname)}
|
||||
{#if $organization?.markedForDeletion && !hideBillingHeaderRoutes.includes(page.url.pathname)}
|
||||
<HeaderAlert title="Organization flagged for deletion">
|
||||
<svelte:fragment>
|
||||
All existing projects in the {$organization.name} organization have been paused. This organization
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { base } from '$app/paths';
|
||||
import { page } from '$app/stores';
|
||||
import { page } from '$app/state';
|
||||
import { BillingPlan } from '$lib/constants';
|
||||
import { Button } from '$lib/elements/forms';
|
||||
import { HeaderAlert } from '$lib/layout';
|
||||
@@ -8,7 +8,7 @@
|
||||
import { orgMissingPaymentMethod } from '$routes/(console)/store';
|
||||
</script>
|
||||
|
||||
{#if ($orgMissingPaymentMethod.billingPlan === BillingPlan.PRO || $orgMissingPaymentMethod.billingPlan === BillingPlan.SCALE) && !$orgMissingPaymentMethod.paymentMethodId && !$orgMissingPaymentMethod.backupPaymentMethodId && !hideBillingHeaderRoutes.includes($page.url.pathname)}
|
||||
{#if ($orgMissingPaymentMethod.billingPlan === BillingPlan.PRO || $orgMissingPaymentMethod.billingPlan === BillingPlan.SCALE) && !$orgMissingPaymentMethod.paymentMethodId && !$orgMissingPaymentMethod.backupPaymentMethodId && !hideBillingHeaderRoutes.includes(page.url.pathname)}
|
||||
<HeaderAlert
|
||||
type="error"
|
||||
title={`Payment method required for ${$orgMissingPaymentMethod.name}`}>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { base } from '$app/paths';
|
||||
import { page } from '$app/stores';
|
||||
import { trackEvent } from '$lib/actions/analytics';
|
||||
import { page } from '$app/state';
|
||||
import { Click, trackEvent } from '$lib/actions/analytics';
|
||||
import { BillingPlan, NEW_DEV_PRO_UPGRADE_COUPON } from '$lib/constants';
|
||||
import { Button } from '$lib/elements/forms';
|
||||
import { organization } from '$lib/stores/organization';
|
||||
@@ -21,7 +21,7 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if show && $organization?.$id && $organization?.billingPlan === BillingPlan.FREE && !$page.url.pathname.includes('/console/account')}
|
||||
{#if show && $organization?.$id && $organization?.billingPlan === BillingPlan.FREE && !page.url.pathname.includes('/console/account')}
|
||||
<GradientBanner on:close={handleClose}>
|
||||
<div class="u-flex u-gap-24 u-main-center u-cross-center u-flex-vertical-mobile">
|
||||
<span class="body-text-1">Get $50 Cloud credits for Appwrite Pro.</span>
|
||||
@@ -31,7 +31,7 @@
|
||||
class="u-line-height-1"
|
||||
href={`${base}/apply-credit?code=${NEW_DEV_PRO_UPGRADE_COUPON}&org=${$organization.$id}`}
|
||||
on:click={() => {
|
||||
trackEvent('click_credits_redeem', {
|
||||
trackEvent(Click.CreditsRedeemClick, {
|
||||
from: 'button',
|
||||
source: 'cloud_credits_banner',
|
||||
campaign: 'WelcomeManual'
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { base } from '$app/paths';
|
||||
import { page } from '$app/stores';
|
||||
import { page } from '$app/state';
|
||||
import { Button } from '$lib/elements/forms';
|
||||
import { HeaderAlert } from '$lib/layout';
|
||||
import { actionRequiredInvoices, hideBillingHeaderRoutes } from '$lib/stores/billing';
|
||||
@@ -9,7 +9,7 @@
|
||||
const endpoint = getApiEndpoint();
|
||||
</script>
|
||||
|
||||
{#if $actionRequiredInvoices && $actionRequiredInvoices?.invoices?.length && !hideBillingHeaderRoutes.includes($page.url.pathname)}
|
||||
{#if $actionRequiredInvoices && $actionRequiredInvoices?.invoices?.length && !hideBillingHeaderRoutes.includes(page.url.pathname)}
|
||||
<HeaderAlert title="Authorization required" type="error">
|
||||
Please authorize your upcoming payment for {$organization.name}. Your bank requires this
|
||||
security measure to proceed with payment.
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
<script lang="ts">
|
||||
import { base } from '$app/paths';
|
||||
import { page } from '$app/stores';
|
||||
import { page } from '$app/state';
|
||||
import { Button } from '$lib/elements/forms';
|
||||
import { HeaderAlert } from '$lib/layout';
|
||||
import { failedInvoice } from '$lib/stores/billing';
|
||||
import { organization } from '$lib/stores/organization';
|
||||
|
||||
$: isOnProjects = $page.route.id.includes('project-[project]');
|
||||
$: isOnProjects = page.route.id.includes('project-[region]-[project]');
|
||||
</script>
|
||||
|
||||
{#if $failedInvoice && $failedInvoice.teamId === $organization.$id && isOnProjects}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/stores';
|
||||
import { page } from '$app/state';
|
||||
import { Button } from '$lib/elements/forms';
|
||||
import { HeaderAlert } from '$lib/layout';
|
||||
import { hideBillingHeaderRoutes, paymentMissingMandate } from '$lib/stores/billing';
|
||||
@@ -16,7 +16,7 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if $paymentMissingMandate && $paymentMissingMandate?.country?.toLowerCase() === 'in' && $paymentMissingMandate.mandateId === null && !hideBillingHeaderRoutes.includes($page.url.pathname)}
|
||||
{#if $paymentMissingMandate && $paymentMissingMandate?.country?.toLowerCase() === 'in' && $paymentMissingMandate.mandateId === null && !hideBillingHeaderRoutes.includes(page.url.pathname)}
|
||||
<HeaderAlert title="Authorization required" type="info">
|
||||
The payment method for {$organization.name} needs to be verified.
|
||||
<svelte:fragment slot="buttons">
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
<script lang="ts">
|
||||
import { base } from '$app/paths';
|
||||
import { page } from '$app/stores';
|
||||
import { page } from '$app/state';
|
||||
import { Button } from '$lib/elements/forms';
|
||||
import { diffDays, toLocaleDate } from '$lib/helpers/date';
|
||||
import { HeaderAlert } from '$lib/layout';
|
||||
import { failedInvoice, hideBillingHeaderRoutes } from '$lib/stores/billing';
|
||||
</script>
|
||||
|
||||
{#if $failedInvoice && !hideBillingHeaderRoutes.includes($page.url.pathname)}
|
||||
{#if $failedInvoice && !hideBillingHeaderRoutes.includes(page.url.pathname)}
|
||||
{@const daysPassed = diffDays(new Date($failedInvoice.dueAt), new Date())}
|
||||
<HeaderAlert title="Your projects are at risk">
|
||||
<svelte:fragment>
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
<script lang="ts">
|
||||
import { Button, FormList, InputText } from '$lib/elements/forms';
|
||||
import { Button, InputText } from '$lib/elements/forms';
|
||||
import { formatCurrency } from '$lib/helpers/numbers';
|
||||
import type { Coupon } from '$lib/sdk/billing';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { Layout } from '@appwrite.io/pink-svelte';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
@@ -36,42 +37,36 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<FormList gap={8}>
|
||||
<Layout.Stack direction="row" gap="s" wrap="wrap" alignItems="center">
|
||||
<InputText
|
||||
placeholder="Coupon code"
|
||||
id="code"
|
||||
label="Add credits"
|
||||
{required}
|
||||
hideRequired
|
||||
disabled={couponData?.status === 'active'}
|
||||
bind:value={coupon}>
|
||||
<Button
|
||||
secondary
|
||||
disabled={couponData?.status === 'active' || !coupon}
|
||||
on:click={addCoupon}>
|
||||
Apply
|
||||
</Button>
|
||||
</InputText>
|
||||
{#if couponData?.status === 'error'}
|
||||
<Button secondary disabled={couponData?.status === 'active' || !coupon} on:click={addCoupon}>
|
||||
Apply
|
||||
</Button>
|
||||
</Layout.Stack>
|
||||
{#if couponData?.status === 'error'}
|
||||
<div>
|
||||
<span class="icon-exclamation-circle u-color-text-danger"></span>
|
||||
<span>
|
||||
{couponData.code.toUpperCase()} is not a valid promo code
|
||||
</span>
|
||||
</div>
|
||||
{:else if couponData?.status === 'active'}
|
||||
<div class="u-flex u-main-space-between u-cross-center">
|
||||
<div>
|
||||
<span class="icon-exclamation-circle u-color-text-danger" />
|
||||
<span>
|
||||
{couponData.code.toUpperCase()} is not a valid promo code
|
||||
</span>
|
||||
<span class="icon-tag u-color-text-success"></span>
|
||||
<slot data={couponData}>
|
||||
<span>
|
||||
{couponData.code.toUpperCase()} applied (-{formatCurrency(couponData.credits)})
|
||||
</span>
|
||||
</slot>
|
||||
</div>
|
||||
{:else if couponData?.status === 'active'}
|
||||
<div class="u-flex u-main-space-between u-cross-center">
|
||||
<div>
|
||||
<span class="icon-tag u-color-text-success" />
|
||||
<slot data={couponData}>
|
||||
<span>
|
||||
{couponData.code.toUpperCase()} applied (-{formatCurrency(
|
||||
couponData.credits
|
||||
)})
|
||||
</span>
|
||||
</slot>
|
||||
</div>
|
||||
<Button round text on:click={removeCoupon}><span class="icon-x"></span></Button>
|
||||
</div>
|
||||
{/if}
|
||||
</FormList>
|
||||
<Button icon text on:click={removeCoupon}><span class="icon-x"></span></Button>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { tooltip } from '$lib/actions/tooltip';
|
||||
import { formatCurrency } from '$lib/helpers/numbers';
|
||||
import type { Coupon } from '$lib/sdk/billing';
|
||||
import { Tooltip } from '@appwrite.io/pink-svelte';
|
||||
|
||||
export let couponData: Partial<Coupon> = {
|
||||
code: null,
|
||||
@@ -15,13 +15,16 @@
|
||||
<span class="u-flex u-main-space-between">
|
||||
<div class="u-flex u-cross-center u-gap-4">
|
||||
<p class="text">
|
||||
<span class="icon-tag u-color-text-success" aria-hidden="true" />
|
||||
<span class="icon-tag u-color-text-success" aria-hidden="true"></span>
|
||||
{#if couponData.credits >= 100}
|
||||
{couponData?.code?.toUpperCase()}
|
||||
{:else}
|
||||
<span use:tooltip={{ content: couponData?.code?.toUpperCase() }}>
|
||||
Credits applied
|
||||
</span>
|
||||
<span
|
||||
><Tooltip
|
||||
>Credits applied <span slot="tooltip"
|
||||
>{couponData?.code?.toUpperCase()}</span
|
||||
></Tooltip
|
||||
></span>
|
||||
{/if}
|
||||
</p>
|
||||
{#if !fixedCoupon}
|
||||
@@ -37,7 +40,7 @@
|
||||
status: null,
|
||||
credits: null
|
||||
})}>
|
||||
<span class="icon-x" aria-hidden="true" />
|
||||
<span class="icon-x" aria-hidden="true"></span>
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
<script lang="ts">
|
||||
import { formatCurrency } from '$lib/helpers/numbers';
|
||||
import type { Coupon } from '$lib/sdk/billing';
|
||||
import { IconX } from '@appwrite.io/pink-icons-svelte';
|
||||
import { Icon } from '@appwrite.io/pink-svelte';
|
||||
|
||||
export let label: string;
|
||||
export let value: number;
|
||||
@@ -16,7 +18,7 @@
|
||||
<span class="u-flex u-main-space-between">
|
||||
<div class="u-flex u-cross-center u-gap-4">
|
||||
<p class="text">
|
||||
<span class="icon-tag u-color-text-success" aria-hidden="true" />
|
||||
<span class="icon-tag u-color-text-success" aria-hidden="true"></span>
|
||||
<span>
|
||||
{label}
|
||||
</span>
|
||||
@@ -34,7 +36,7 @@
|
||||
status: null,
|
||||
credits: null
|
||||
})}>
|
||||
<span class="icon-x" aria-hidden="true" />
|
||||
<Icon icon={IconX} />
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
<script lang="ts">
|
||||
import { trackEvent } from '$lib/actions/analytics';
|
||||
import { Click, trackEvent } from '$lib/actions/analytics';
|
||||
import { BillingPlan } from '$lib/constants';
|
||||
import { Button } from '$lib/elements/forms';
|
||||
import { tierToPlan, upgradeURL } from '$lib/stores/billing';
|
||||
import { Layout, Typography } from '@appwrite.io/pink-svelte';
|
||||
import { Card } from '..';
|
||||
|
||||
export let service: string;
|
||||
@@ -11,25 +12,24 @@
|
||||
|
||||
<Card>
|
||||
<slot>
|
||||
<div class="u-flex u-flex-vertical u-main-center u-cross-center u-gap-8">
|
||||
<h6 class="body-text-1 u-bold u-trim-1">Upgrade to add {service}</h6>
|
||||
<p class="text u-text-center">
|
||||
<Layout.Stack alignItems="center">
|
||||
<Typography.Text variant="m-600">Upgrade to add {service}</Typography.Text>
|
||||
<Typography.Text>
|
||||
Upgrade to a {tierToPlan(BillingPlan.PRO).name} plan to add {service} to your organization
|
||||
</p>
|
||||
</Typography.Text>
|
||||
|
||||
<Button
|
||||
class="u-margin-block-start-16"
|
||||
secondary
|
||||
fullWidthMobile
|
||||
href={$upgradeURL}
|
||||
on:click={() => {
|
||||
trackEvent('click_organization_upgrade', {
|
||||
trackEvent(Click.OrganizationClickUpgrade, {
|
||||
from: 'button',
|
||||
source: eventSource
|
||||
});
|
||||
}}>
|
||||
Upgrade
|
||||
</Button>
|
||||
</div>
|
||||
</Layout.Stack>
|
||||
</slot>
|
||||
</Card>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { FormList, InputChoice, InputNumber } from '$lib/elements/forms';
|
||||
import { InputChoice, InputNumber } from '$lib/elements/forms';
|
||||
import { formatCurrency } from '$lib/helpers/numbers';
|
||||
import type { Coupon, Estimation } from '$lib/sdk/billing';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
@@ -107,7 +107,7 @@
|
||||
{#each estimation.discounts ?? [] as item}
|
||||
<DiscountsApplied {fixedCoupon} bind:couponData {...item} />
|
||||
{/each}
|
||||
<div class="u-sep-block-start" />
|
||||
<div class="u-sep-block-start"></div>
|
||||
<span class="u-flex u-main-space-between">
|
||||
<p class="text">Total due</p>
|
||||
<p class="text">
|
||||
@@ -122,25 +122,23 @@
|
||||
</p>
|
||||
{/if}
|
||||
|
||||
<FormList class="u-margin-block-start-24">
|
||||
<InputChoice
|
||||
type="switchbox"
|
||||
id="budget"
|
||||
label="Enable budget cap"
|
||||
tooltip="If enabled, you will be notified when your spending reaches 75% of the set cap. Update cap alerts in your organization settings."
|
||||
fullWidth
|
||||
bind:value={budgetEnabled}>
|
||||
{#if budgetEnabled}
|
||||
<div class="u-margin-block-start-16">
|
||||
<InputNumber
|
||||
id="budget"
|
||||
label="Budget cap (USD)"
|
||||
placeholder="0"
|
||||
min={0}
|
||||
bind:value={billingBudget} />
|
||||
</div>
|
||||
{/if}
|
||||
</InputChoice>
|
||||
</FormList>
|
||||
<InputChoice
|
||||
type="switchbox"
|
||||
id="budget"
|
||||
label="Enable budget cap"
|
||||
tooltip="If enabled, you will be notified when your spending reaches 75% of the set cap. Update cap alerts in your organization settings."
|
||||
fullWidth
|
||||
bind:value={budgetEnabled}>
|
||||
{#if budgetEnabled}
|
||||
<div class="u-margin-block-start-16">
|
||||
<InputNumber
|
||||
id="budget"
|
||||
label="Budget cap (USD)"
|
||||
placeholder="0"
|
||||
min={0}
|
||||
bind:value={billingBudget} />
|
||||
</div>
|
||||
{/if}
|
||||
</InputChoice>
|
||||
</Card>
|
||||
{/if}
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
<script lang="ts">
|
||||
import { InputChoice, InputNumber } from '$lib/elements/forms';
|
||||
import { toLocaleDate } from '$lib/helpers/date';
|
||||
import { formatCurrency } from '$lib/helpers/numbers';
|
||||
import type { Coupon, PlansMap } from '$lib/sdk/billing';
|
||||
import { type Tier } from '$lib/stores/billing';
|
||||
import { Card, Divider, Layout, Typography } from '@appwrite.io/pink-svelte';
|
||||
import { CreditsApplied } from '.';
|
||||
|
||||
export let billingPlan: Tier;
|
||||
export let collaborators: string[];
|
||||
export let couponData: Partial<Coupon>;
|
||||
export let plans: PlansMap;
|
||||
export let billingBudget: number;
|
||||
export let fixedCoupon = false; // If true, the coupon cannot be removed
|
||||
export let isDowngrade = false;
|
||||
|
||||
const today = new Date();
|
||||
const billingPayDate = new Date(today.getTime() + 30 * 24 * 60 * 60 * 1000);
|
||||
|
||||
let budgetEnabled = false;
|
||||
|
||||
$: currentPlan = plans.get(billingPlan);
|
||||
$: extraSeatsCost = 0; // 0 untile trial period later replace (collaborators?.length ?? 0) * (currentPlan?.addons?.member?.price ?? 0);
|
||||
$: grossCost = currentPlan.price + extraSeatsCost;
|
||||
$: estimatedTotal =
|
||||
couponData?.status === 'active'
|
||||
? grossCost - couponData.credits >= 0
|
||||
? grossCost - couponData.credits
|
||||
: 0
|
||||
: grossCost;
|
||||
$: trialEndDate = new Date(
|
||||
billingPayDate.getTime() + currentPlan.trialDays * 24 * 60 * 60 * 1000
|
||||
);
|
||||
</script>
|
||||
|
||||
<Card.Base padding="s">
|
||||
<Layout.Stack>
|
||||
<slot />
|
||||
<Layout.Stack direction="row" justifyContent="space-between">
|
||||
<Typography.Text>{currentPlan.name} plan</Typography.Text>
|
||||
<Typography.Text>{formatCurrency(currentPlan.price)}</Typography.Text>
|
||||
</Layout.Stack>
|
||||
<Layout.Stack direction="row" justifyContent="space-between">
|
||||
<Typography.Text variant={isDowngrade ? 'm-500' : 'm-400'}
|
||||
>Additional seats ({collaborators?.length ?? 0})</Typography.Text>
|
||||
<Typography.Text variant={isDowngrade ? 'm-500' : 'm-400'}
|
||||
>{formatCurrency(extraSeatsCost)}</Typography.Text>
|
||||
</Layout.Stack>
|
||||
{#if couponData?.status === 'active'}
|
||||
<CreditsApplied bind:couponData {fixedCoupon} />
|
||||
{/if}
|
||||
<Divider />
|
||||
<Layout.Stack direction="row" justifyContent="space-between">
|
||||
<Typography.Text>
|
||||
Upcoming charge<br />
|
||||
Due on {!currentPlan.trialDays
|
||||
? toLocaleDate(billingPayDate.toString())
|
||||
: toLocaleDate(trialEndDate.toString())}</Typography.Text>
|
||||
<Typography.Text>{formatCurrency(estimatedTotal)}</Typography.Text>
|
||||
</Layout.Stack>
|
||||
|
||||
<Typography.Text>
|
||||
You'll pay <b>{formatCurrency(estimatedTotal)}</b> now, with your first billing cycle
|
||||
starting on
|
||||
<b
|
||||
>{!currentPlan.trialDays
|
||||
? toLocaleDate(billingPayDate.toString())
|
||||
: toLocaleDate(trialEndDate.toString())}</b
|
||||
>. {#if couponData?.status === 'active'}Once your credits run out, you'll be charged
|
||||
<b>{formatCurrency(currentPlan.price)}</b> plus usage fees every 30 days.
|
||||
{/if}
|
||||
</Typography.Text>
|
||||
|
||||
<InputChoice
|
||||
type="switchbox"
|
||||
id="budget"
|
||||
label="Enable budget cap"
|
||||
tooltip="If enabled, you will be notified when your spending reaches 75% of the set cap. Update cap alerts in your organization settings."
|
||||
fullWidth
|
||||
bind:value={budgetEnabled}>
|
||||
{#if budgetEnabled}
|
||||
<div class="u-margin-block-start-16">
|
||||
<InputNumber
|
||||
required
|
||||
autofocus
|
||||
id="budget"
|
||||
label="Budget cap (USD)"
|
||||
placeholder="0"
|
||||
min={0}
|
||||
bind:value={billingBudget} />
|
||||
</div>
|
||||
{/if}
|
||||
</InputChoice>
|
||||
</Layout.Stack>
|
||||
</Card.Base>
|
||||
@@ -1,11 +1,12 @@
|
||||
<script lang="ts">
|
||||
import { FormList, InputChoice, InputText } from '$lib/elements/forms';
|
||||
import { onDestroy, onMount } from 'svelte';
|
||||
import { CreditCardBrandImage, RadioBoxes } from '..';
|
||||
import { unmountPaymentElement } from '$lib/stores/stripe';
|
||||
import { Pill } from '$lib/elements';
|
||||
import { InputChoice, InputText } from '$lib/elements/forms';
|
||||
import { onMount } from 'svelte';
|
||||
import { CreditCardBrandImage } from '..';
|
||||
import { initializeStripe, unmountPaymentElement } from '$lib/stores/stripe';
|
||||
import { Badge, Card, Layout } from '@appwrite.io/pink-svelte';
|
||||
import type { PaymentMethodData } from '$lib/sdk/billing';
|
||||
|
||||
export let methods: Record<string, unknown>[];
|
||||
export let methods: PaymentMethodData[];
|
||||
export let group: string;
|
||||
export let name: string;
|
||||
export let defaultMethod: string = null;
|
||||
@@ -36,61 +37,48 @@
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
observer.disconnect();
|
||||
unmountPaymentElement();
|
||||
return () => {
|
||||
observer.disconnect();
|
||||
unmountPaymentElement();
|
||||
};
|
||||
});
|
||||
|
||||
$: if (element) {
|
||||
initializeStripe(element);
|
||||
observer.observe(element, { childList: true });
|
||||
}
|
||||
|
||||
//Set setAsDefault as false when group changes
|
||||
$: if (group || group === null) {
|
||||
$: if (group || group === '$new') {
|
||||
setAsDefault = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
<RadioBoxes
|
||||
elements={methods}
|
||||
total={methods?.length}
|
||||
variableName="$id"
|
||||
name="payment"
|
||||
bind:group
|
||||
{disabledCondition}>
|
||||
<svelte:fragment slot="element" let:element>
|
||||
<slot {element}>
|
||||
<span class="u-flex u-gap-16 u-flex-vertical">
|
||||
<span class="u-flex u-gap-16">
|
||||
<span class="u-flex u-cross-center u-gap-8" style="padding-inline:0.25rem">
|
||||
<span>
|
||||
<span class="u-capitalize">{element.brand}</span> ending in {element.last4}</span>
|
||||
<CreditCardBrandImage brand={element.brand?.toString()} />
|
||||
</span>
|
||||
{#if element.$id === backupMethod}
|
||||
<Pill>Backup</Pill>
|
||||
{:else if element.$id === defaultMethod}
|
||||
<Pill>Default</Pill>
|
||||
{/if}
|
||||
</span>
|
||||
{#if !!defaultMethod && element.$id !== defaultMethod && group === element.$id && showSetAsDefault && element.$id !== backupMethod}
|
||||
<ul>
|
||||
<InputChoice
|
||||
bind:value={setAsDefault}
|
||||
id="default"
|
||||
label="Set as default payment method for this organization" />
|
||||
</ul>
|
||||
<Layout.Stack>
|
||||
{#each methods as method}
|
||||
{@const value = method.$id}
|
||||
<Card.Selector
|
||||
title={method.name}
|
||||
name={value}
|
||||
bind:group
|
||||
{value}
|
||||
disabled={disabledCondition ? value === disabledCondition : false}>
|
||||
<svelte:fragment slot="action">
|
||||
{#if method.$id === backupMethod}
|
||||
<Badge variant="secondary" content="Backup" size="xs" />
|
||||
{:else if method.$id === defaultMethod}
|
||||
<Badge variant="secondary" content="Default" size="xs" />
|
||||
{/if}
|
||||
</span>
|
||||
</slot>
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="new">
|
||||
<span style="padding-inline:0.25rem">Add new payment method</span>
|
||||
</svelte:fragment>
|
||||
|
||||
<FormList class="u-margin-block-start-8" gap={16}>
|
||||
</svelte:fragment>
|
||||
<Layout.Stack direction="row" alignItems="center" gap="s">
|
||||
{method.brand} ending in {method.last4}
|
||||
<CreditCardBrandImage brand={method.brand?.toString()} />
|
||||
</Layout.Stack>
|
||||
</Card.Selector>
|
||||
{/each}
|
||||
<Card.Selector title="Add new payment method" name="$new" bind:group value="$new" />
|
||||
{#if group === '$new'}
|
||||
<InputText
|
||||
id="name"
|
||||
label="Cardholder name"
|
||||
@@ -103,20 +91,16 @@
|
||||
<div class="loader-container" bind:this={loader}>
|
||||
<div class="loader"></div>
|
||||
</div>
|
||||
<div id="payment-element" bind:this={element}>
|
||||
<!-- Stripe will create form elements here -->
|
||||
</div>
|
||||
<div bind:this={element}></div>
|
||||
</div>
|
||||
{#if showSetAsDefault}
|
||||
<ul>
|
||||
<InputChoice
|
||||
bind:value={setAsDefault}
|
||||
id="default"
|
||||
label="Set as default payment method for this organization" />
|
||||
</ul>
|
||||
<InputChoice
|
||||
bind:value={setAsDefault}
|
||||
id="default"
|
||||
label="Set as default payment method for this organization" />
|
||||
{/if}
|
||||
</FormList>
|
||||
</RadioBoxes>
|
||||
{/if}
|
||||
</Layout.Stack>
|
||||
|
||||
<style lang="scss">
|
||||
.aw-stripe-container {
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
<script lang="ts">
|
||||
import { FakeModal } from '$lib/components';
|
||||
import { InputText, Button, FormList } from '$lib/elements/forms';
|
||||
import { createEventDispatcher, onDestroy, onMount } from 'svelte';
|
||||
import { InputText, Button } from '$lib/elements/forms';
|
||||
import { createEventDispatcher, onMount } from 'svelte';
|
||||
import { initializeStripe, submitStripeCard } from '$lib/stores/stripe';
|
||||
import { invalidate } from '$app/navigation';
|
||||
import { Dependencies } from '$lib/constants';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
import { page } from '$app/stores';
|
||||
import { page } from '$app/state';
|
||||
import { Spinner } from '@appwrite.io/pink-svelte';
|
||||
|
||||
export let show = false;
|
||||
|
||||
@@ -15,16 +16,12 @@
|
||||
let name: string;
|
||||
let error: string;
|
||||
|
||||
onMount(async () => {
|
||||
await initializeStripe();
|
||||
});
|
||||
|
||||
async function handleSubmit() {
|
||||
try {
|
||||
const card = await submitStripeCard(name, $page?.params?.organization ?? null);
|
||||
const card = await submitStripeCard(name, page?.params?.organization ?? null);
|
||||
show = false;
|
||||
invalidate(Dependencies.PAYMENT_METHODS);
|
||||
dispatch('submit', card);
|
||||
show = false;
|
||||
addNotification({
|
||||
type: 'success',
|
||||
message: 'A new payment method has been added to your account'
|
||||
@@ -34,12 +31,13 @@
|
||||
}
|
||||
}
|
||||
|
||||
let isLoading = true;
|
||||
let element: HTMLElement;
|
||||
let loader: HTMLDivElement;
|
||||
|
||||
let observer: MutationObserver;
|
||||
|
||||
onMount(() => {
|
||||
initializeStripe(element);
|
||||
observer = new MutationObserver((mutationsList) => {
|
||||
for (let mutation of mutationsList) {
|
||||
if (mutation.type === 'childList') {
|
||||
@@ -49,18 +47,17 @@
|
||||
node instanceof Element &&
|
||||
node.className.toLowerCase().includes('__privatestripeelement')
|
||||
) {
|
||||
loader.style.display = 'none';
|
||||
isLoading = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
observer.disconnect();
|
||||
document.documentElement.classList.remove('u-overflow-hidden');
|
||||
return () => {
|
||||
observer.disconnect();
|
||||
};
|
||||
});
|
||||
|
||||
$: if (element) {
|
||||
@@ -68,32 +65,28 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<FakeModal
|
||||
bind:show
|
||||
title="Add payment method"
|
||||
bind:error
|
||||
onSubmit={handleSubmit}
|
||||
headerDivider={false}>
|
||||
<FormList gap={16}>
|
||||
<slot />
|
||||
<InputText
|
||||
id="name"
|
||||
label="Cardholder name"
|
||||
placeholder="Cardholder name"
|
||||
bind:value={name}
|
||||
required
|
||||
autofocus={true}
|
||||
hideRequired />
|
||||
<div class="aw-stripe-container" data-private>
|
||||
<div class="loader-container" bind:this={loader}>
|
||||
<div class="loader"></div>
|
||||
</div>
|
||||
<div id="payment-element" bind:this={element}>
|
||||
<!-- Stripe will create form elements here -->
|
||||
<FakeModal bind:show title="Add payment method" bind:error onSubmit={handleSubmit}>
|
||||
<slot />
|
||||
<InputText
|
||||
id="name"
|
||||
required
|
||||
autofocus={true}
|
||||
bind:value={name}
|
||||
label="Cardholder name"
|
||||
placeholder="Cardholder name" />
|
||||
|
||||
<div class="aw-stripe-container" data-private>
|
||||
{#if isLoading}
|
||||
<div class="loader-element">
|
||||
<Spinner />
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="stripe-element" bind:this={element}>
|
||||
<!-- Stripe will create form elements here -->
|
||||
</div>
|
||||
<slot name="end"></slot>
|
||||
</FormList>
|
||||
</div>
|
||||
<slot name="end"></slot>
|
||||
<svelte:fragment slot="footer">
|
||||
<Button secondary on:click={() => (show = false)}>Cancel</Button>
|
||||
<Button submit disabled={!name}>Add</Button>
|
||||
@@ -102,14 +95,17 @@
|
||||
|
||||
<style lang="scss">
|
||||
.aw-stripe-container {
|
||||
min-height: 295px;
|
||||
position: relative;
|
||||
.loader-container {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
z-index: 0;
|
||||
display: flex;
|
||||
min-height: 245px;
|
||||
|
||||
.stripe-element {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.loader-element {
|
||||
width: 100%;
|
||||
align-self: center;
|
||||
justify-items: end;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -2,38 +2,40 @@
|
||||
import { BillingPlan } from '$lib/constants';
|
||||
import { formatNum } from '$lib/helpers/string';
|
||||
import { plansInfo, tierFree, tierPro, tierScale, type Tier } from '$lib/stores/billing';
|
||||
import { Card, SecondaryTabs, SecondaryTabsItem } from '..';
|
||||
import { Card, Layout, Tabs, Typography } from '@appwrite.io/pink-svelte';
|
||||
|
||||
export let downgrade = false;
|
||||
|
||||
let selectedTab: Tier = BillingPlan.FREE;
|
||||
export let downgrade = false;
|
||||
|
||||
$: plan = $plansInfo.get(selectedTab);
|
||||
</script>
|
||||
|
||||
<Card style="--card-padding: 1.5rem">
|
||||
<div class="comparison-box">
|
||||
<SecondaryTabs stretch>
|
||||
<SecondaryTabsItem
|
||||
disabled={selectedTab === BillingPlan.FREE}
|
||||
<Card.Base>
|
||||
<Layout.Stack>
|
||||
<Tabs.Root stretch let:root>
|
||||
<Tabs.Item.Button
|
||||
{root}
|
||||
active={selectedTab === BillingPlan.FREE}
|
||||
on:click={() => (selectedTab = BillingPlan.FREE)}>
|
||||
{tierFree.name}
|
||||
</SecondaryTabsItem>
|
||||
<SecondaryTabsItem
|
||||
disabled={selectedTab === BillingPlan.PRO}
|
||||
</Tabs.Item.Button>
|
||||
<Tabs.Item.Button
|
||||
{root}
|
||||
active={selectedTab === BillingPlan.PRO}
|
||||
on:click={() => (selectedTab = BillingPlan.PRO)}>
|
||||
{tierPro.name}
|
||||
</SecondaryTabsItem>
|
||||
<SecondaryTabsItem
|
||||
disabled={selectedTab === BillingPlan.SCALE}
|
||||
</Tabs.Item.Button>
|
||||
<Tabs.Item.Button
|
||||
{root}
|
||||
active={selectedTab === BillingPlan.SCALE}
|
||||
on:click={() => (selectedTab = BillingPlan.SCALE)}>
|
||||
{tierScale.name}
|
||||
</SecondaryTabsItem>
|
||||
</SecondaryTabs>
|
||||
</div>
|
||||
</Tabs.Item.Button>
|
||||
</Tabs.Root>
|
||||
|
||||
<div class="u-margin-block-start-24">
|
||||
<Typography.Text variant="m-600">{plan.name} plan</Typography.Text>
|
||||
{#if selectedTab === BillingPlan.FREE}
|
||||
<h3 class="u-bold body-text-1">{plan.name} plan</h3>
|
||||
{#if downgrade}
|
||||
<ul class="u-margin-block-start-8 list u-gap-4 u-small">
|
||||
<li class="list-item u-gap-4 u-cross-center">
|
||||
@@ -67,37 +69,26 @@
|
||||
</li>
|
||||
</ul>
|
||||
{:else}
|
||||
<ul class="u-margin-block-start-8 un-order-list">
|
||||
<ul class="un-order-list">
|
||||
<li>
|
||||
<span class="text">
|
||||
Limited to {plan.databases} Database, {plan.buckets} Buckets, {plan.functions}
|
||||
Functions per project
|
||||
</span>
|
||||
Limited to {plan.databases} Database, {plan.buckets} Buckets, {plan.functions}
|
||||
Functions per project
|
||||
</li>
|
||||
<li>Limited to 1 organization member</li>
|
||||
<li>
|
||||
Limited to {plan.bandwidth}GB bandwidth
|
||||
</li>
|
||||
<li>
|
||||
<span class="text"> Limited to 1 organization member </span>
|
||||
Limited to {plan.storage}GB storage
|
||||
</li>
|
||||
<li>
|
||||
<span class="text">
|
||||
{plan.bandwidth}GB bandwidth
|
||||
</span>
|
||||
</li>
|
||||
<li>
|
||||
<span class="text">
|
||||
{plan.storage}GB storage
|
||||
</span>
|
||||
</li>
|
||||
<li>
|
||||
<span class="text">
|
||||
{formatNum(plan.executions)} executions
|
||||
</span>
|
||||
Limited to {formatNum(plan.executions)} executions
|
||||
</li>
|
||||
</ul>
|
||||
{/if}
|
||||
{:else if selectedTab === BillingPlan.PRO}
|
||||
<h3 class="u-bold body-text-1">{plan.name} plan</h3>
|
||||
<p class="u-margin-block-start-8">Everything in the Free plan, plus:</p>
|
||||
<ul class="un-order-list u-margin-inline-start-4">
|
||||
<Typography.Text>Everything in the Free plan, plus:</Typography.Text>
|
||||
<ul class="un-order-list">
|
||||
<li>Unlimited databases, buckets, functions</li>
|
||||
<li>{plan.bandwidth}GB bandwidth</li>
|
||||
<li>{plan.storage}GB storage</li>
|
||||
@@ -105,9 +96,8 @@
|
||||
<li>Email support</li>
|
||||
</ul>
|
||||
{:else if selectedTab === BillingPlan.SCALE}
|
||||
<h3 class="u-bold body-text-1">{plan.name} plan</h3>
|
||||
<p class="u-margin-block-start-8">Everything in the Pro plan, plus:</p>
|
||||
<ul class="un-order-list u-margin-inline-start-4">
|
||||
<Typography.Text>Everything in the Pro plan, plus:</Typography.Text>
|
||||
<ul class="un-order-list">
|
||||
<li>Unlimited seats</li>
|
||||
<li>Organization roles</li>
|
||||
<li>SOC-2, HIPAA compliance</li>
|
||||
@@ -115,29 +105,5 @@
|
||||
<li>Priority support</li>
|
||||
</ul>
|
||||
{/if}
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<style lang="scss">
|
||||
.comparison-box {
|
||||
border-radius: var(--border-radius-small);
|
||||
background: hsl(var(--color-neutral-5));
|
||||
}
|
||||
:global(.theme-dark) .comparison-box {
|
||||
background: hsl(var(--color-neutral-85));
|
||||
}
|
||||
|
||||
.comparison-box :global(.secondary-tabs-button:where(:disabled)) {
|
||||
background: hsl(var(--color-neutral-0));
|
||||
border: 1px solid hsl(var(--color-neutral-10));
|
||||
}
|
||||
:global(.theme-dark) .comparison-box :global(.secondary-tabs-button:where(:disabled)) {
|
||||
background: hsl(var(--color-neutral-80));
|
||||
border: 1px solid hsl(var(--color-neutral-85));
|
||||
}
|
||||
|
||||
.inline-tag {
|
||||
line-height: 140%;
|
||||
font-weight: 500;
|
||||
}
|
||||
</style>
|
||||
</Layout.Stack>
|
||||
</Card.Base>
|
||||
|
||||
@@ -1,14 +1,4 @@
|
||||
<script lang="ts">
|
||||
import {
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableCellHead,
|
||||
TableCellText,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
TableScroll
|
||||
} from '$lib/elements/table';
|
||||
import { Alert } from '$lib/components';
|
||||
import { calculateExcess, plansInfo, tierToPlan, type Tier } from '$lib/stores/billing';
|
||||
import { organization } from '$lib/stores/organization';
|
||||
import { toLocaleDate } from '$lib/helpers/date';
|
||||
@@ -19,7 +9,8 @@
|
||||
import type { Aggregation } from '$lib/sdk/billing';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { BillingPlan } from '$lib/constants';
|
||||
import { tooltip } from '$lib/actions/tooltip';
|
||||
import { Alert, Icon, Table, Tooltip } from '@appwrite.io/pink-svelte';
|
||||
import { IconInfo } from '@appwrite.io/pink-icons-svelte';
|
||||
|
||||
export let tier: Tier;
|
||||
|
||||
@@ -44,97 +35,91 @@
|
||||
});
|
||||
</script>
|
||||
|
||||
<Alert type="warning" {...$$restProps}>
|
||||
<svelte:fragment slot="title">
|
||||
Your organization will switch to {tierToPlan(BillingPlan.FREE).name} plan on {toLocaleDate(
|
||||
{#if showExcess}
|
||||
<Alert.Inline
|
||||
status="error"
|
||||
title={` Your organization will switch to ${tierToPlan(BillingPlan.FREE).name} plan on ${toLocaleDate(
|
||||
$organization.billingNextInvoiceDate
|
||||
)}.
|
||||
</svelte:fragment>
|
||||
{#if !showExcess}
|
||||
You will retain access to your {tierToPlan($organization.billingPlan).name} plan features until
|
||||
your billing period ends. After that, your organization will be limited to Free plan resources,
|
||||
and service disruptions may occur if usage exceeds plan limits.
|
||||
{:else}
|
||||
)}`}>
|
||||
You will retain access to {tierToPlan($organization.billingPlan).name} plan features until your
|
||||
billing period ends. After that,
|
||||
{#if excess?.members > 0}<span class="u-bold">
|
||||
all team members except the owner will be removed,</span>
|
||||
{:else}
|
||||
<span class="u-bold">your organization will be limited to Free plan resources,</span>
|
||||
{/if} and service disruptions may occur if usage exceeds Free plan limits.
|
||||
{/if}
|
||||
</Alert>
|
||||
{#if showExcess}
|
||||
<TableScroll dense class="u-margin-block-start-16">
|
||||
<TableHeader>
|
||||
<TableCellHead>Resource</TableCellHead>
|
||||
<TableCellHead>Free limit</TableCellHead>
|
||||
<TableCellHead>
|
||||
Excess usage <span
|
||||
use:tooltip={{ content: 'Metrics are estimates updated every 24 hours' }}
|
||||
class="icon-info"></span>
|
||||
</TableCellHead>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{#if excess?.members}
|
||||
<TableRow>
|
||||
<TableCellText title="members">Organization members</TableCellText>
|
||||
<TableCellText title="limit"
|
||||
>{plan.addons.seats.limit || 0} members</TableCellText>
|
||||
<TableCell title="excess">
|
||||
<p class="u-color-text-danger u-flex u-cross-center u-gap-4">
|
||||
<span class="icon-arrow-up" />
|
||||
{excess?.members} members
|
||||
</p>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
{/if}
|
||||
{#if excess?.storage}
|
||||
<TableRow>
|
||||
<TableCellText title="storage">Storage</TableCellText>
|
||||
<TableCellText title="limit">{plan.storage} GB</TableCellText>
|
||||
<TableCell title="excess">
|
||||
<p class="u-color-text-danger">
|
||||
<span class="icon-arrow-up" />
|
||||
{humanFileSize(excess?.storage).value}
|
||||
{humanFileSize(excess?.storage).unit}
|
||||
</p>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
{/if}
|
||||
{#if excess?.executions}
|
||||
<TableRow>
|
||||
<TableCellText title="executions">Function executions</TableCellText>
|
||||
<TableCellText title="limit">
|
||||
{abbreviateNumber(plan.executions)} executions
|
||||
</TableCellText>
|
||||
<TableCell title="excess">
|
||||
<p class="u-color-text-danger">
|
||||
<span class="icon-arrow-up" />
|
||||
<span
|
||||
title={excess?.executions
|
||||
? excess.executions.toString()
|
||||
: 'executions'}>
|
||||
{formatNum(excess?.executions)} executions
|
||||
</span>
|
||||
</p>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
{/if}
|
||||
{#if excess?.users}
|
||||
<TableRow>
|
||||
<TableCellText title="users">Users</TableCellText>
|
||||
<TableCellText title="limit">
|
||||
{abbreviateNumber(plan.users)} users
|
||||
</TableCellText>
|
||||
<TableCell title="excess">
|
||||
<p class="u-color-text-danger">
|
||||
<span class="icon-arrow-up" />
|
||||
<span title={excess?.users ? excess.users.toString() : 'users'}>
|
||||
{formatNum(excess?.users)} users
|
||||
</span>
|
||||
</p>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
{/if}
|
||||
</TableBody>
|
||||
</TableScroll>
|
||||
</Alert.Inline>
|
||||
|
||||
<Table.Root columns={3} let:root>
|
||||
<svelte:fragment slot="header" let:root>
|
||||
<Table.Header.Cell {root}>Resource</Table.Header.Cell>
|
||||
<Table.Header.Cell {root}>Free limit</Table.Header.Cell>
|
||||
<Table.Header.Cell {root}>
|
||||
Excess usage <Tooltip maxWidth="fit-content"
|
||||
><Icon icon={IconInfo} />
|
||||
<span slot="tooltip">Metrics are estimates updated every 24 hours</span>
|
||||
</Tooltip>
|
||||
</Table.Header.Cell>
|
||||
</svelte:fragment>
|
||||
{#if excess?.members}
|
||||
<Table.Row.Base {root}>
|
||||
<Table.Cell {root}>Organization members</Table.Cell>
|
||||
<Table.Cell {root}>{plan.addons.seats.limit} members</Table.Cell>
|
||||
<Table.Cell {root}>
|
||||
<p class="u-color-text-danger u-flex u-cross-center u-gap-4">
|
||||
<span class="icon-arrow-up"></span>
|
||||
{excess?.members} members
|
||||
</p>
|
||||
</Table.Cell>
|
||||
</Table.Row.Base>
|
||||
{/if}
|
||||
{#if excess?.storage}
|
||||
<Table.Row.Base {root}>
|
||||
<Table.Cell {root}>Storage</Table.Cell>
|
||||
<Table.Cell {root}>{plan.storage} GB</Table.Cell>
|
||||
<Table.Cell {root}>
|
||||
<p class="u-color-text-danger">
|
||||
<span class="icon-arrow-up"></span>
|
||||
{humanFileSize(excess?.storage).value}
|
||||
{humanFileSize(excess?.storage).unit}
|
||||
</p>
|
||||
</Table.Cell>
|
||||
</Table.Row.Base>
|
||||
{/if}
|
||||
{#if excess?.executions}
|
||||
<Table.Row.Base {root}>
|
||||
<Table.Cell {root}>Function executions</Table.Cell>
|
||||
<Table.Cell {root}>
|
||||
{abbreviateNumber(plan.executions)} executions
|
||||
</Table.Cell>
|
||||
<Table.Cell {root}>
|
||||
<p class="u-color-text-danger">
|
||||
<span class="icon-arrow-up"></span>
|
||||
<span
|
||||
title={excess?.executions
|
||||
? excess.executions.toString()
|
||||
: 'executions'}>
|
||||
{formatNum(excess?.executions)} executions
|
||||
</span>
|
||||
</p>
|
||||
</Table.Cell>
|
||||
</Table.Row.Base>
|
||||
{/if}
|
||||
{#if excess?.users}
|
||||
<Table.Row.Base {root}>
|
||||
<Table.Cell {root}>Users</Table.Cell>
|
||||
<Table.Cell {root}>
|
||||
{abbreviateNumber(plan.users)} users
|
||||
</Table.Cell>
|
||||
<Table.Cell column="usage" {root}>
|
||||
<p class="u-color-text-danger">
|
||||
<span class="icon-arrow-up"></span>
|
||||
<span title={excess?.users ? excess.users.toString() : 'users'}>
|
||||
{formatNum(excess?.users)} users
|
||||
</span>
|
||||
</p>
|
||||
</Table.Cell>
|
||||
</Table.Row.Base>
|
||||
{/if}
|
||||
</Table.Root>
|
||||
{/if}
|
||||
|
||||
@@ -1,107 +1,75 @@
|
||||
<script lang="ts">
|
||||
import { BillingPlan } from '$lib/constants';
|
||||
import { formatCurrency } from '$lib/helpers/numbers';
|
||||
import { plansInfo, tierFree, tierPro, tierScale, type Tier } from '$lib/stores/billing';
|
||||
import { plansInfo, type Tier, tierFree, tierPro, tierScale } from '$lib/stores/billing';
|
||||
import { organization } from '$lib/stores/organization';
|
||||
import { Badge, Layout, Typography } from '@appwrite.io/pink-svelte';
|
||||
import { LabelCard } from '..';
|
||||
|
||||
export let billingPlan: Tier;
|
||||
export let anyOrgFree = false;
|
||||
export let isNewOrg = false;
|
||||
export let selfService = true;
|
||||
let classes: string = '';
|
||||
export { classes as class };
|
||||
|
||||
$: freePlan = $plansInfo.get(BillingPlan.FREE);
|
||||
$: proPlan = $plansInfo.get(BillingPlan.PRO);
|
||||
$: scalePlan = $plansInfo.get(BillingPlan.SCALE);
|
||||
</script>
|
||||
|
||||
{#if billingPlan}
|
||||
<ul class="u-flex u-flex-vertical u-gap-16 u-margin-block-start-8 {classes}">
|
||||
<li>
|
||||
<LabelCard
|
||||
name="plan"
|
||||
bind:group={billingPlan}
|
||||
disabled={anyOrgFree || !selfService}
|
||||
value={BillingPlan.FREE}
|
||||
tooltipShow={anyOrgFree}
|
||||
tooltipText="You are limited to 1 Free organization per account."
|
||||
padding={1.5}>
|
||||
<svelte:fragment slot="custom" let:disabled>
|
||||
<div
|
||||
class="u-flex u-flex-vertical u-gap-4 u-width-full-line"
|
||||
class:u-opacity-50={disabled}>
|
||||
<h4 class="body-text-2 u-bold">
|
||||
{tierFree.name}
|
||||
{#if $organization?.billingPlan === BillingPlan.FREE && !isNewOrg}
|
||||
<span class="inline-tag">Current plan</span>
|
||||
{/if}
|
||||
</h4>
|
||||
<p class="u-color-text-offline u-small">
|
||||
{tierFree.description}
|
||||
</p>
|
||||
<p>
|
||||
{formatCurrency(freePlan?.price ?? 0)}
|
||||
</p>
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
</LabelCard>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<LabelCard
|
||||
name="plan"
|
||||
disabled={!selfService}
|
||||
bind:group={billingPlan}
|
||||
value={BillingPlan.PRO}
|
||||
padding={1.5}>
|
||||
<svelte:fragment slot="custom" let:disabled>
|
||||
<div
|
||||
class="u-flex u-flex-vertical u-gap-4 u-width-full-line"
|
||||
class:u-opacity-50={disabled}>
|
||||
<h4 class="body-text-2 u-bold">
|
||||
{tierPro.name}
|
||||
{#if $organization?.billingPlan === BillingPlan.PRO && !isNewOrg}
|
||||
<span class="inline-tag">Current plan</span>
|
||||
{/if}
|
||||
</h4>
|
||||
<p class="u-color-text-offline u-small">
|
||||
{tierPro.description}
|
||||
</p>
|
||||
<p>
|
||||
{formatCurrency(proPlan?.price ?? 0)} per member/month + usage
|
||||
</p>
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
</LabelCard>
|
||||
</li>
|
||||
<li>
|
||||
<LabelCard
|
||||
name="plan"
|
||||
bind:group={billingPlan}
|
||||
value={BillingPlan.SCALE}
|
||||
padding={1.5}
|
||||
disabled={!selfService}>
|
||||
<svelte:fragment slot="custom" let:disabled>
|
||||
<div
|
||||
class="u-flex u-flex-vertical u-gap-4 u-width-full-line"
|
||||
class:u-opacity-50={disabled}>
|
||||
<h4 class="body-text-2 u-bold">
|
||||
{tierScale.name}
|
||||
{#if $organization?.billingPlan === BillingPlan.SCALE && !isNewOrg}
|
||||
<span class="inline-tag">Current plan</span>
|
||||
{/if}
|
||||
</h4>
|
||||
<p class="u-color-text-offline u-small">
|
||||
{tierScale.description}
|
||||
</p>
|
||||
<p>
|
||||
{formatCurrency(scalePlan?.price ?? 0)} per month + usage
|
||||
</p>
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
</LabelCard>
|
||||
</li>
|
||||
</ul>
|
||||
{/if}
|
||||
<Layout.Stack>
|
||||
<LabelCard
|
||||
name="plan"
|
||||
bind:group={billingPlan}
|
||||
disabled={anyOrgFree || !selfService}
|
||||
value={BillingPlan.FREE}
|
||||
tooltipShow={anyOrgFree}
|
||||
title={tierFree.name}
|
||||
tooltipText="You are limited to 1 Free organization per account.">
|
||||
<svelte:fragment slot="action">
|
||||
{#if $organization?.billingPlan === BillingPlan.FREE && !isNewOrg}
|
||||
<Badge variant="secondary" size="xs" content="Current plan" />
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
<Typography.Caption variant="400">
|
||||
{tierFree.description}
|
||||
</Typography.Caption>
|
||||
<Typography.Text>
|
||||
{formatCurrency(freePlan?.price ?? 0)}
|
||||
</Typography.Text>
|
||||
</LabelCard>
|
||||
<LabelCard
|
||||
name="plan"
|
||||
disabled={!selfService}
|
||||
bind:group={billingPlan}
|
||||
value={BillingPlan.PRO}
|
||||
title={tierPro.name}>
|
||||
<svelte:fragment slot="action">
|
||||
{#if $organization?.billingPlan === BillingPlan.PRO && !isNewOrg}
|
||||
<Badge variant="secondary" size="xs" content="Current plan" />
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
<Typography.Caption variant="400">
|
||||
{tierPro.description}
|
||||
</Typography.Caption>
|
||||
<Typography.Text>
|
||||
{formatCurrency(proPlan?.price ?? 0)} per month + usage
|
||||
</Typography.Text>
|
||||
</LabelCard>
|
||||
<LabelCard
|
||||
name="plan"
|
||||
bind:group={billingPlan}
|
||||
value={BillingPlan.SCALE}
|
||||
title={tierScale.name}>
|
||||
<svelte:fragment slot="action">
|
||||
{#if $organization?.billingPlan === BillingPlan.SCALE && !isNewOrg}
|
||||
<Badge variant="secondary" size="xs" content="Current plan" />
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
<Typography.Caption variant="400">
|
||||
{tierScale.description}
|
||||
</Typography.Caption>
|
||||
<Typography.Text>
|
||||
{formatCurrency(scalePlan?.price ?? 0)} per month + usage
|
||||
</Typography.Text>
|
||||
</LabelCard>
|
||||
</Layout.Stack>
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
<script lang="ts">
|
||||
import { Button, Helper, InputChoice, InputSelectSearch, InputText } from '$lib/elements/forms';
|
||||
import { Button, InputText } from '$lib/elements/forms';
|
||||
import type { PaymentList, PaymentMethodData } from '$lib/sdk/billing';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { hasStripePublicKey, isCloud } from '$lib/system';
|
||||
import { onMount } from 'svelte';
|
||||
import { Alert, Card, CreditCardBrandImage } from '..';
|
||||
import PaymentModal from './paymentModal.svelte';
|
||||
import { capitalize } from '$lib/helpers/string';
|
||||
import { Alert, Icon, Layout, Selector, Card, Typography } from '@appwrite.io/pink-svelte';
|
||||
import { IconExclamationCircle, IconPlus } from '@appwrite.io/pink-icons-svelte';
|
||||
import InputSelect from '$lib/elements/forms/inputSelect.svelte';
|
||||
import { invalidate } from '$app/navigation';
|
||||
import { Dependencies } from '$lib/constants';
|
||||
|
||||
export let methods: PaymentList;
|
||||
export let value: string;
|
||||
@@ -14,22 +17,10 @@
|
||||
|
||||
let showTaxId = false;
|
||||
let showPaymentModal = false;
|
||||
let input: HTMLInputElement;
|
||||
let error: string;
|
||||
|
||||
function handleInvalid(event: Event) {
|
||||
event.preventDefault();
|
||||
|
||||
if (input.validity.valueMissing) {
|
||||
error = 'This field is required';
|
||||
return;
|
||||
}
|
||||
error = input.validationMessage;
|
||||
}
|
||||
|
||||
async function cardSaved(event: CustomEvent<PaymentMethodData>) {
|
||||
value = event.detail.$id;
|
||||
methods = await sdk.forConsole.billing.listPaymentMethods();
|
||||
invalidate(Dependencies.UPGRADE_PLAN);
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
@@ -39,113 +30,76 @@
|
||||
});
|
||||
|
||||
$: filteredMethods = methods?.paymentMethods?.filter((method) => !!method?.last4);
|
||||
|
||||
$: selectedPaymentMethod = methods?.paymentMethods?.find((method) => method.$id === value);
|
||||
</script>
|
||||
|
||||
{#if filteredMethods?.length}
|
||||
{#if selectedPaymentMethod?.country?.toLowerCase() === 'in'}
|
||||
<Alert type="warning">
|
||||
<svelte:fragment slot="title">Indian credit or debit card-holders</svelte:fragment>
|
||||
To comply with RBI regulations in India, Appwrite will ask for verification to charge up
|
||||
to $150 USD on your payment method. We will never charge more than the cost of your plan
|
||||
and the resources you use, or your budget cap limit. For higher usage limits, please contact
|
||||
us.
|
||||
</Alert>
|
||||
{/if}
|
||||
<InputSelectSearch
|
||||
id="method"
|
||||
required
|
||||
label="Payment method"
|
||||
placeholder="Select payment method"
|
||||
bind:value
|
||||
options={filteredMethods.map((method) => {
|
||||
return {
|
||||
value: method.$id,
|
||||
label: `${capitalize(method.brand)} ending in ${method.last4}`,
|
||||
data: [method.brand]
|
||||
};
|
||||
})}
|
||||
interactiveOutput
|
||||
let:option={o}>
|
||||
<svelte:fragment slot="output" let:option={o}>
|
||||
<output class="input-text u-cursor-pointer">
|
||||
<span class="u-flex u-gap-16 u-flex-vertical">
|
||||
<span class="u-flex u-gap-16">
|
||||
<span class="u-flex u-cross-center u-gap-8" style="padding-inline:0.25rem">
|
||||
<span>{o.label}</span>
|
||||
<CreditCardBrandImage brand={o.data?.toString()} />
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
</output>
|
||||
</svelte:fragment>
|
||||
|
||||
<span class="u-flex u-gap-16 u-flex-vertical">
|
||||
<span class="u-flex u-gap-16">
|
||||
<span class="u-flex u-cross-center u-gap-8" style="padding-inline:0.25rem">
|
||||
<span>{o.label}</span>
|
||||
<CreditCardBrandImage brand={o.data?.toString()} />
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
<svelte:fragment slot="listEnd">
|
||||
<Button text on:click={() => (showPaymentModal = true)}>
|
||||
<span class="icon-plus"></span>
|
||||
<span class="text">Add new payment method</span>
|
||||
</Button>
|
||||
</svelte:fragment>
|
||||
</InputSelectSearch>
|
||||
{:else}
|
||||
<div>
|
||||
<input
|
||||
bind:this={input}
|
||||
on:invalid={handleInvalid}
|
||||
required
|
||||
class="u-hide"
|
||||
type="text"
|
||||
name="method"
|
||||
id="method" />
|
||||
<Card
|
||||
isDashed
|
||||
style="--p-card-padding:0.75rem; --p-card-bg-color: transparent; --p-card-border-radius: 0.5rem"
|
||||
isTile>
|
||||
<div class="u-flex u-main-space-between u-cross-center">
|
||||
<p>
|
||||
<span class="icon-exclamation-circle"></span>
|
||||
<span class="text">No saved payment methods</span>
|
||||
</p>
|
||||
<Button secondary on:click={() => (showPaymentModal = true)}>
|
||||
<span class="icon-plus"></span> <span class="text">Add</span>
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
{#if error}
|
||||
<Helper class="u-position-relative" type="warning">{error}</Helper>
|
||||
<Layout.Stack gap="s">
|
||||
{#if filteredMethods?.length}
|
||||
{#if selectedPaymentMethod?.country?.toLowerCase() === 'in'}
|
||||
<Alert.Inline status="warning">
|
||||
<svelte:fragment slot="title">Indian credit or debit card-holders</svelte:fragment>
|
||||
To comply with RBI regulations in India, Appwrite will ask for verification to charge
|
||||
up to $150 USD on your payment method. We will never charge more than the cost of your
|
||||
plan and the resources you use, or your budget cap limit. For higher usage limits, please
|
||||
contact us.
|
||||
</Alert.Inline>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
<InputSelect
|
||||
id="method"
|
||||
required
|
||||
label="Payment method"
|
||||
placeholder="Select payment method"
|
||||
bind:value
|
||||
options={filteredMethods.map((method) => {
|
||||
return {
|
||||
value: method.$id,
|
||||
label: `${capitalize(method.brand)} ending in ${method.last4}`,
|
||||
data: [method.brand]
|
||||
};
|
||||
})} />
|
||||
<Layout.Stack direction="row" alignItems="center">
|
||||
<Button on:click={() => (showPaymentModal = true)} compact size="s">
|
||||
<Icon icon={IconPlus} slot="start" size="s" />
|
||||
Add payment method
|
||||
</Button>
|
||||
<slot name="actions" />
|
||||
</Layout.Stack>
|
||||
{:else}
|
||||
<Card.Base variant="secondary" radius="s" padding="xs">
|
||||
<Layout.Stack direction="row">
|
||||
<Layout.Stack direction="row" gap="xxs" alignItems="center">
|
||||
<Icon
|
||||
icon={IconExclamationCircle}
|
||||
size="m"
|
||||
color="--fgcolor-neutral-tertiary" />
|
||||
<Typography.Text variant="m-400" color="--fgcolor-neutral-tertiary"
|
||||
>No saved payment methods</Typography.Text>
|
||||
</Layout.Stack>
|
||||
<Button secondary on:click={() => (showPaymentModal = true)}>
|
||||
<Icon icon={IconPlus} slot="start" size="s" />
|
||||
Add
|
||||
</Button>
|
||||
</Layout.Stack>
|
||||
<slot name="actions" />
|
||||
</Card.Base>
|
||||
{/if}
|
||||
</Layout.Stack>
|
||||
|
||||
{#if showPaymentModal && isCloud && hasStripePublicKey}
|
||||
<PaymentModal bind:show={showPaymentModal} on:submit={cardSaved}>
|
||||
<svelte:fragment slot="end">
|
||||
<InputChoice
|
||||
type="checkbox"
|
||||
<Selector.Checkbox
|
||||
id="taxIdCheck"
|
||||
label="I'm purchasing as a business"
|
||||
fullWidth
|
||||
bind:value={showTaxId}>
|
||||
{#if showTaxId}
|
||||
<div class="u-margin-block-start-8">
|
||||
<InputText
|
||||
id="taxId"
|
||||
label="Tax ID"
|
||||
autofocus
|
||||
placeholder="Tax ID"
|
||||
bind:value={taxId} />
|
||||
</div>
|
||||
{/if}
|
||||
</InputChoice>
|
||||
bind:checked={showTaxId} />
|
||||
{#if showTaxId}
|
||||
<InputText
|
||||
id="taxId"
|
||||
label="Tax ID"
|
||||
autofocus
|
||||
placeholder="Tax ID"
|
||||
bind:value={taxId} />
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
</PaymentModal>
|
||||
{/if}
|
||||
|
||||
@@ -24,8 +24,7 @@
|
||||
tooltipShow={plan.$id === BillingPlan.FREE && anyOrgFree}
|
||||
tooltipText={plan.$id === BillingPlan.FREE
|
||||
? 'You are limited to 1 Free organization per account.'
|
||||
: ''}
|
||||
padding={1.5}>
|
||||
: ''}>
|
||||
<svelte:fragment slot="custom" let:disabled>
|
||||
<div
|
||||
class="u-flex u-flex-vertical u-gap-4 u-width-full-line"
|
||||
|
||||
@@ -1,19 +1,12 @@
|
||||
<script lang="ts">
|
||||
import { Modal } from '$lib/components';
|
||||
import { Button } from '$lib/elements/forms';
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCellHead,
|
||||
TableCellText,
|
||||
TableHeader,
|
||||
TableRow
|
||||
} from '$lib/elements/table';
|
||||
import { toLocaleDate } from '$lib/helpers/date';
|
||||
import { organization, type Organization } from '$lib/stores/organization';
|
||||
import { type Organization } from '$lib/stores/organization';
|
||||
import { plansInfo } from '$lib/stores/billing';
|
||||
import { abbreviateNumber, formatCurrency } from '$lib/helpers/numbers';
|
||||
import { BillingPlan } from '$lib/constants';
|
||||
import { Table, Typography } from '@appwrite.io/pink-svelte';
|
||||
|
||||
export let show = false;
|
||||
export let org: Organization;
|
||||
@@ -22,7 +15,7 @@
|
||||
|
||||
$: nextDate = org?.name
|
||||
? new Date(new Date().getFullYear(), new Date().getMonth() + 1, 1).toString()
|
||||
: $organization?.billingNextInvoiceDate;
|
||||
: org?.billingNextInvoiceDate;
|
||||
|
||||
const planData = [
|
||||
{
|
||||
@@ -52,71 +45,67 @@
|
||||
$: isFree = org.billingPlan === BillingPlan.FREE;
|
||||
|
||||
// equal or above means unlimited!
|
||||
$: getCorrectSeatsCountValue = (count: number): string | number => {
|
||||
const getCorrectSeatsCountValue = (count: number): string | number => {
|
||||
// php int max is always larger than js
|
||||
const exceedsSafeLimit = count >= Number.MAX_SAFE_INTEGER;
|
||||
return exceedsSafeLimit ? 'Unlimited' : count || 0;
|
||||
};
|
||||
</script>
|
||||
|
||||
<Modal bind:show size="big" headerDivider={false} title="Usage rates">
|
||||
<Modal bind:show title="Usage rates">
|
||||
{#if isFree}
|
||||
Usage on the {$plansInfo?.get(BillingPlan.FREE).name} plan is limited for the following resources.
|
||||
Next billing period: {toLocaleDate(nextDate)}.
|
||||
<Typography.Text>
|
||||
Usage on the {$plansInfo?.get(BillingPlan.FREE).name} plan is limited for the following resources.
|
||||
Next billing period: {toLocaleDate(nextDate)}.
|
||||
</Typography.Text>
|
||||
{:else if org.billingPlan === BillingPlan.PRO}
|
||||
<p>
|
||||
<Typography.Text>
|
||||
Usage on the Pro plan will be charged at the end of each billing period at the following
|
||||
rates. Next billing period: {toLocaleDate(nextDate)}.
|
||||
</p>
|
||||
</Typography.Text>
|
||||
{:else if org.billingPlan === BillingPlan.SCALE}
|
||||
<p>
|
||||
<Typography.Text>
|
||||
Usage on the Scale plan will be charged at the end of each billing period at the
|
||||
following rates. Next billing period: {toLocaleDate(nextDate)}.
|
||||
</p>
|
||||
</Typography.Text>
|
||||
{/if}
|
||||
<Table noStyles noMargin>
|
||||
<TableHeader>
|
||||
<TableCellHead>Resource</TableCellHead>
|
||||
<TableCellHead>Limit</TableCellHead>
|
||||
{#if !isFree}
|
||||
<TableCellHead>Rate</TableCellHead>
|
||||
<Table.Root
|
||||
columns={[{ id: 'resource' }, { id: 'limit' }, { id: 'rate', hide: isFree }]}
|
||||
let:root>
|
||||
<svelte:fragment slot="header" let:root>
|
||||
<Table.Header.Cell column="resource" {root}>Resource</Table.Header.Cell>
|
||||
<Table.Header.Cell column="limit" {root}>Limit</Table.Header.Cell>
|
||||
<Table.Header.Cell column="rate" {root}>Rate</Table.Header.Cell>
|
||||
</svelte:fragment>
|
||||
{#each planData as usage}
|
||||
{#if usage['id'] === 'members'}
|
||||
<Table.Row.Base {root}>
|
||||
<Table.Cell column="resource" {root}>{usage.resource}</Table.Cell>
|
||||
<Table.Cell column="limit" {root}>
|
||||
{getCorrectSeatsCountValue(plan.addons.seats.limit)}
|
||||
</Table.Cell>
|
||||
<Table.Cell column="rate" {root}>
|
||||
{formatCurrency(plan.addons?.seats?.price)}/{usage?.unit}
|
||||
</Table.Cell>
|
||||
</Table.Row.Base>
|
||||
{:else}
|
||||
{@const addon = plan.addons[usage.id]}
|
||||
<Table.Row.Base {root}>
|
||||
<Table.Cell column="resource" {root}>{usage.resource}</Table.Cell>
|
||||
<Table.Cell column="limit" {root}>
|
||||
{abbreviateNumber(plan[usage.id])}{usage?.unit}
|
||||
</Table.Cell>
|
||||
{#if !isFree}
|
||||
<Table.Cell column="rate" {root}>
|
||||
{formatCurrency(addon?.price)}/{['MB', 'GB', 'TB'].includes(addon?.unit)
|
||||
? addon?.value
|
||||
: abbreviateNumber(addon?.value, 0)}{usage?.unit}
|
||||
</Table.Cell>
|
||||
{/if}
|
||||
</Table.Row.Base>
|
||||
{/if}
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{#each planData as usage}
|
||||
{#if usage['id'] === 'members'}
|
||||
<TableRow>
|
||||
<TableCellText title="resource">{usage.resource}</TableCellText>
|
||||
<TableCellText title="limit">
|
||||
{getCorrectSeatsCountValue(plan.addons.seats.limit)}
|
||||
</TableCellText>
|
||||
{#if !isFree}
|
||||
<TableCellText title="rate">
|
||||
{formatCurrency(plan.addons.seats.price)}/{usage?.unit}
|
||||
</TableCellText>
|
||||
{/if}
|
||||
</TableRow>
|
||||
{:else}
|
||||
{@const addon = plan.usage[usage.id]}
|
||||
<TableRow>
|
||||
<TableCellText title="resource">{usage.resource}</TableCellText>
|
||||
<TableCellText title="limit">
|
||||
{abbreviateNumber(plan[usage.id])}{usage?.unit}
|
||||
</TableCellText>
|
||||
{#if !isFree}
|
||||
<TableCellText title="rate">
|
||||
{formatCurrency(addon?.price)}/{['MB', 'GB', 'TB'].includes(
|
||||
addon?.unit
|
||||
)
|
||||
? addon?.value
|
||||
: abbreviateNumber(addon?.value, 0)}{usage?.unit}
|
||||
</TableCellText>
|
||||
{/if}
|
||||
</TableRow>
|
||||
{/if}
|
||||
{/each}
|
||||
</TableBody>
|
||||
</Table>
|
||||
{/each}
|
||||
</Table.Root>
|
||||
<svelte:fragment slot="footer">
|
||||
<Button text on:click={() => (show = false)}>Close</Button>
|
||||
</svelte:fragment>
|
||||
|
||||
@@ -1,24 +1,43 @@
|
||||
<script lang="ts">
|
||||
import { Modal } from '$lib/components';
|
||||
import { Button, FormList, InputText } from '$lib/elements/forms';
|
||||
import { Button, InputText } from '$lib/elements/forms';
|
||||
import type { Coupon } from '$lib/sdk/billing';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
export let show = false;
|
||||
let error: string = null;
|
||||
let coupon: string = '';
|
||||
export let isNewOrg = false;
|
||||
export let couponData: Partial<Coupon> = {
|
||||
code: null,
|
||||
status: null,
|
||||
credits: null
|
||||
};
|
||||
let error: string = null;
|
||||
let coupon: string = '';
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
async function addCoupon() {
|
||||
try {
|
||||
const response = await sdk.forConsole.billing.getCouponAccount(coupon);
|
||||
// const response = await sdk.forConsole.billing.getCoupon(coupon);
|
||||
const response = await sdk.forConsole.billing.getCouponAccount(coupon); //TODO: double check that this is the correct method
|
||||
|
||||
if (response.onlyNewOrgs && !isNewOrg) {
|
||||
show = false;
|
||||
addNotification({
|
||||
type: 'error',
|
||||
message: 'Coupon only valid for new organizations'
|
||||
});
|
||||
} else {
|
||||
couponData = response;
|
||||
dispatch('validation', couponData);
|
||||
coupon = null;
|
||||
show = false;
|
||||
addNotification({
|
||||
type: 'success',
|
||||
message: 'Credits applied successfully'
|
||||
});
|
||||
}
|
||||
couponData = response;
|
||||
dispatch('validation', couponData);
|
||||
coupon = null;
|
||||
@@ -37,21 +56,20 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<Modal
|
||||
bind:show
|
||||
title="Add credits"
|
||||
headerDivider={false}
|
||||
onSubmit={addCoupon}
|
||||
size="big"
|
||||
bind:error>
|
||||
Credits will be applied automatically to your next invoice.
|
||||
<Modal bind:show title="Add credits" onSubmit={addCoupon} bind:error>
|
||||
<svelte:fragment slot="description">
|
||||
Credits will be applied automatically to your next invoice.
|
||||
</svelte:fragment>
|
||||
|
||||
<FormList>
|
||||
<InputText placeholder="Promo code" id="code" label="Add promo code" bind:value={coupon} />
|
||||
</FormList>
|
||||
<InputText
|
||||
required
|
||||
placeholder="Promo code"
|
||||
id="code"
|
||||
label="Add promo code"
|
||||
bind:value={coupon} />
|
||||
|
||||
<svelte:fragment slot="footer">
|
||||
<Button text on:click={() => (show = false)}>Cancel</Button>
|
||||
<Button submit>Add</Button>
|
||||
<Button submit disabled={coupon === ''}>Add</Button>
|
||||
</svelte:fragment>
|
||||
</Modal>
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
<script lang="ts">
|
||||
import type { SheetMenu, SubMenu } from '$lib/components/bottom-sheet/index.js';
|
||||
import { ActionMenu, Layout, Selector } from '@appwrite.io/pink-svelte';
|
||||
|
||||
export let menu: SubMenu;
|
||||
export let isOpen: boolean;
|
||||
export let navigateSubMenu: (menu: SheetMenu) => void;
|
||||
export let navigatePreviousMenu: () => void;
|
||||
</script>
|
||||
|
||||
{#if menu?.title}
|
||||
<span class="menu-title">{menu.title}</span>
|
||||
{/if}
|
||||
<ActionMenu.Root>
|
||||
{#each menu.items as menuItem}
|
||||
{#if menuItem.href}
|
||||
<ActionMenu.Item.Anchor
|
||||
size="l"
|
||||
leadingIcon={menuItem.leadingIcon}
|
||||
trailingIcon={menuItem.trailingIcon}
|
||||
href={menuItem.href}
|
||||
on:click={() => {
|
||||
isOpen = false;
|
||||
}}>
|
||||
<span class="anchor">{menuItem.name}</span>
|
||||
</ActionMenu.Item.Anchor>
|
||||
{:else}
|
||||
<ActionMenu.Item.Button
|
||||
size="l"
|
||||
leadingIcon={menuItem.leadingIcon}
|
||||
trailingIcon={menuItem.trailingIcon}
|
||||
on:click={() => {
|
||||
if (menuItem.subMenu) {
|
||||
navigateSubMenu(menuItem.subMenu);
|
||||
} else if (menuItem.navigatePrevious) {
|
||||
navigatePreviousMenu();
|
||||
} else if (menuItem.onClick !== undefined) {
|
||||
menuItem.onClick();
|
||||
if (menuItem.closeOnClick !== false) {
|
||||
isOpen = false;
|
||||
}
|
||||
}
|
||||
}}>
|
||||
{#if menuItem?.checked !== undefined}
|
||||
<Layout.Stack direction="row" gap="s">
|
||||
<Selector.Checkbox checked={menuItem.checked} size="s" />
|
||||
{menuItem.name}
|
||||
</Layout.Stack>
|
||||
{:else}
|
||||
{menuItem.name}
|
||||
{/if}
|
||||
</ActionMenu.Item.Button>
|
||||
{/if}
|
||||
{/each}
|
||||
</ActionMenu.Root>
|
||||
|
||||
<style lang="scss">
|
||||
.menu-title {
|
||||
padding: var(--space-3) var(--space-5);
|
||||
display: block;
|
||||
text-transform: uppercase;
|
||||
font-size: var(--font-size-xs, 12px);
|
||||
line-height: 130%; /* 15.6px */
|
||||
letter-spacing: 0.96px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,66 @@
|
||||
<script lang="ts">
|
||||
import SheetMenuBlock from './SheetMenuBlock.svelte';
|
||||
import { BottomSheet } from '@appwrite.io/pink-svelte';
|
||||
import type { $$Props, SheetMenu } from '$lib/components/bottom-sheet/index';
|
||||
|
||||
export let menu: $$Props['menu'];
|
||||
export let isOpen: $$Props['isOpen'] = false;
|
||||
let sheetContainerRef: $$Props['sheetContainerRef'];
|
||||
let activeMenu = menu;
|
||||
let showDivider = true;
|
||||
let previousMenu = activeMenu;
|
||||
|
||||
function navigateSubMenu(subMenu: SheetMenu) {
|
||||
previousMenu = activeMenu;
|
||||
if (sheetContainerRef) {
|
||||
const currentHeight = sheetContainerRef.offsetHeight;
|
||||
sheetContainerRef.style.overflowY = 'hidden';
|
||||
sheetContainerRef.style.maxHeight = `${currentHeight}px`;
|
||||
activeMenu = subMenu;
|
||||
requestAnimationFrame(() => {
|
||||
if (sheetContainerRef) {
|
||||
const newHeight = sheetContainerRef.scrollHeight;
|
||||
sheetContainerRef.style.maxHeight = `${newHeight + 5}px`;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
activeMenu = subMenu;
|
||||
}
|
||||
showDivider = activeMenu.bottom !== undefined;
|
||||
}
|
||||
|
||||
function navigatePreviousMenu() {
|
||||
activeMenu = previousMenu;
|
||||
showDivider = activeMenu.bottom !== undefined;
|
||||
}
|
||||
|
||||
function restoreMenu(isOpenState: boolean) {
|
||||
showDivider = activeMenu.bottom !== undefined;
|
||||
if (!isOpenState) {
|
||||
setTimeout(() => {
|
||||
activeMenu = menu;
|
||||
}, 400);
|
||||
}
|
||||
}
|
||||
|
||||
$: restoreMenu(isOpen);
|
||||
</script>
|
||||
|
||||
<BottomSheet.Default bind:isOpen useSlots={true} bind:sheetContainerRef bind:showDivider>
|
||||
<div slot="top">
|
||||
<SheetMenuBlock
|
||||
menu={activeMenu.top}
|
||||
{navigateSubMenu}
|
||||
{navigatePreviousMenu}
|
||||
bind:isOpen />
|
||||
</div>
|
||||
<div slot="bottom">
|
||||
{#if activeMenu.bottom}
|
||||
<SheetMenuBlock
|
||||
menu={activeMenu.bottom}
|
||||
{navigateSubMenu}
|
||||
{navigatePreviousMenu}
|
||||
bind:isOpen />
|
||||
{/if}
|
||||
</div>
|
||||
</BottomSheet.Default>
|
||||
@@ -0,0 +1,30 @@
|
||||
import Menu from './bottomSheetMenu.svelte';
|
||||
import type { ComponentType } from 'svelte';
|
||||
|
||||
export type $$Props = {
|
||||
isOpen: boolean;
|
||||
useSlots?: boolean;
|
||||
sheetContainerRef?: HTMLDialogElement;
|
||||
showDivider?: boolean;
|
||||
menu: SheetMenu;
|
||||
};
|
||||
export type SubMenu = {
|
||||
title?: string;
|
||||
items: MenuItem[];
|
||||
};
|
||||
|
||||
type MenuItem = {
|
||||
name: string;
|
||||
leadingIcon?: ComponentType;
|
||||
trailingIcon?: ComponentType;
|
||||
onClick?: () => void;
|
||||
href?: string;
|
||||
closeOnClick?: boolean;
|
||||
navigatePrevious?: boolean;
|
||||
checked?: boolean;
|
||||
subMenu?: { top: SubMenu; bottom: SubMenu };
|
||||
};
|
||||
|
||||
export type SheetMenu = { top: SubMenu; bottom: SubMenu };
|
||||
|
||||
export default { Menu };
|
||||
@@ -14,15 +14,15 @@
|
||||
import { upgradeURL } from '$lib/stores/billing';
|
||||
import { addBottomModalAlerts } from '$routes/(console)/bottomAlerts';
|
||||
import { project } from '$routes/(console)/project-[region]-[project]/store';
|
||||
import { page } from '$app/stores';
|
||||
import { trackEvent } from '$lib/actions/analytics';
|
||||
import { page } from '$app/state';
|
||||
import { Click, trackEvent } from '$lib/actions/analytics';
|
||||
import { goto } from '$app/navigation';
|
||||
|
||||
let currentIndex = 0;
|
||||
let openModalOnMobile = false;
|
||||
|
||||
function getPageScope(pathname: string) {
|
||||
const isProjectPage = pathname.includes('project-[project]');
|
||||
const isProjectPage = pathname.includes('project-[region]-[project]');
|
||||
const isOrganizationPage = pathname.includes('organization-[organization]');
|
||||
|
||||
return { isProjectPage, isOrganizationPage };
|
||||
@@ -49,7 +49,7 @@
|
||||
});
|
||||
}
|
||||
|
||||
$: filteredModalAlerts = filterModalAlerts($bottomModalAlertsConfig.alerts, $page.route.id);
|
||||
$: filteredModalAlerts = filterModalAlerts($bottomModalAlertsConfig.alerts, page.route.id);
|
||||
|
||||
$: currentModalAlert = filteredModalAlerts[currentIndex] as BottomModalAlertItem;
|
||||
|
||||
@@ -131,13 +131,16 @@
|
||||
});
|
||||
</script>
|
||||
|
||||
{#if filteredModalAlerts.length > 0 && currentModalAlert && !$page.url.pathname.includes('console/onboarding')}
|
||||
{#if filteredModalAlerts.length > 0 && currentModalAlert && !page.url.pathname.includes('console/onboarding')}
|
||||
{@const shouldShowUpgrade = showUpgrade()}
|
||||
<div class="main-alert-wrapper is-not-mobile">
|
||||
<div class="alert-container">
|
||||
<article class="card">
|
||||
{#key currentModalAlert.id}
|
||||
<button class="icon-inline-tag" on:click={() => handleClose()}>
|
||||
<button
|
||||
aria-label="Close modal"
|
||||
class="icon-inline-tag"
|
||||
on:click={() => handleClose()}>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="20"
|
||||
@@ -173,17 +176,19 @@
|
||||
|
||||
<div class="u-flex u-gap-10">
|
||||
<button
|
||||
aria-label="Previous"
|
||||
class="icon-cheveron-left"
|
||||
on:click={showPrevious}
|
||||
disabled={currentIndex === 0}
|
||||
class:active={currentIndex > 0} />
|
||||
class:active={currentIndex > 0}></button>
|
||||
|
||||
<button
|
||||
aria-label="Next"
|
||||
class="icon-cheveron-right"
|
||||
on:click={showNext}
|
||||
disabled={currentIndex === filteredModalAlerts.length - 1}
|
||||
class:active={currentIndex !==
|
||||
filteredModalAlerts.length - 1} />
|
||||
filteredModalAlerts.length - 1}></button>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
@@ -220,7 +225,7 @@
|
||||
handleClose();
|
||||
}
|
||||
|
||||
trackEvent('click_promo', {
|
||||
trackEvent(Click.PromoClick, {
|
||||
promo: currentModalAlert.id,
|
||||
type: shouldShowUpgrade ? 'upgrade' : 'try_now'
|
||||
});
|
||||
@@ -253,7 +258,10 @@
|
||||
<div class="alert-container">
|
||||
<article class="card">
|
||||
{#key currentModalAlert.id}
|
||||
<button class="icon-inline-tag" on:click={() => handleClose()}>
|
||||
<button
|
||||
aria-label="Close modal"
|
||||
class="icon-inline-tag"
|
||||
on:click={() => handleClose()}>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="20"
|
||||
@@ -289,18 +297,20 @@
|
||||
|
||||
<div class="u-flex u-gap-10">
|
||||
<button
|
||||
aria-label="Previous"
|
||||
class="icon-cheveron-left"
|
||||
on:click={showPrevious}
|
||||
disabled={currentIndex === 0}
|
||||
class:active={currentIndex > 0} />
|
||||
class:active={currentIndex > 0}></button>
|
||||
|
||||
<button
|
||||
aria-label="Next"
|
||||
class="icon-cheveron-right"
|
||||
on:click={showNext}
|
||||
disabled={currentIndex ===
|
||||
filteredModalAlerts.length - 1}
|
||||
class:active={currentIndex !==
|
||||
filteredModalAlerts.length - 1} />
|
||||
filteredModalAlerts.length - 1}></button>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
@@ -332,7 +342,7 @@
|
||||
fullWidthMobile
|
||||
on:click={() => {
|
||||
openModalOnMobile = false;
|
||||
trackEvent('click_promo', {
|
||||
trackEvent(Click.PromoClick, {
|
||||
promo: currentModalAlert.id,
|
||||
type: shouldShowUpgrade ? 'upgrade' : 'try_now'
|
||||
});
|
||||
@@ -363,7 +373,11 @@
|
||||
</div>
|
||||
{:else}
|
||||
{@const mobileConfig = getMobileWindowConfig()}
|
||||
<button
|
||||
<!-- we don't need keydown because we show this only on mobile -->
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<div
|
||||
tabindex="0"
|
||||
role="button"
|
||||
class:showing={!openModalOnMobile}
|
||||
class="card notification-card u-width-full-line"
|
||||
on:click={() => {
|
||||
@@ -377,8 +391,8 @@
|
||||
<div class="u-flex-vertical u-gap-4">
|
||||
<div class="u-flex u-cross-center u-main-space-between">
|
||||
<h3 class="body-text-2 u-bold">{mobileConfig.title}</h3>
|
||||
<button on:click={hideAllModalAlerts}>
|
||||
<span class="icon-x" />
|
||||
<button on:click={hideAllModalAlerts} aria-label="Close">
|
||||
<span class="icon-x"></span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -390,7 +404,7 @@
|
||||
{/if}
|
||||
</span>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -1,20 +1,11 @@
|
||||
<script lang="ts">
|
||||
export let radius: keyof typeof radiuses = 'small';
|
||||
export let padding = 24;
|
||||
let classes = '';
|
||||
export { classes as class };
|
||||
import { Card } from '@appwrite.io/pink-svelte';
|
||||
import type { BaseCardProps } from './card.svelte';
|
||||
|
||||
enum radiuses {
|
||||
xsmall = '--border-radius-xsmall',
|
||||
small = '--border-radius-small',
|
||||
medium = '--border-radius-medium',
|
||||
large = '--border-radius-large'
|
||||
}
|
||||
export let radius: BaseCardProps['radius'] = 'm';
|
||||
export let padding: BaseCardProps['padding'] = 's';
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="box {classes}"
|
||||
style:--box-border-radius={`var(${radiuses[radius]})`}
|
||||
style:--box-padding={`${padding / 16}rem`}>
|
||||
<Card.Base variant="secondary" {radius} {padding}>
|
||||
<slot />
|
||||
</div>
|
||||
</Card.Base>
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
<script lang="ts">
|
||||
import { Box } from '.';
|
||||
import { Layout } from '@appwrite.io/pink-svelte';
|
||||
</script>
|
||||
|
||||
<Box>
|
||||
<div class="u-flex u-gap-16">
|
||||
<Layout.Stack direction="row" justifyContent="flex-start" alignItems="center">
|
||||
<slot name="image" />
|
||||
<div class="u-cross-child-center u-line-height-1-5">
|
||||
<div>
|
||||
<slot name="title" />
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
</Layout.Stack>
|
||||
</Box>
|
||||
|
||||
@@ -0,0 +1,459 @@
|
||||
<script lang="ts">
|
||||
import { createMenubar, melt } from '@melt-ui/svelte';
|
||||
import { Badge, Icon, type SheetMenu, ActionMenu, Card } from '@appwrite.io/pink-svelte';
|
||||
import {
|
||||
IconChevronDown,
|
||||
IconChevronRight,
|
||||
IconPlus,
|
||||
IconPlusSm
|
||||
} from '@appwrite.io/pink-icons-svelte';
|
||||
import { BottomSheet } from '$lib/components';
|
||||
import { isSmallViewport } from '$lib/stores/viewport';
|
||||
import { isCloud } from '$lib/system';
|
||||
import { goto } from '$app/navigation';
|
||||
import { base } from '$app/paths';
|
||||
import { newOrgModal } from '$lib/stores/organization';
|
||||
import { Click, trackEvent } from '$lib/actions/analytics';
|
||||
|
||||
type Project = {
|
||||
name: string;
|
||||
$id: string;
|
||||
isSelected: boolean;
|
||||
region: string;
|
||||
};
|
||||
type Organization = {
|
||||
name: string;
|
||||
$id: string;
|
||||
tierName: string;
|
||||
isSelected: boolean;
|
||||
projects: Array<Project>;
|
||||
};
|
||||
|
||||
const {
|
||||
elements: { menubar },
|
||||
builders: { createMenu }
|
||||
} = createMenubar();
|
||||
|
||||
const {
|
||||
elements: {
|
||||
trigger: triggerOrganizations,
|
||||
menu: menuOrganizations,
|
||||
item: itemOrganizations,
|
||||
separator: separatorOrganizations
|
||||
},
|
||||
builders: { createSubmenu: createSubmenuOrganizations, createMenuRadioGroup }
|
||||
} = createMenu();
|
||||
|
||||
const {
|
||||
elements: { radioGroup: radioGroupOrganizations }
|
||||
} = createMenuRadioGroup({});
|
||||
|
||||
const {
|
||||
elements: { subMenu: subMenuOrganizations, subTrigger: subTriggerOrganizations }
|
||||
} = createSubmenuOrganizations();
|
||||
|
||||
const {
|
||||
elements: {
|
||||
trigger: triggerProjects,
|
||||
menu: menuProjects,
|
||||
item: itemProjects,
|
||||
separator: separatorProjects
|
||||
}
|
||||
} = createMenu();
|
||||
|
||||
export let organizations: Organization[] = [];
|
||||
|
||||
$: selectedOrg = organizations.find((organization) => organization.isSelected);
|
||||
$: selectedProject = selectedOrg?.projects.find((project) => project.isSelected);
|
||||
|
||||
let organisationBottomSheetOpen = false;
|
||||
let projectsBottomSheetOpen = false;
|
||||
|
||||
function createOrg() {
|
||||
trackEvent(Click.OrganizationClickCreate, { source: 'breadcrumbs' });
|
||||
if (isCloud) {
|
||||
goto(`${base}/create-organization`);
|
||||
} else newOrgModal.set(true);
|
||||
}
|
||||
|
||||
const switchOrganization = {
|
||||
top: {
|
||||
title: 'Switch Organization',
|
||||
items: organizations.map((organization) => ({
|
||||
name: organization.name,
|
||||
href: `/console/organization-${organization?.$id}`
|
||||
}))
|
||||
},
|
||||
bottom: {
|
||||
items: [
|
||||
{
|
||||
name: 'Create organization',
|
||||
leadingIcon: IconPlus,
|
||||
onClick: createOrg
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
$: organizationsBottomSheet = !selectedOrg
|
||||
? switchOrganization
|
||||
: {
|
||||
top: {
|
||||
items: [
|
||||
{
|
||||
name: 'Organization overview',
|
||||
href: `/console/organization-${selectedOrg?.$id}`
|
||||
}
|
||||
]
|
||||
},
|
||||
bottom:
|
||||
organizations.length > 1
|
||||
? {
|
||||
items: [
|
||||
{
|
||||
name: 'Switch organization',
|
||||
trailingIcon: IconChevronRight,
|
||||
subMenu: switchOrganization
|
||||
}
|
||||
]
|
||||
}
|
||||
: {
|
||||
items: [
|
||||
{
|
||||
name: 'Create organization',
|
||||
leadingIcon: IconPlus,
|
||||
onClick: createOrg
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
let projectsBottomSheet: SheetMenu;
|
||||
$: projectsBottomSheet = {
|
||||
top:
|
||||
selectedOrg?.projects.length > 1
|
||||
? {
|
||||
title: 'Switch project',
|
||||
items: !selectedOrg
|
||||
? []
|
||||
: selectedOrg?.projects
|
||||
.map((project, index) => {
|
||||
if (index < 4) {
|
||||
return {
|
||||
name: project.name,
|
||||
href: `/console/project-${project.region}-${project.$id}/overview`
|
||||
};
|
||||
} else if (index === 4) {
|
||||
return {
|
||||
name: 'All projects',
|
||||
href: `/console/organization-${selectedOrg?.$id}`
|
||||
};
|
||||
}
|
||||
return null;
|
||||
})
|
||||
.filter((project) => project !== null)
|
||||
}
|
||||
: {
|
||||
items: [
|
||||
{
|
||||
name: 'Create project',
|
||||
trailingIcon: IconPlus,
|
||||
href: `/console/organization-${selectedOrg?.$id}?create-project`
|
||||
}
|
||||
]
|
||||
},
|
||||
bottom:
|
||||
selectedOrg?.projects.length > 1
|
||||
? {
|
||||
items: [
|
||||
{
|
||||
name: 'Create project',
|
||||
trailingIcon: IconPlus,
|
||||
href: `/console/organization-${selectedOrg?.$id}?create-project`
|
||||
}
|
||||
]
|
||||
}
|
||||
: undefined
|
||||
};
|
||||
|
||||
function onResize() {
|
||||
if ((organisationBottomSheetOpen || projectsBottomSheetOpen) && !$isSmallViewport) {
|
||||
organisationBottomSheetOpen = false;
|
||||
projectsBottomSheetOpen = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:window on:resize={onResize} />
|
||||
<div use:melt={$menubar}>
|
||||
{#if !$isSmallViewport}
|
||||
<span class="breadcrumb-separator">/</span>
|
||||
<button
|
||||
type="button"
|
||||
class="trigger"
|
||||
use:melt={$triggerOrganizations}
|
||||
aria-label="Open organizations tab">
|
||||
<span class="orgName">{selectedOrg?.name ?? 'Organization'}</span>
|
||||
<span class="not-mobile"
|
||||
>{#if selectedOrg?.tierName}<Badge
|
||||
variant="secondary"
|
||||
content={selectedOrg?.tierName} />{/if}</span>
|
||||
<Icon icon={IconChevronDown} size="s" color="--fgcolor-neutral-secondary" />
|
||||
</button>
|
||||
{:else}
|
||||
<button
|
||||
type="button"
|
||||
class="trigger"
|
||||
on:click={() => {
|
||||
organisationBottomSheetOpen = true;
|
||||
}}
|
||||
aria-label="Open organizations tab">
|
||||
<span class="orgName" class:noProjects={!selectedProject}
|
||||
>{selectedOrg?.name ?? 'Organization'}</span>
|
||||
<span class="not-mobile"
|
||||
><Badge variant="secondary" content={selectedOrg?.tierName ?? ''} /></span>
|
||||
<Icon icon={IconChevronDown} size="s" color="--fgcolor-neutral-secondary" />
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
<div class="menu" use:melt={$menuOrganizations}>
|
||||
<Card.Base padding="xxxs" shadow={true}>
|
||||
{#if selectedOrg}
|
||||
<div use:melt={$itemOrganizations}>
|
||||
<ActionMenu.Root>
|
||||
<ActionMenu.Item.Anchor href={`/console/organization-${selectedOrg?.$id}`}
|
||||
>Organization overview</ActionMenu.Item.Anchor
|
||||
></ActionMenu.Root>
|
||||
</div>
|
||||
{#if organizations.length > 1}
|
||||
<div class="separator" use:melt={$separatorOrganizations}></div>
|
||||
|
||||
<div use:melt={$subTriggerOrganizations}>
|
||||
<ActionMenu.Root>
|
||||
<ActionMenu.Item.Button trailingIcon={IconChevronRight}
|
||||
>Switch organization</ActionMenu.Item.Button>
|
||||
</ActionMenu.Root>
|
||||
</div>
|
||||
<div class="menu subMenu" use:melt={$subMenuOrganizations}>
|
||||
<Card.Base padding="xxxs" shadow={true}>
|
||||
<div use:melt={$radioGroupOrganizations}>
|
||||
{#each organizations as organization}
|
||||
<div use:melt={$itemOrganizations}>
|
||||
<ActionMenu.Root>
|
||||
<ActionMenu.Item.Anchor
|
||||
href={`/console/organization-${organization?.$id}`}
|
||||
>{organization.name}</ActionMenu.Item.Anchor>
|
||||
</ActionMenu.Root>
|
||||
</div>
|
||||
{/each}
|
||||
<div class="separator" use:melt={$separatorOrganizations}></div>
|
||||
<div use:melt={$itemOrganizations}>
|
||||
<ActionMenu.Root>
|
||||
<ActionMenu.Item.Button
|
||||
leadingIcon={IconPlusSm}
|
||||
on:click={createOrg}
|
||||
>Create organization</ActionMenu.Item.Button
|
||||
></ActionMenu.Root>
|
||||
</div>
|
||||
</div>
|
||||
</Card.Base>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="separator" use:melt={$separatorOrganizations}></div>
|
||||
<div use:melt={$itemOrganizations}>
|
||||
<ActionMenu.Root>
|
||||
<ActionMenu.Item.Button leadingIcon={IconPlusSm} on:click={createOrg}
|
||||
>Create organization</ActionMenu.Item.Button
|
||||
></ActionMenu.Root>
|
||||
</div>
|
||||
{/if}
|
||||
{:else}
|
||||
{#each organizations as organization}
|
||||
<div use:melt={$itemOrganizations}>
|
||||
<ActionMenu.Root>
|
||||
<ActionMenu.Item.Anchor
|
||||
href={`/console/organization-${organization?.$id}`}
|
||||
>{organization.name}</ActionMenu.Item.Anchor
|
||||
></ActionMenu.Root>
|
||||
</div>
|
||||
{/each}
|
||||
<div class="separator" use:melt={$separatorOrganizations}></div>
|
||||
<div use:melt={$itemOrganizations}>
|
||||
<ActionMenu.Root>
|
||||
<ActionMenu.Item.Button leadingIcon={IconPlusSm} on:click={createOrg}
|
||||
>Create organization</ActionMenu.Item.Button
|
||||
></ActionMenu.Root>
|
||||
</div>
|
||||
{/if}
|
||||
</Card.Base>
|
||||
</div>
|
||||
|
||||
{#if selectedOrg && selectedProject}
|
||||
<span class="breadcrumb-separator">/</span>
|
||||
{#if !$isSmallViewport}
|
||||
<button
|
||||
type="button"
|
||||
class="trigger"
|
||||
use:melt={$triggerProjects}
|
||||
aria-label="Open projects tab">
|
||||
<span class="projectName">{selectedProject.name}</span>
|
||||
<Icon icon={IconChevronDown} size="s" />
|
||||
</button>
|
||||
{:else}
|
||||
<button
|
||||
type="button"
|
||||
class="trigger"
|
||||
on:click={() => (projectsBottomSheetOpen = true)}
|
||||
aria-label="Open projects tab">
|
||||
<span class="projectName">{selectedProject.name}</span>
|
||||
<Icon icon={IconChevronDown} size="s" />
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
<div class="menu" use:melt={$menuProjects}>
|
||||
<Card.Base padding="xxxs" shadow={true}>
|
||||
{#if selectedOrg.projects.length > 1}
|
||||
{#each selectedOrg.projects as project, index}
|
||||
{#if index < 4}
|
||||
<div use:melt={$itemProjects}>
|
||||
<ActionMenu.Root>
|
||||
<ActionMenu.Item.Anchor
|
||||
href={`/console/project-${project.region}-${project.$id}`}
|
||||
>{project.name}</ActionMenu.Item.Anchor
|
||||
></ActionMenu.Root>
|
||||
</div>
|
||||
{:else if index === 4}
|
||||
<div use:melt={$itemProjects}>
|
||||
<ActionMenu.Root>
|
||||
<ActionMenu.Item.Anchor
|
||||
href={`/console/organization-${selectedOrg.$id}`}
|
||||
>All projects</ActionMenu.Item.Anchor
|
||||
></ActionMenu.Root>
|
||||
</div>
|
||||
{/if}
|
||||
{/each}
|
||||
<div class="separator" use:melt={$separatorProjects}></div>
|
||||
{/if}
|
||||
<div use:melt={$itemProjects}>
|
||||
<ActionMenu.Root>
|
||||
<ActionMenu.Item.Anchor
|
||||
leadingIcon={IconPlusSm}
|
||||
href={`/console/organization-${selectedOrg?.$id}?create-project`}
|
||||
>Create project</ActionMenu.Item.Anchor
|
||||
></ActionMenu.Root>
|
||||
</div>
|
||||
</Card.Base>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<BottomSheet.Menu bind:isOpen={organisationBottomSheetOpen} menu={organizationsBottomSheet}
|
||||
></BottomSheet.Menu>
|
||||
<BottomSheet.Menu bind:isOpen={projectsBottomSheetOpen} menu={projectsBottomSheet}
|
||||
></BottomSheet.Menu>
|
||||
|
||||
<style lang="scss">
|
||||
.menu {
|
||||
min-width: 244px;
|
||||
z-index: 20;
|
||||
}
|
||||
|
||||
.not-mobile {
|
||||
display: none;
|
||||
|
||||
@media (min-width: 768px) {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.subMenu {
|
||||
min-width: 244px;
|
||||
margin-inline: -4px;
|
||||
margin-block: -4px;
|
||||
}
|
||||
|
||||
.orgName,
|
||||
.projectName {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 60px;
|
||||
color: var(--fgcolor-neutral-secondary);
|
||||
|
||||
@media (min-width: 390px) {
|
||||
max-width: 95px;
|
||||
}
|
||||
|
||||
@media (min-width: 400px) {
|
||||
max-width: 105px;
|
||||
}
|
||||
|
||||
@media (min-width: 800px) {
|
||||
max-width: 125px;
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
max-width: 150px;
|
||||
}
|
||||
}
|
||||
|
||||
.noProjects {
|
||||
max-width: 150px;
|
||||
}
|
||||
|
||||
:global(.item[data-highlighted]) {
|
||||
border-radius: var(--border-radius-S, 8px);
|
||||
background: var(--overlay-neutral-hover, rgba(25, 25, 28, 0.03));
|
||||
}
|
||||
.trigger {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: var(--space-1, 2px) var(--space-2, 4px) var(--space-1, 2px) var(--space-3, 6px);
|
||||
gap: var(--space-2, 4px);
|
||||
margin: 0 var(--space-5, 10px) 0 var(--space-5, 10px);
|
||||
|
||||
transition: color 0.2s ease;
|
||||
|
||||
color: var(--fgcolor-neutral-primary, #2d2d31);
|
||||
border-radius: var(--corner-radius-medium, 8px);
|
||||
|
||||
cursor: pointer;
|
||||
/* Body text/level 2 Regular */
|
||||
font-family: Inter;
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 150%; /* 21px */
|
||||
}
|
||||
|
||||
.trigger:hover {
|
||||
background: var(--overlay-neutral-hover, rgba(25, 25, 28, 0.03));
|
||||
}
|
||||
|
||||
:global(.trigger[data-highlighted]) {
|
||||
outline: none;
|
||||
background: var(--bgcolor-neutral-secondary, #f4f4f7);
|
||||
}
|
||||
|
||||
:global(.trigger[data-highlighted]:focus-visible) {
|
||||
outline: none;
|
||||
box-shadow: 0 0 0 2px var(--bgcolor-neutral-secondary, #f4f4f7);
|
||||
}
|
||||
|
||||
.trigger:focus-visible {
|
||||
z-index: 30;
|
||||
box-shadow:
|
||||
var(--shadow-offsetx-0, 0px) var(--shadow-offsety-0, 0px) 0 2px
|
||||
var(--bgcolor-neutral-default, #fafafb),
|
||||
0 0 0 4px var(--border-focus, #818186);
|
||||
}
|
||||
.separator {
|
||||
height: 1px;
|
||||
margin-block: 2px;
|
||||
margin-inline-start: calc(var(--base-4) * -1);
|
||||
width: calc(100% + var(--base-8));
|
||||
background-color: var(--border-neutral);
|
||||
}
|
||||
.breadcrumb-separator {
|
||||
color: var(--fgcolor-neutral-tertiary, #97979b);
|
||||
}
|
||||
</style>
|
||||
@@ -1,8 +1,18 @@
|
||||
<script context="module" lang="ts">
|
||||
export type BaseCardProps = Partial<{
|
||||
variant: 'primary' | 'secondary';
|
||||
radius: 's' | 'm' | 'l';
|
||||
padding: 'none' | 'xxxs' | 'xxs' | 'xs' | 's' | 'm' | 'l';
|
||||
border: 'solid' | 'dashed';
|
||||
shadow?: boolean;
|
||||
disabled?: boolean;
|
||||
}>;
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { clickOnEnter } from '$lib/helpers/a11y';
|
||||
import { Card, Layout } from '@appwrite.io/pink-svelte';
|
||||
|
||||
type BaseProps = {
|
||||
isTile?: boolean;
|
||||
isDashed?: boolean;
|
||||
danger?: boolean;
|
||||
style?: string;
|
||||
@@ -19,41 +29,43 @@
|
||||
isButton?: never;
|
||||
};
|
||||
|
||||
type $$Props = BaseProps & (ButtonProps | AnchorProps | BaseProps);
|
||||
type $$Props = BaseProps & (ButtonProps | AnchorProps | BaseProps) & BaseCardProps;
|
||||
|
||||
export let isTile = false;
|
||||
export let isDashed = false;
|
||||
export let isButton = false;
|
||||
export let danger = false;
|
||||
export let href: string = null;
|
||||
let classes = '';
|
||||
export { classes as class };
|
||||
export let style = '';
|
||||
export let padding: $$Props['padding'] = 'm';
|
||||
export let radius: $$Props['radius'] = 'm';
|
||||
export let variant: $$Props['variant'] = 'primary';
|
||||
|
||||
function getElement() {
|
||||
switch (true) {
|
||||
case !!href:
|
||||
return 'a';
|
||||
case isButton:
|
||||
return 'button';
|
||||
default:
|
||||
return 'article';
|
||||
}
|
||||
}
|
||||
$: resolvedClasses = [classes].filter(Boolean).join(' ');
|
||||
</script>
|
||||
|
||||
<svelte:element
|
||||
this={getElement()}
|
||||
class="card {classes}"
|
||||
class:common-section={!isTile}
|
||||
class:is-border-dashed={isDashed}
|
||||
class:is-danger={danger}
|
||||
class:is-allowed-focus={href}
|
||||
{...$$restProps}
|
||||
{style}
|
||||
on:click
|
||||
on:keyup={clickOnEnter}
|
||||
role={href || isButton ? 'button' : 'presentation'}
|
||||
{href}>
|
||||
<slot />
|
||||
</svelte:element>
|
||||
{#if href}
|
||||
<Card.Link class={resolvedClasses} {href} {style} {padding} {radius} {variant} on:click>
|
||||
<Layout.Stack gap="xl">
|
||||
<slot />
|
||||
</Layout.Stack>
|
||||
</Card.Link>
|
||||
{:else if isButton}
|
||||
<Card.Button class={resolvedClasses} {style} {padding} {radius} {variant} on:click>
|
||||
<Layout.Stack gap="xl">
|
||||
<slot />
|
||||
</Layout.Stack>
|
||||
</Card.Button>
|
||||
{:else}
|
||||
<Card.Base
|
||||
class={resolvedClasses}
|
||||
{style}
|
||||
border={isDashed ? 'dashed' : 'solid'}
|
||||
{padding}
|
||||
{radius}
|
||||
{variant}>
|
||||
<Layout.Stack gap="xl">
|
||||
<slot />
|
||||
</Layout.Stack>
|
||||
</Card.Base>
|
||||
{/if}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/stores';
|
||||
import { page } from '$app/state';
|
||||
import { Empty } from '$lib/components';
|
||||
import { CARD_LIMIT } from '$lib/constants';
|
||||
import { getServiceLimit, type PlanServices } from '$lib/stores/billing';
|
||||
@@ -7,7 +7,7 @@
|
||||
import { isCloud } from '$lib/system';
|
||||
import CardPlanLimit from './cardPlanLimit.svelte';
|
||||
|
||||
export let showEmpty = true;
|
||||
export let disableEmpty = true;
|
||||
export let offset = 0;
|
||||
export let total = 0;
|
||||
export let event: string = null;
|
||||
@@ -16,22 +16,29 @@
|
||||
|
||||
$: planLimit = getServiceLimit(serviceId) || Infinity;
|
||||
|
||||
$: limit = preferences.get($page.params.project, $page.route)?.limit ?? CARD_LIMIT;
|
||||
$: limit = preferences.get(page.route)?.limit ?? CARD_LIMIT;
|
||||
</script>
|
||||
|
||||
<ul
|
||||
class="grid-box common-section u-margin-block-start-32"
|
||||
style={`--grid-gap:1.5rem; --grid-item-size-small-screens: 18rem; --grid-item-size:${total > 3 ? '22rem' : '25rem'};`}
|
||||
data-private>
|
||||
<ul class="grid-box" style={`--grid-item-size:${total > 3 ? '22rem' : '25rem'};`} data-private>
|
||||
<slot />
|
||||
|
||||
{#if total > 3 ? total < limit + offset : total % 2 !== 0}
|
||||
{#if isCloud && serviceId && total >= planLimit}
|
||||
<CardPlanLimit {service} />
|
||||
{:else if showEmpty}
|
||||
<Empty on:click target={event}>
|
||||
{:else}
|
||||
<Empty on:click target={event} disabled={disableEmpty}>
|
||||
<slot name="empty" />
|
||||
</Empty>
|
||||
{/if}
|
||||
{/if}
|
||||
</ul>
|
||||
|
||||
<style lang="scss">
|
||||
.grid-box {
|
||||
display: grid;
|
||||
grid-auto-rows: 1fr;
|
||||
gap: var(--gap-xl);
|
||||
flex-shrink: 0;
|
||||
grid-template-columns: repeat(auto-fit, minmax(var(--grid-item-size), 1fr));
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,30 +1,33 @@
|
||||
<script lang="ts">
|
||||
import { Card } from './';
|
||||
import { Divider, Layout, Card, Typography } from '@appwrite.io/pink-svelte';
|
||||
|
||||
export let danger = false;
|
||||
export let hideOverflow = false;
|
||||
export let hideFooter = false;
|
||||
export let gap: 'none' | 'xxxs' | 'xxs' | 'xs' | 's' | 'm' | 'l' | 'xl' | 'xxl' | 'xxxl' = 'l';
|
||||
</script>
|
||||
|
||||
<Card {danger} class={danger ? 'u-padding-inline-start-32' : ''}>
|
||||
<div class="common-section grid-1-2" class:hideOverflow>
|
||||
<div class="grid-1-2-col-1 u-flex u-flex-vertical u-gap-16">
|
||||
<slot />
|
||||
</div>
|
||||
<div class="grid-1-2-col-2 u-flex u-flex-vertical u-gap-16 u-min-width-0">
|
||||
<slot name="aside" />
|
||||
</div>
|
||||
</div>
|
||||
{#if $$slots.actions && !hideFooter}
|
||||
<div class="common-section card-separator u-flex u-main-end">
|
||||
<slot name="actions" />
|
||||
</div>
|
||||
{/if}
|
||||
</Card>
|
||||
|
||||
<style lang="scss">
|
||||
.hideOverflow > * {
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
<Card.Base>
|
||||
<Layout.Stack gap="xl" justifyContent="space-around">
|
||||
<Layout.GridFraction gap="xxxl" rowGap="xl" start={1} end={2}>
|
||||
<Layout.Stack gap="xxs">
|
||||
<Typography.Title size="s" truncate><slot name="title" /></Typography.Title>
|
||||
{#if $$slots.default}
|
||||
<Typography.Text>
|
||||
<slot />
|
||||
</Typography.Text>
|
||||
{/if}
|
||||
</Layout.Stack>
|
||||
<Layout.Stack {gap}>
|
||||
<slot name="aside" />
|
||||
</Layout.Stack>
|
||||
</Layout.GridFraction>
|
||||
{#if $$slots.actions && !hideFooter}
|
||||
<span
|
||||
style="margin-left: calc(-1* var(--space-9));margin-right: calc(-1* var(--space-9));width:auto;">
|
||||
<Divider />
|
||||
</span>
|
||||
<Layout.Stack direction="row-reverse">
|
||||
<slot name="actions" />
|
||||
</Layout.Stack>
|
||||
{/if}
|
||||
</Layout.Stack>
|
||||
</Card.Base>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { Button } from '$lib/elements/forms';
|
||||
import { upgradeURL } from '$lib/stores/billing';
|
||||
import { Click, trackEvent } from '$lib/actions/analytics';
|
||||
|
||||
export let service: string;
|
||||
</script>
|
||||
@@ -8,6 +9,11 @@
|
||||
<article class="card u-grid u-cross-center u-width-full-line">
|
||||
<div class="u-flex u-flex-vertical u-gap-24 u-main-center u-cross-center">
|
||||
<p class="text u-text-center">Upgrade your plan to add more {service}</p>
|
||||
<Button secondary href={$upgradeURL}>Change plan</Button>
|
||||
<Button
|
||||
secondary
|
||||
href={$upgradeURL}
|
||||
on:click={() => {
|
||||
trackEvent(Click.OrganizationClickUpgrade, { source: 'card_plan_limit' });
|
||||
}}>Change plan</Button>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
@@ -51,7 +51,7 @@
|
||||
{#if label}
|
||||
<Pill>
|
||||
{#if labelIcon}
|
||||
<span class={`icon-${labelIcon}`} aria-hidden="true" />
|
||||
<span class={`icon-${labelIcon}`} aria-hidden="true"></span>
|
||||
{/if}
|
||||
{label}
|
||||
</Pill>
|
||||
@@ -59,7 +59,7 @@
|
||||
{#if withCopy}
|
||||
<Copy value={code}>
|
||||
<button class="button is-small is-text is-only-icon" aria-label="copy code">
|
||||
<span class="icon-duplicate" aria-hidden="true" />
|
||||
<span class="icon-duplicate" aria-hidden="true"></span>
|
||||
</button>
|
||||
</Copy>
|
||||
{/if}
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
<!-- svelte-ignore a11y-no-redundant-roles -->
|
||||
<summary
|
||||
class={`collapsible-button u-position-relative u-gap-${gap}`}
|
||||
style="padding: 0"
|
||||
on:keyup={clickOnEnter}
|
||||
on:click
|
||||
role="button"
|
||||
@@ -43,7 +44,7 @@
|
||||
<span
|
||||
class="icon-cheveron-down u-font-size-20"
|
||||
class:u-color-text-disabled={disabled}
|
||||
aria-hidden="true" />
|
||||
aria-hidden="true"></span>
|
||||
</div>
|
||||
</slot>
|
||||
</summary>
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/state';
|
||||
import type { Writable } from 'svelte/store';
|
||||
import { preferences } from '$lib/stores/preferences';
|
||||
import { onMount, type Snippet } from 'svelte';
|
||||
import type { Column } from '$lib/helpers/types';
|
||||
import { ActionMenu, Layout, Popover, Selector } from '@appwrite.io/pink-svelte';
|
||||
|
||||
let {
|
||||
columns,
|
||||
isCustomCollection = false,
|
||||
allowNoColumns = false,
|
||||
children
|
||||
}: {
|
||||
columns: Writable<Column[]>;
|
||||
isCustomCollection?: boolean;
|
||||
allowNoColumns?: boolean;
|
||||
children: Snippet<[toggle: () => void, selectedColumnsNumber: number]>;
|
||||
} = $props();
|
||||
|
||||
onMount(async () => {
|
||||
if (isCustomCollection) {
|
||||
const prefs = preferences.getCustomCollectionColumns(page.params.collection);
|
||||
columns.set(
|
||||
$columns.map((column) => {
|
||||
column.hide = prefs?.includes(column.id) ?? false;
|
||||
return column;
|
||||
})
|
||||
);
|
||||
} else {
|
||||
const prefs = preferences.get(page.route);
|
||||
|
||||
// Override the shown columns only if a preference was set
|
||||
if (prefs?.columns) {
|
||||
columns.set(
|
||||
$columns.map((column) => {
|
||||
column.hide = prefs.columns?.includes(column.id) ?? false;
|
||||
return column;
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
columns.subscribe((ctx) => {
|
||||
const columns = ctx.filter((n) => n.hide === true).map((n) => n.id);
|
||||
|
||||
if (isCustomCollection) {
|
||||
preferences.setCustomCollectionColumns(columns);
|
||||
} else {
|
||||
preferences.setColumns(columns);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
let selectedColumnsNumber = $derived(
|
||||
$columns.reduce((acc, column) => {
|
||||
if (column.hide === true) return acc;
|
||||
|
||||
return ++acc;
|
||||
}, 0)
|
||||
);
|
||||
</script>
|
||||
|
||||
{#if $columns?.length}
|
||||
<Popover let:toggle placement="bottom-end" padding="none">
|
||||
{@render children(toggle, selectedColumnsNumber)}
|
||||
<svelte:fragment slot="tooltip">
|
||||
<ActionMenu.Root>
|
||||
{#each $columns as column}
|
||||
{#if !column?.exclude}
|
||||
<ActionMenu.Item.Button
|
||||
on:click={() => (column.hide = !column.hide)}
|
||||
disabled={allowNoColumns
|
||||
? false
|
||||
: selectedColumnsNumber <= 1 && column.hide !== true}>
|
||||
<Layout.Stack direction="row" gap="s">
|
||||
<Selector.Checkbox
|
||||
checked={!column.hide}
|
||||
size="s"
|
||||
on:click={() => (column.hide = !column.hide)} />
|
||||
{column.title}
|
||||
</Layout.Stack>
|
||||
</ActionMenu.Item.Button>
|
||||
{/if}
|
||||
{/each}
|
||||
</ActionMenu.Root>
|
||||
</svelte:fragment>
|
||||
</Popover>
|
||||
{/if}
|
||||
@@ -0,0 +1,68 @@
|
||||
<script lang="ts">
|
||||
import { Button, Form, InputCheckbox } from '$lib/elements/forms';
|
||||
import { Alert, Dialog, Layout } from '@appwrite.io/pink-svelte';
|
||||
|
||||
export let open: boolean;
|
||||
export let title: string;
|
||||
export let error: string = null;
|
||||
export let action: string = 'Delete';
|
||||
export let canDelete: boolean = true;
|
||||
export let disabled: boolean = false;
|
||||
export let confirmDeletion: boolean = false;
|
||||
export let onSubmit: (e: SubmitEvent) => Promise<void> | void = function () {
|
||||
return;
|
||||
};
|
||||
|
||||
let confirm = false;
|
||||
let checkboxId = `delete_${title.replaceAll(' ', '_').toLowerCase()}`;
|
||||
|
||||
// reset checkbox status
|
||||
$: if (open && confirmDeletion) {
|
||||
confirm = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
<Form isModal {onSubmit}>
|
||||
<Dialog {title} bind:open>
|
||||
<Layout.Stack gap="xl">
|
||||
{#if error}
|
||||
<Alert.Inline
|
||||
dismissible
|
||||
status="error"
|
||||
on:dismiss={() => {
|
||||
error = null;
|
||||
}}>
|
||||
{error}
|
||||
</Alert.Inline>
|
||||
{/if}
|
||||
|
||||
<Layout.Stack gap="l">
|
||||
<slot />
|
||||
|
||||
{#if confirmDeletion}
|
||||
<InputCheckbox
|
||||
size="s"
|
||||
required
|
||||
id={checkboxId}
|
||||
bind:checked={confirm}
|
||||
label="I understand and confirm" />
|
||||
{/if}
|
||||
</Layout.Stack>
|
||||
</Layout.Stack>
|
||||
|
||||
<svelte:fragment slot="footer">
|
||||
<Layout.Stack direction="row" gap="s" justifyContent="flex-end">
|
||||
<slot name="footer">
|
||||
<Button text on:click={() => (open = false)}>Cancel</Button>
|
||||
{#if canDelete}
|
||||
<Button
|
||||
danger
|
||||
submit
|
||||
disabled={disabled || (confirmDeletion ? !confirm : false)}
|
||||
>{action}</Button>
|
||||
{/if}
|
||||
</slot>
|
||||
</Layout.Stack>
|
||||
</svelte:fragment>
|
||||
</Dialog>
|
||||
</Form>
|
||||
@@ -122,7 +122,7 @@
|
||||
</Modal>
|
||||
|
||||
<style lang="scss">
|
||||
@use '@appwrite.io/pink/src/abstract/variables/devices';
|
||||
@use '@appwrite.io/pink-legacy/src/abstract/variables/devices';
|
||||
|
||||
.card {
|
||||
position: fixed;
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
<script lang="ts">
|
||||
import { trackEvent } from '$lib/actions/analytics';
|
||||
import { tooltip } from '$lib/actions/tooltip';
|
||||
import { clickOnEnter } from '$lib/helpers/a11y';
|
||||
import { copy } from '$lib/helpers/copy';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
import { Tooltip } from '@appwrite.io/pink-svelte';
|
||||
|
||||
export let value: string;
|
||||
export let event: string = null;
|
||||
export let eventContext = 'click_id_tag';
|
||||
export let tooltipDisabled = false;
|
||||
export let copyText: string = 'Click to copy';
|
||||
export let appendTo: Parameters<typeof tooltip>['1']['appendTo'] = undefined;
|
||||
|
||||
let content = copyText;
|
||||
|
||||
@@ -31,20 +31,20 @@
|
||||
});
|
||||
}
|
||||
}
|
||||
//TODO: remove this component
|
||||
</script>
|
||||
|
||||
<span
|
||||
data-private
|
||||
role="button"
|
||||
tabindex="0"
|
||||
style:cursor="pointer"
|
||||
on:click|preventDefault={handleClick}
|
||||
on:keyup={clickOnEnter}
|
||||
on:mouseenter={() => setTimeout(() => (content = copyText))}
|
||||
use:tooltip={{
|
||||
content,
|
||||
hideOnClick: false,
|
||||
appendTo
|
||||
}}>
|
||||
<slot />
|
||||
</span>
|
||||
<Tooltip disabled={tooltipDisabled}>
|
||||
<span
|
||||
data-private
|
||||
style:display="inline-flex"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
style:cursor="pointer"
|
||||
on:click|preventDefault|stopPropagation={handleClick}
|
||||
on:keyup={clickOnEnter}
|
||||
on:mouseenter={() => setTimeout(() => (content = 'Click to copy'))}>
|
||||
<slot />
|
||||
</span>
|
||||
<p slot="tooltip">{content}</p>
|
||||
</Tooltip>
|
||||
|
||||
@@ -1,58 +1,17 @@
|
||||
<script lang="ts">
|
||||
import { tooltip } from '$lib/actions/tooltip';
|
||||
import { Label } from '$lib/elements/forms';
|
||||
import type { SvelteComponent } from 'svelte';
|
||||
import { Copy, Drop } from '.';
|
||||
import { Copy } from '.';
|
||||
import { Input, Layout } from '@appwrite.io/pink-svelte';
|
||||
import { IconDuplicate } from '@appwrite.io/pink-icons-svelte';
|
||||
|
||||
export let value: string;
|
||||
export let label: string = null;
|
||||
export let optionalText: string | undefined = undefined;
|
||||
export let showLabel = false;
|
||||
export let labelTooltip: string = null;
|
||||
export let popover: typeof SvelteComponent<unknown> = null;
|
||||
export let popoverProps: Record<string, unknown> = {};
|
||||
export let appendTo: Parameters<typeof tooltip>['1']['appendTo'] = undefined;
|
||||
|
||||
let show = false;
|
||||
</script>
|
||||
|
||||
<div>
|
||||
{#if label}
|
||||
<Label tooltip={labelTooltip} {optionalText} hide={!showLabel} for={label}>
|
||||
{label}{#if popover}
|
||||
<Drop isPopover bind:show display="inline-block">
|
||||
<!-- TODO: make unclicked icon greyed out and hover and clicked filled -->
|
||||
<button
|
||||
type="button"
|
||||
on:click={() => (show = !show)}
|
||||
class="tooltip"
|
||||
aria-label="input tooltip">
|
||||
<span
|
||||
class="icon-info"
|
||||
aria-hidden="true"
|
||||
style:font-size="var(--icon-size-small)" />
|
||||
</button>
|
||||
<svelte:fragment slot="list">
|
||||
<div
|
||||
class="dropped card u-max-width-250"
|
||||
style:--p-card-padding=".75rem"
|
||||
style:--card-border-radius="var(--border-radius-small)"
|
||||
style:box-shadow="var(--shadow-large)">
|
||||
<svelte:component this={popover} {...popoverProps} />
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
</Drop>
|
||||
{/if}
|
||||
</Label>
|
||||
{/if}
|
||||
<div class="input-text-wrapper" style="--amount-of-buttons:1">
|
||||
<input id={label} type="text" {value} readonly />
|
||||
<div class="options-list">
|
||||
{#key appendTo}
|
||||
<Copy {value} {appendTo}>
|
||||
<span class="icon-duplicate" aria-hidden="true" />
|
||||
</Copy>
|
||||
{/key}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Input.Text readonly {value} {label} helper={optionalText} required>
|
||||
<Copy {value} slot="end">
|
||||
<Layout.Stack>
|
||||
<Input.Action icon={IconDuplicate} />
|
||||
</Layout.Stack>
|
||||
</Copy>
|
||||
</Input.Text>
|
||||
|
||||
@@ -1,41 +1,41 @@
|
||||
<script lang="ts">
|
||||
import type { PaymentMethodData } from '$lib/sdk/billing';
|
||||
import { Alert } from '.';
|
||||
import { Badge, Layout, Link, Popover, Table } from '@appwrite.io/pink-svelte';
|
||||
import CreditCardBrandImage from './creditCardBrandImage.svelte';
|
||||
import type { TableRootProp } from '$lib/helpers/types';
|
||||
|
||||
export let isBox = false;
|
||||
export let root: TableRootProp;
|
||||
export let paymentMethod: PaymentMethodData;
|
||||
export let isBackup: boolean = false;
|
||||
</script>
|
||||
|
||||
<div class:box={isBox}>
|
||||
<div class="u-flex u-main-space-between u-cross-start" style="padding-block: 0.5rem;">
|
||||
<div class="u-line-height-1-5 u-flex u-flex-vertical u-gap-2">
|
||||
<span class="u-flex u-cross-center u-gap-8">
|
||||
<p class="text u-bold">
|
||||
<span class="u-capitalize">{paymentMethod?.brand}</span> ending in {paymentMethod?.last4}
|
||||
</p>
|
||||
<CreditCardBrandImage brand={paymentMethod?.brand} />
|
||||
</span>
|
||||
<p class="text">
|
||||
Expires {paymentMethod?.expiryMonth}/{paymentMethod?.expiryYear}
|
||||
</p>
|
||||
{#if paymentMethod?.name}
|
||||
<p class="text">
|
||||
{paymentMethod.name}
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<slot />
|
||||
</div>
|
||||
{#if paymentMethod?.expired}
|
||||
<Alert type="error" class="u-margin-block-start-16 u-width-full-line">
|
||||
<svelte:fragment slot="title">This payment method has expired</svelte:fragment>
|
||||
</Alert>
|
||||
<Table.Cell column="cc" {root}>
|
||||
<Layout.Stack direction="row" alignItems="center" gap="s">
|
||||
<CreditCardBrandImage brand={paymentMethod?.brand} />
|
||||
<span>ending in {paymentMethod?.last4}</span>
|
||||
{#if isBackup}
|
||||
<Badge variant="secondary" content="Backup" />
|
||||
{/if}
|
||||
</Layout.Stack>
|
||||
</Table.Cell>
|
||||
<Table.Cell column="name" {root}>{paymentMethod?.name}</Table.Cell>
|
||||
<Table.Cell column="expiry" {root}
|
||||
>{paymentMethod?.expiryMonth}/{paymentMethod?.expiryYear}</Table.Cell>
|
||||
<Table.Cell column="status" {root}>
|
||||
{#if paymentMethod?.lastError || paymentMethod?.expired}
|
||||
<Popover let:toggle>
|
||||
<Layout.Stack gap="xs" direction="row">
|
||||
<Badge variant="secondary" type="error" content="Failed" />
|
||||
<Link.Button on:click={toggle}>Details</Link.Button>
|
||||
</Layout.Stack>
|
||||
<svelte:fragment slot="tooltip">
|
||||
{#if paymentMethod?.expired}
|
||||
This payment method has expired
|
||||
{/if}
|
||||
{#if paymentMethod?.lastError}
|
||||
{paymentMethod.lastError}
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
</Popover>
|
||||
{/if}
|
||||
{#if paymentMethod?.lastError}
|
||||
<Alert type="error" class="u-margin-block-start-16 u-width-full-line">
|
||||
{paymentMethod.lastError}
|
||||
</Alert>
|
||||
{/if}
|
||||
</div>
|
||||
</Table.Cell>
|
||||
|
||||
@@ -0,0 +1,242 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { base } from '$app/paths';
|
||||
import { page } from '$app/state';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { Dependencies } from '$lib/constants';
|
||||
import { goto, invalidate } from '$app/navigation';
|
||||
import { getProjectId } from '$lib/helpers/project';
|
||||
import { writable, type Writable } from 'svelte/store';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
import { Layout, Typography } from '@appwrite.io/pink-svelte';
|
||||
import { type Models, type Payload, Query } from '@appwrite.io/console';
|
||||
|
||||
type ImportItem = {
|
||||
status: string;
|
||||
collection?: string;
|
||||
};
|
||||
|
||||
type ImportItemsMap = Map<string, ImportItem>;
|
||||
|
||||
/**
|
||||
* Keeps a track of the active and ongoing csv migrations.
|
||||
*
|
||||
* The structure is as follows -
|
||||
* `{ migrationId: { status: status, collection: collection } }`
|
||||
*/
|
||||
const importItems: Writable<ImportItemsMap> = writable(new Map());
|
||||
|
||||
async function showCompletionNotification(
|
||||
databaseId: string,
|
||||
collectionId: string,
|
||||
importData: Payload
|
||||
) {
|
||||
await invalidate(Dependencies.DOCUMENTS);
|
||||
const url = `${base}/project-${page.params.region}-${page.params.project}/databases/database-${databaseId}/collection-${collectionId}`;
|
||||
|
||||
// extract clean message from nested backend error.
|
||||
const match = importData.errors.join('').match(/message: '(.*)' Message:/i);
|
||||
const errorMessage = match?.[1];
|
||||
|
||||
const type = importData.status === 'completed' ? 'success' : 'error';
|
||||
const message =
|
||||
importData.status === 'completed'
|
||||
? 'CSV import finished successfully.'
|
||||
: `${errorMessage}`;
|
||||
|
||||
addNotification({
|
||||
type,
|
||||
message,
|
||||
isHtml: true,
|
||||
buttons:
|
||||
collectionId === page.params.collection || type === 'error'
|
||||
? undefined
|
||||
: [
|
||||
{
|
||||
name: 'View documents',
|
||||
method: () => goto(url)
|
||||
}
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
async function updateOrAddItem(importData: Payload | Models.Migration) {
|
||||
if (importData.source.toLowerCase() !== 'csv') return;
|
||||
|
||||
const status = importData.status;
|
||||
const resourceId = importData.resourceId ?? '';
|
||||
const [databaseId, collectionId] = resourceId.split(':') ?? [];
|
||||
|
||||
const current = $importItems.get(importData.$id);
|
||||
let collectionName = current?.collection ?? null;
|
||||
|
||||
if (!collectionName && collectionId) {
|
||||
try {
|
||||
const collection = await sdk
|
||||
.forProject(page.params.region, page.params.project)
|
||||
.databases.getCollection(databaseId, collectionId);
|
||||
collectionName = collection.name;
|
||||
} catch {
|
||||
collectionName = null;
|
||||
}
|
||||
}
|
||||
|
||||
importItems.update((items) => {
|
||||
const existing = items.get(importData.$id);
|
||||
|
||||
const isDone = (s: string) => s === 'completed' || s === 'failed';
|
||||
const isInProgress = (s: string) => ['pending', 'processing', 'uploading'].includes(s);
|
||||
|
||||
const shouldSkip =
|
||||
(existing && isDone(existing.status) && isInProgress(status)) ||
|
||||
existing?.status === status;
|
||||
|
||||
if (shouldSkip) return items;
|
||||
|
||||
const next = new Map(items);
|
||||
next.set(importData.$id, { status, collection: collectionName ?? undefined });
|
||||
return next;
|
||||
});
|
||||
|
||||
if (status === 'completed' || status === 'failed') {
|
||||
await showCompletionNotification(databaseId, collectionId, importData);
|
||||
}
|
||||
}
|
||||
|
||||
function clear() {
|
||||
importItems.update((items) => {
|
||||
items.clear();
|
||||
return items;
|
||||
});
|
||||
}
|
||||
|
||||
function graphSize(status: string): number {
|
||||
switch (status) {
|
||||
case 'pending':
|
||||
return 10;
|
||||
case 'processing':
|
||||
return 30;
|
||||
case 'uploading':
|
||||
return 60;
|
||||
case 'completed':
|
||||
case 'failed':
|
||||
return 100;
|
||||
default:
|
||||
return 30;
|
||||
}
|
||||
}
|
||||
|
||||
function text(status: string, collectionName = '') {
|
||||
const name = collectionName ? `<b>${collectionName}</b>` : '';
|
||||
switch (status) {
|
||||
case 'completed':
|
||||
case 'failed':
|
||||
return `Import to ${name} ${status}`;
|
||||
case 'processing':
|
||||
return `Importing CSV file${name ? ` to ${name}` : ''}`;
|
||||
default:
|
||||
return 'Preparing CSV for import...';
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
sdk.forProject(page.params.region, page.params.project)
|
||||
.migrations.list([
|
||||
Query.equal('source', 'CSV'),
|
||||
Query.equal('status', ['pending', 'processing'])
|
||||
])
|
||||
.then((migrations) => {
|
||||
migrations.migrations.forEach(updateOrAddItem);
|
||||
});
|
||||
|
||||
return sdk.forConsole.client.subscribe('console', (response) => {
|
||||
if (!response.channels.includes(`projects.${getProjectId()}`)) return;
|
||||
if (response.events.includes('migrations.*')) {
|
||||
updateOrAddItem(response.payload as Payload);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
$: isOpen = true;
|
||||
$: showCsvImportBox = $importItems.size > 0;
|
||||
</script>
|
||||
|
||||
{#if showCsvImportBox}
|
||||
<Layout.Stack direction="column" gap="l" alignItems="flex-end">
|
||||
<section class="upload-box">
|
||||
<header class="upload-box-header">
|
||||
<h4 class="upload-box-title">
|
||||
<Typography.Text variant="m-500">
|
||||
Importing documents ({$importItems.size})
|
||||
</Typography.Text>
|
||||
</h4>
|
||||
<button
|
||||
class="upload-box-button"
|
||||
class:is-open={isOpen}
|
||||
aria-label="toggle upload box"
|
||||
on:click={() => (isOpen = !isOpen)}>
|
||||
<span class="icon-cheveron-up" aria-hidden="true"></span>
|
||||
</button>
|
||||
<button
|
||||
class="upload-box-button"
|
||||
aria-label="close backup restore box"
|
||||
on:click={clear}>
|
||||
<span class="icon-x" aria-hidden="true"></span>
|
||||
</button>
|
||||
</header>
|
||||
|
||||
{#each [...$importItems.entries()] as [key, value] (key)}
|
||||
<div class="upload-box-content" class:is-open={isOpen}>
|
||||
<ul class="upload-box-list">
|
||||
<li class="upload-box-item">
|
||||
<section class="progress-bar u-width-full-line">
|
||||
<div
|
||||
class="progress-bar-top-line u-flex u-gap-8 u-main-space-between">
|
||||
<Typography.Text>
|
||||
{@html text(value.status, value.collection)}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
<div
|
||||
class="progress-bar-container"
|
||||
class:is-danger={value.status === 'failed'}
|
||||
style="--graph-size:{graphSize(value.status)}%">
|
||||
</div>
|
||||
</section>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
{/each}
|
||||
</section>
|
||||
</Layout.Stack>
|
||||
{/if}
|
||||
|
||||
<style lang="scss">
|
||||
.upload-box-title {
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.upload-box-content {
|
||||
min-width: 400px;
|
||||
max-width: 100vw;
|
||||
}
|
||||
|
||||
.upload-box-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.progress-bar-container {
|
||||
height: 4px;
|
||||
|
||||
&::before {
|
||||
height: 4px;
|
||||
background-color: var(--bgcolor-neutral-invert);
|
||||
}
|
||||
|
||||
&.is-danger::before {
|
||||
height: 4px;
|
||||
background-color: var(--bgcolor-error);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,13 +1,14 @@
|
||||
<script lang="ts">
|
||||
import { trackEvent } from '$lib/actions/analytics';
|
||||
import { InnerModal } from '$lib/components';
|
||||
import { Click, trackEvent } from '$lib/actions/analytics';
|
||||
import { InputId } from '$lib/elements/forms';
|
||||
import { InputProjectId } from '$lib/elements/forms';
|
||||
import Button from '$lib/elements/forms/button.svelte';
|
||||
import { IconX } from '@appwrite.io/pink-icons-svelte';
|
||||
import { Card, Divider, Icon, Layout, Typography } from '@appwrite.io/pink-svelte';
|
||||
export let show = false;
|
||||
export let name: string;
|
||||
export let id: string;
|
||||
export let autofocus = true;
|
||||
export let fullWidth = false;
|
||||
export let isProject = false;
|
||||
$: if (!show) {
|
||||
id = null;
|
||||
@@ -18,22 +19,36 @@
|
||||
}
|
||||
|
||||
$: if (show) {
|
||||
trackEvent('click_show_custom_id');
|
||||
trackEvent(Click.ShowCustomIdClick);
|
||||
}
|
||||
</script>
|
||||
|
||||
<InnerModal bind:show {fullWidth}>
|
||||
<svelte:fragment slot="title">{name} ID</svelte:fragment>
|
||||
<svelte:fragment slot="subtitle">
|
||||
Enter a custom {name} ID. Leave blank for a randomly generated one.
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="content">
|
||||
<div class="form">
|
||||
{#if show}
|
||||
<Card.Base
|
||||
variant="secondary"
|
||||
padding="s"
|
||||
--input-background-color="var(--bgcolor-neutral-primary)">
|
||||
<Layout.Stack gap="xl">
|
||||
<Layout.Stack gap="s">
|
||||
<Layout.Stack direction="row" justifyContent="space-between" alignContent="center">
|
||||
<Typography.Text variant="m-600">{name} ID</Typography.Text>
|
||||
<Button extraCompact on:click={() => (show = false)}>
|
||||
<Icon icon={IconX} size="s" />
|
||||
</Button>
|
||||
</Layout.Stack>
|
||||
<Typography.Text>
|
||||
Enter a custom {name} ID. Leave blank for a randomly generated one.
|
||||
</Typography.Text>
|
||||
</Layout.Stack>
|
||||
<span
|
||||
style="margin-left: calc(-1* var(--space-7));margin-right: calc(-1* var(--space-7));width:auto;">
|
||||
<Divider />
|
||||
</span>
|
||||
{#if isProject}
|
||||
<InputProjectId bind:value={id} {autofocus} />
|
||||
{:else}
|
||||
<InputId bind:value={id} {autofocus} />
|
||||
<InputId required bind:value={id} {autofocus} />
|
||||
{/if}
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
</InnerModal>
|
||||
</Layout.Stack>
|
||||
</Card.Base>
|
||||
{/if}
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
<script lang="ts">
|
||||
import { Link } from '$lib/elements';
|
||||
import { consoleVariables } from '$routes/(console)/store';
|
||||
import { IconInfo } from '@appwrite.io/pink-icons-svelte';
|
||||
import {
|
||||
Badge,
|
||||
Layout,
|
||||
Typography,
|
||||
Table,
|
||||
Icon,
|
||||
InteractiveText
|
||||
} from '@appwrite.io/pink-svelte';
|
||||
|
||||
export let domain: string;
|
||||
export let verified = false;
|
||||
|
||||
let subdomain = domain.split('.').slice(0, -2).join('.');
|
||||
</script>
|
||||
|
||||
<Layout.Stack gap="xl">
|
||||
<Layout.Stack gap="s">
|
||||
<Layout.Stack gap="s" direction="row" alignItems="center">
|
||||
<Typography.Text variant="l-500" color="--fgcolor-neutral-primary">
|
||||
{domain}
|
||||
</Typography.Text>
|
||||
{#if verified}
|
||||
<Badge variant="secondary" type="success" content="Verified" />
|
||||
{:else if verified === false}
|
||||
<Badge variant="secondary" type="error" content="Verification failed" />
|
||||
{:else}
|
||||
<Badge
|
||||
variant="secondary"
|
||||
type="warning"
|
||||
size="xs"
|
||||
content="Pending verification" />
|
||||
{/if}
|
||||
</Layout.Stack>
|
||||
<Typography.Text variant="m-400">
|
||||
Add the following record on your DNS provider. Note that DNS changes may take time to
|
||||
propagate fully.
|
||||
</Typography.Text>
|
||||
</Layout.Stack>
|
||||
|
||||
<Table.Root columns={3} let:root>
|
||||
<svelte:fragment slot="header" let:root>
|
||||
<Table.Header.Cell {root}>Type</Table.Header.Cell>
|
||||
<Table.Header.Cell {root}>Name</Table.Header.Cell>
|
||||
<Table.Header.Cell {root}>Value</Table.Header.Cell>
|
||||
</svelte:fragment>
|
||||
<Table.Row.Base {root}>
|
||||
<Table.Cell {root}>CNAME</Table.Cell>
|
||||
<Table.Cell {root}>{subdomain}</Table.Cell>
|
||||
<Table.Cell {root}>
|
||||
<InteractiveText
|
||||
variant="copy"
|
||||
isVisible
|
||||
text={$consoleVariables._APP_DOMAIN_TARGET_CNAME} />
|
||||
</Table.Cell>
|
||||
</Table.Row.Base>
|
||||
</Table.Root>
|
||||
<Layout.Stack gap="s" direction="row" alignItems="center">
|
||||
<Icon icon={IconInfo} size="s" color="--fgcolor-neutral-secondary" />
|
||||
<Typography.Text variant="m-400" color="--fgcolor-neutral-secondary">
|
||||
A list of all domain providers and their DNS setting is available <Link
|
||||
variant="muted"
|
||||
external
|
||||
href="https://appwrite.io/docs/advanced/platform/custom-domains">here</Link
|
||||
>.
|
||||
</Typography.Text>
|
||||
</Layout.Stack>
|
||||
</Layout.Stack>
|
||||
@@ -0,0 +1,44 @@
|
||||
<script lang="ts">
|
||||
import { consoleVariables } from '$routes/(console)/store';
|
||||
|
||||
import { Badge, Layout, Typography, Table, InteractiveText } from '@appwrite.io/pink-svelte';
|
||||
|
||||
export let domain: string;
|
||||
export let verified = false;
|
||||
|
||||
const nameserverList = $consoleVariables?._APP_DOMAINS_NAMESERVERS
|
||||
? $consoleVariables?._APP_DOMAINS_NAMESERVERS?.split(',')
|
||||
: ['ns1.appwrite.io', 'ns2.appwrite.io'];
|
||||
</script>
|
||||
|
||||
<Layout.Stack gap="s">
|
||||
<Layout.Stack gap="s" direction="row" alignItems="center">
|
||||
<Typography.Text variant="l-500" color="--fgcolor-neutral-primary">
|
||||
{domain}
|
||||
</Typography.Text>
|
||||
{#if verified}
|
||||
<Badge variant="secondary" type="success" content="Verified" />
|
||||
{:else if verified === false}
|
||||
<Badge variant="secondary" type="warning" size="xs" content="Pending verification" />
|
||||
{/if}
|
||||
</Layout.Stack>
|
||||
<Typography.Text variant="m-400">
|
||||
Add the following nameservers on your DNS provider. Note that DNS changes may take time to
|
||||
propagate fully.
|
||||
</Typography.Text>
|
||||
</Layout.Stack>
|
||||
|
||||
<Table.Root columns={2} let:root>
|
||||
<svelte:fragment slot="header" let:root>
|
||||
<Table.Header.Cell {root}>Type</Table.Header.Cell>
|
||||
<Table.Header.Cell {root}>Value</Table.Header.Cell>
|
||||
</svelte:fragment>
|
||||
{#each nameserverList as nameserver}
|
||||
<Table.Row.Base {root}>
|
||||
<Table.Cell {root}>NS</Table.Cell>
|
||||
<Table.Cell {root}>
|
||||
<InteractiveText variant="copy" isVisible text={nameserver} />
|
||||
</Table.Cell>
|
||||
</Table.Row.Base>
|
||||
{/each}
|
||||
</Table.Root>
|
||||
@@ -0,0 +1,92 @@
|
||||
<script lang="ts">
|
||||
import { Link } from '$lib/elements';
|
||||
import { consoleVariables } from '$routes/(console)/store';
|
||||
import { IconInfo } from '@appwrite.io/pink-icons-svelte';
|
||||
import {
|
||||
Badge,
|
||||
Layout,
|
||||
Typography,
|
||||
Table,
|
||||
Icon,
|
||||
InteractiveText,
|
||||
Alert
|
||||
} from '@appwrite.io/pink-svelte';
|
||||
|
||||
export let domain: string;
|
||||
export let verified = false;
|
||||
export let variant: 'cname' | 'a' | 'aaaa';
|
||||
|
||||
let subdomain = domain?.split('.')?.slice(0, -2)?.join('.');
|
||||
|
||||
function setTarget() {
|
||||
switch (variant) {
|
||||
case 'cname':
|
||||
return $consoleVariables._APP_DOMAIN_TARGET_CNAME;
|
||||
case 'a':
|
||||
return $consoleVariables._APP_DOMAIN_TARGET_A;
|
||||
case 'aaaa':
|
||||
return $consoleVariables._APP_DOMAIN_TARGET_AAAA;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<Layout.Stack gap="xl">
|
||||
<Layout.Stack gap="s">
|
||||
<Layout.Stack gap="s" direction="row" alignItems="center">
|
||||
<Typography.Text variant="l-500" color="--fgcolor-neutral-primary">
|
||||
{domain}
|
||||
</Typography.Text>
|
||||
{#if verified}
|
||||
<Badge variant="secondary" type="success" content="Verified" />
|
||||
{:else if verified === false}
|
||||
<Badge variant="secondary" type="error" content="Verification failed" />
|
||||
{:else}
|
||||
<Badge
|
||||
variant="secondary"
|
||||
type="warning"
|
||||
size="xs"
|
||||
content="Pending verification" />
|
||||
{/if}
|
||||
</Layout.Stack>
|
||||
<Typography.Text variant="m-400">
|
||||
Add the following record on your DNS provider. Note that DNS changes may take time to
|
||||
propagate fully.
|
||||
</Typography.Text>
|
||||
</Layout.Stack>
|
||||
|
||||
<Table.Root columns={3} let:root>
|
||||
<svelte:fragment slot="header" let:root>
|
||||
<Table.Header.Cell {root}>Type</Table.Header.Cell>
|
||||
<Table.Header.Cell {root}>Name</Table.Header.Cell>
|
||||
<Table.Header.Cell {root}>Value</Table.Header.Cell>
|
||||
</svelte:fragment>
|
||||
<Table.Row.Base {root}>
|
||||
<Table.Cell {root}>{variant.toUpperCase()}</Table.Cell>
|
||||
<Table.Cell {root}>{subdomain || '@'}</Table.Cell>
|
||||
<Table.Cell {root}>
|
||||
<InteractiveText variant="copy" isVisible text={setTarget()} />
|
||||
</Table.Cell>
|
||||
</Table.Row.Base>
|
||||
</Table.Root>
|
||||
<Layout.Stack gap="s" direction="row" alignItems="center">
|
||||
<Icon icon={IconInfo} size="s" color="--fgcolor-neutral-secondary" />
|
||||
{#if variant === 'cname'}
|
||||
<Alert.Inline>
|
||||
If your domain uses CAA records, ensure certainly.com is authorized — otherwise, SSL
|
||||
setup may fail. A list of all domain providers and their DNS setting is available <Link
|
||||
variant="muted"
|
||||
external
|
||||
href="https://appwrite.io/docs/advanced/platform/custom-domains">here</Link
|
||||
>.
|
||||
</Alert.Inline>
|
||||
{:else}
|
||||
<Typography.Text variant="m-400" color="--fgcolor-neutral-secondary">
|
||||
A list of all domain providers and their DNS setting is available <Link
|
||||
variant="muted"
|
||||
external
|
||||
href="https://appwrite.io/docs/advanced/platform/custom-domains">here</Link
|
||||
>.
|
||||
</Typography.Text>
|
||||
{/if}
|
||||
</Layout.Stack>
|
||||
</Layout.Stack>
|
||||
@@ -125,7 +125,8 @@
|
||||
class="drop-arrow"
|
||||
class:is-popover={isPopover}
|
||||
class:u-hide={!show || (show && noArrow)}
|
||||
bind:this={arrow} />
|
||||
bind:this={arrow}>
|
||||
</div>
|
||||
{#if show}
|
||||
<slot name="list" />
|
||||
{/if}
|
||||
|
||||
@@ -28,10 +28,10 @@
|
||||
{disabled}>
|
||||
<span class="text"><slot /></span>
|
||||
{#if icon}
|
||||
<span class={`icon-${icon}`} aria-hidden="true" />
|
||||
<span class={`icon-${icon}`} aria-hidden="true"></span>
|
||||
{/if}
|
||||
{#if loading}
|
||||
<span class="loader is-small u-line-height-1-5" aria-hidden="true" />
|
||||
<span class="loader is-small u-line-height-1-5" aria-hidden="true"></span>
|
||||
{/if}
|
||||
</button>
|
||||
</li>
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
rel={external ? 'noopener noreferrer' : ''}>
|
||||
<span class="text"><slot /></span>
|
||||
{#if icon}
|
||||
<span class={`icon-${icon}`} style={iconStyle} aria-hidden="true" />
|
||||
<span class={`icon-${icon}`} style={iconStyle} aria-hidden="true"></span>
|
||||
{/if}
|
||||
</a>
|
||||
</li>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user