Merge pull request #1848 from appwrite/feat-pink-v2

Feat pink v2
This commit is contained in:
Torsten Dittmann
2025-05-18 13:13:27 +02:00
committed by GitHub
1334 changed files with 60300 additions and 40335 deletions
-10
View File
@@ -1,10 +0,0 @@
src/sdk.ts
.DS_Store
node_modules
/build
/.svelte-kit
/package
.env
.env.*
!.env.example
/playwright-report
-43
View File
@@ -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
}
};
-1
View File
@@ -24,7 +24,6 @@ dist/
*.swp
### Node ###
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
+23 -23
View File
@@ -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
View File
@@ -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
+2 -2
View File
@@ -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());
});
+51
View File
@@ -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
View File
@@ -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"
}
+1
View File
@@ -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'
+3305 -3848
View File
File diff suppressed because it is too large Load Diff
-124
View File
@@ -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>
+1
View File
@@ -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;
+106 -6
View File
@@ -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'
}
+7 -1
View File
@@ -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
View File
@@ -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
+3 -5
View File
@@ -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 {
+3 -3
View File
@@ -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
View File
@@ -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)"
}
}
}
+1 -1
View File
@@ -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);
+4 -3
View File
@@ -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;
+50 -69
View File
@@ -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>
+30 -15
View File
@@ -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>
+60 -62
View File
@@ -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>
+15 -9
View File
@@ -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
+5 -6
View File
@@ -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;
+4 -4
View File
@@ -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;
+13 -14
View File
@@ -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;
+12 -13
View File
@@ -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)
+3 -5
View File
@@ -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)
+8 -9
View File
@@ -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;
+5 -6
View File
@@ -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;
+10 -11
View File
@@ -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;
+2 -2
View File
@@ -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'> & {
+2 -2
View File
@@ -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">
+15 -12
View File
@@ -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>
+15 -27
View File
@@ -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>
+8 -6
View File
@@ -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>
+29 -21
View File
@@ -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>
+25 -30
View File
@@ -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>
+43 -59
View File
@@ -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 {
+43 -47
View File
@@ -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>
+83 -98
View File
@@ -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}
+59 -91
View File
@@ -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}
+1 -2
View File
@@ -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"
+49 -60
View File
@@ -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>
+30
View File
@@ -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 };
+31 -17
View File
@@ -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}
+6 -15
View File
@@ -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>
+4 -3
View File
@@ -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>
+459
View File
@@ -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>
+42 -30
View File
@@ -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}
+16 -9
View File
@@ -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>
+28 -25
View File
@@ -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>
+7 -1
View File
@@ -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>
+2 -2
View File
@@ -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}
+2 -1
View File
@@ -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>
+89
View File
@@ -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}
+68
View File
@@ -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>
+1 -1
View File
@@ -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;
+17 -17
View File
@@ -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>
+10 -51
View File
@@ -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 -->
&nbsp;<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>
+33 -33
View File
@@ -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>
+242
View File
@@ -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>
+30 -15
View File
@@ -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>
+2 -1
View File
@@ -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}
+2 -2
View File
@@ -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>
+1 -1
View File
@@ -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