mirror of
https://github.com/appwrite/console.git
synced 2026-06-06 19:27:48 +00:00
Merge branch 'appwrite:main' into bug-7362-file-extension-validation
This commit is contained in:
+1
-1
@@ -2,4 +2,4 @@ VITE_APPWRITE_ENDPOINT=http://localhost/v1
|
||||
VITE_APPWRITE_GROWTH_ENDPOINT=
|
||||
VITE_GA_PROJECT=
|
||||
VITE_CONSOLE_MODE=self-hosted
|
||||
VITE_STRIPE_PUBLIC_KEY=
|
||||
VITE_STRIPE_PUBLIC_KEY=
|
||||
@@ -1,13 +1,8 @@
|
||||
name: Tests
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
paths-ignore:
|
||||
- '**/*.md'
|
||||
- 'static/**/*'
|
||||
pull_request:
|
||||
branches: [main]
|
||||
branches: ['**']
|
||||
paths-ignore:
|
||||
- '**/*.md'
|
||||
- 'static/**/*'
|
||||
@@ -23,9 +18,9 @@ jobs:
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18
|
||||
# - name: Audit dependencies
|
||||
# run: npm audit --audit-level low
|
||||
node-version: 20
|
||||
- name: Audit dependencies
|
||||
run: npm audit --audit-level critical
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
- name: Svelte Diagnostics
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
# Ignore files for PNPM, NPM and YARN
|
||||
pnpm-lock.yaml
|
||||
package-lock.json
|
||||
yarn.lock
|
||||
Generated
+1465
-1090
File diff suppressed because it is too large
Load Diff
+18
-19
@@ -10,17 +10,17 @@
|
||||
"sync": "svelte-kit sync",
|
||||
"check": "svelte-check --tsconfig ./tsconfig.json --fail-on-warnings --threshold warning",
|
||||
"check:watch": "svelte-check --tsconfig ./tsconfig.json --watch",
|
||||
"lint": "prettier --ignore-path .gitignore --check --plugin prettier-plugin-svelte . && eslint .",
|
||||
"format": "prettier --ignore-path .gitignore --write --plugin prettier-plugin-svelte .",
|
||||
"test": "vitest run",
|
||||
"test:ui": "vitest --ui",
|
||||
"test:watch": "vitest watch",
|
||||
"lint": "prettier --check . && eslint .",
|
||||
"format": "prettier --write .",
|
||||
"test": "TZ=EST vitest run",
|
||||
"test:ui": "TZ=EST vitest --ui",
|
||||
"test:watch": "TZ=EST vitest watch",
|
||||
"e2e": "playwright test tests/e2e"
|
||||
},
|
||||
"dependencies": {
|
||||
"@appwrite.io/console": "^0.4.2",
|
||||
"@appwrite.io/pink": "0.2.0",
|
||||
"@appwrite.io/pink-icons": "0.2.0",
|
||||
"@appwrite.io/console": "^0.6.0",
|
||||
"@appwrite.io/pink": "0.8.0",
|
||||
"@appwrite.io/pink-icons": "0.8.0",
|
||||
"@popperjs/core": "^2.11.8",
|
||||
"@sentry/svelte": "^7.66.0",
|
||||
"@sentry/tracing": "^7.66.0",
|
||||
@@ -37,16 +37,15 @@
|
||||
"pretty-bytes": "^6.1.1",
|
||||
"prismjs": "^1.29.0",
|
||||
"svelte-confetti": "^1.3.0",
|
||||
"tippy.js": "^6.3.7",
|
||||
"web-vitals": "^3.4.0"
|
||||
"tippy.js": "^6.3.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@melt-ui/pp": "^0.1.4",
|
||||
"@melt-ui/svelte": "^0.61.2",
|
||||
"@playwright/test": "^1.37.1",
|
||||
"@sveltejs/adapter-static": "^2.0.3",
|
||||
"@sveltejs/kit": "^1.24.0",
|
||||
"@sveltejs/vite-plugin-svelte": "^2.4.5",
|
||||
"@sveltejs/adapter-static": "^3.0.1",
|
||||
"@sveltejs/kit": "^2.3.4",
|
||||
"@sveltejs/vite-plugin-svelte": "^3.0.1",
|
||||
"@testing-library/dom": "^9.0.1",
|
||||
"@testing-library/jest-dom": "^5.16.5",
|
||||
"@testing-library/svelte": "^4.0.3",
|
||||
@@ -55,24 +54,24 @@
|
||||
"@types/prismjs": "^1.26.0",
|
||||
"@typescript-eslint/eslint-plugin": "^6.5.0",
|
||||
"@typescript-eslint/parser": "^6.5.0",
|
||||
"@vitest/ui": "^0.29.7",
|
||||
"@vitest/ui": "^1.2.1",
|
||||
"eslint": "^8.48.0",
|
||||
"eslint-config-prettier": "^9.0.0",
|
||||
"eslint-plugin-svelte": "^2.33.0",
|
||||
"jsdom": "^22.1.0",
|
||||
"kleur": "^4.1.5",
|
||||
"prettier": "^3.0.3",
|
||||
"prettier-plugin-svelte": "^3.0.3",
|
||||
"prettier": "^3.2.2",
|
||||
"prettier-plugin-svelte": "^3.1.2",
|
||||
"sass": "^1.66.1",
|
||||
"svelte": "^4.2.0",
|
||||
"svelte": "^4.2.9",
|
||||
"svelte-check": "^3.5.1",
|
||||
"svelte-jester": "^2.3.2",
|
||||
"svelte-preprocess": "^5.0.4",
|
||||
"svelte-sequential-preprocessor": "^2.0.1",
|
||||
"tslib": "^2.6.2",
|
||||
"typescript": "^5.2.2",
|
||||
"vite": "^4.4.9",
|
||||
"vitest": "^0.29.7"
|
||||
"vite": "^5.0.11",
|
||||
"vitest": "^1.2.1"
|
||||
},
|
||||
"type": "module"
|
||||
}
|
||||
|
||||
Generated
-7647
File diff suppressed because it is too large
Load Diff
@@ -3,7 +3,10 @@
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="description" content="" />
|
||||
|
||||
<link rel="icon" type="image/svg+xml" href="/logos/appwrite-icon.svg" />
|
||||
<link rel="mask-icon" type="image/png" href="/logos/appwrite-icon.png" />
|
||||
|
||||
<link
|
||||
rel="preload"
|
||||
href="/fonts/inter/inter-v8-latin-600.woff2"
|
||||
|
||||
Vendored
+1
-3
@@ -1,4 +1,2 @@
|
||||
/// <reference types="@sveltejs/kit" />
|
||||
interface Window {
|
||||
VERCEL_ANALYTICS_ID: string | false;
|
||||
}
|
||||
interface Window {}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import { AppwriteException } from '@appwrite.io/console';
|
||||
import type { HandleClientError } from '@sveltejs/kit';
|
||||
|
||||
export const handleError: HandleClientError = async ({ error, message, status }) => {
|
||||
if (error instanceof AppwriteException && error.code === 0) {
|
||||
status = undefined;
|
||||
message = error.message;
|
||||
}
|
||||
return {
|
||||
message,
|
||||
status
|
||||
};
|
||||
};
|
||||
@@ -139,6 +139,7 @@ export function isTrackingAllowed() {
|
||||
}
|
||||
|
||||
export enum Submit {
|
||||
DownloadDPA = 'submit_download_dpa',
|
||||
Error = 'submit_error',
|
||||
AccountCreate = 'submit_account_create',
|
||||
AccountLogin = 'submit_account_login',
|
||||
@@ -150,6 +151,7 @@ export enum Submit {
|
||||
AccountDelete = 'submit_account_delete',
|
||||
AccountDeleteSession = 'submit_account_delete_session',
|
||||
AccountDeleteAllSessions = 'submit_account_delete_all_sessions',
|
||||
AccountAuthenticatorDelete = 'submit_account_authenticator_delete',
|
||||
UserCreate = 'submit_user_create',
|
||||
UserDelete = 'submit_user_delete',
|
||||
UserUpdateEmail = 'submit_user_update_email',
|
||||
@@ -161,6 +163,9 @@ export enum Submit {
|
||||
UserUpdateStatus = 'submit_user_update_status',
|
||||
UserUpdateVerificationEmail = 'submit_user_update_verification_email',
|
||||
UserUpdateVerificationPhone = 'submit_user_update_verification_phone',
|
||||
UserTargetCreate = 'submit_user_target_create',
|
||||
UserTargetDelete = 'submit_user_target_delete',
|
||||
UserAuthenticatorDelete = 'submit_user_authenticator_delete',
|
||||
OrganizationCreate = 'submit_organization_create',
|
||||
OrganizationDelete = 'submit_organization_delete',
|
||||
OrganizationUpdateName = 'submit_organization_update_name',
|
||||
@@ -242,6 +247,7 @@ export enum Submit {
|
||||
WebhookUpdateUrl = 'submit_webhook_update_url',
|
||||
WebhookUpdateEvents = 'submit_webhook_update_events',
|
||||
WebhookUpdateName = 'submit_webhook_update_name',
|
||||
WebhookUpdateEnabled = 'submit_webhook_update_enabled',
|
||||
WebhookUpdateSecurity = 'submit_webhook_update_security',
|
||||
BucketCreate = 'submit_bucket_create',
|
||||
BucketDelete = 'submit_bucket_delete',
|
||||
@@ -262,6 +268,8 @@ export enum Submit {
|
||||
PaymentMethodCreate = 'submit_payment_method_create',
|
||||
PaymentMethodUpdate = 'submit_payment_method_update',
|
||||
PaymentMethodDelete = 'submit_payment_method_delete',
|
||||
RetryPayment = 'submit_retry_payment',
|
||||
VerifyPayment = 'submit_verify_payment',
|
||||
BillingAddressCreate = 'submit_billing_address_create',
|
||||
BillingAddressUpdate = 'submit_billing_address_update',
|
||||
BillingAddressDelete = 'submit_billing_address_delete',
|
||||
@@ -287,5 +295,17 @@ export enum Submit {
|
||||
SmsResetTemplate = 'submit_sms_reset_template',
|
||||
SmsUpdateInviteTemplate = 'submit_sms_update_invite_template',
|
||||
SmsUpdateLoginTemplate = 'submit_sms_update_login_template',
|
||||
SmsUpdateVerificationTemplate = 'submit_sms_update_verification_template'
|
||||
SmsUpdateVerificationTemplate = 'submit_sms_update_verification_template',
|
||||
MessagingProviderCreate = 'submit_messaging_provider_create',
|
||||
MessagingProviderDelete = 'submit_messaging_provider_delete',
|
||||
MessagingProviderUpdate = 'submit_messaging_provider_update',
|
||||
MessagingMessageCreate = 'submit_messaging_message_create',
|
||||
MessagingMessageUpdate = 'submit_messaging_message_update',
|
||||
MessagingMessageDelete = 'submit_messaging_message_delete',
|
||||
MessagingTopicCreate = 'submit_messaging_topic_create',
|
||||
MessagingTopicDelete = 'submit_messaging_topic_delete',
|
||||
MessagingTopicUpdateName = 'submit_messaging_topic_update_name',
|
||||
MessagingTopicUpdatePermissions = 'submit_messaging_topic_update_permissions',
|
||||
MessagingTopicSubscriberAdd = 'submit_messaging_topic_subscriber_add',
|
||||
MessagingTopicSubscriberDelete = 'submit_messaging_topic_subscriber_delete'
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import { onMount } from 'svelte';
|
||||
import { derived, writable } from 'svelte/store';
|
||||
import { nanoid } from 'nanoid/non-secure';
|
||||
import { trackEvent } from '$lib/actions/analytics';
|
||||
import { omit } from '$lib/helpers/omit';
|
||||
|
||||
const groups = [
|
||||
'ungrouped',
|
||||
@@ -17,6 +18,10 @@ const groups = [
|
||||
'platforms',
|
||||
'databases',
|
||||
'functions',
|
||||
'messaging',
|
||||
'messages',
|
||||
'providers',
|
||||
'topics',
|
||||
'storage',
|
||||
'domains',
|
||||
'webhooks',
|
||||
@@ -44,6 +49,7 @@ type BaseCommand = {
|
||||
forceEnable?: boolean;
|
||||
group?: CommandGroup;
|
||||
icon?: string;
|
||||
image?: string;
|
||||
rank?: number;
|
||||
nested?: boolean;
|
||||
keepOpen?: boolean;
|
||||
@@ -69,7 +75,23 @@ export const disabledMap = writable<Map<string, boolean>>(new Map());
|
||||
|
||||
// Derived stores
|
||||
export const commands = derived(commandMap, ($commandMap) => {
|
||||
return Array.from($commandMap.values()).flat();
|
||||
const res: Command[] = [];
|
||||
const keys = new Set<string>();
|
||||
|
||||
const allCommands = Array.from($commandMap.values()).flat().toReversed();
|
||||
|
||||
for (const command of allCommands) {
|
||||
if (isKeyedCommand(command) && !command.disabled) {
|
||||
const keysString = command.keys.join('+');
|
||||
if (keys.has(keysString)) {
|
||||
res.push(omit(command, 'keys'));
|
||||
continue;
|
||||
}
|
||||
keys.add(keysString);
|
||||
}
|
||||
res.push(command);
|
||||
}
|
||||
return res;
|
||||
});
|
||||
|
||||
const commandsEnabled = derived(disabledMap, ($disabledMap) => {
|
||||
@@ -109,9 +131,9 @@ function hasDisputing(command: KeyedCommand, allCommands: Command[]) {
|
||||
}
|
||||
|
||||
export const commandCenterKeyDownHandler = derived(
|
||||
[commandMap, commandsEnabled, wizard],
|
||||
([$commandMap, enabled, $wizard]) => {
|
||||
const commandsArr = Array.from($commandMap.values()).flat();
|
||||
[commands, commandsEnabled, wizard],
|
||||
([$commands, enabled, $wizard]) => {
|
||||
const commandsArr = $commands;
|
||||
let recentKeyCodes: number[] = [];
|
||||
let validCommands: KeyedCommand[] = [];
|
||||
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
<script lang="ts">
|
||||
import { providers } from '$routes/console/project-[project]/messaging/providers/store';
|
||||
import {
|
||||
messageParams,
|
||||
providerType,
|
||||
targetsById
|
||||
} from '$routes/console/project-[project]/messaging/wizard/store';
|
||||
import { MessagingProviderType } from '@appwrite.io/console';
|
||||
import Template from './template.svelte';
|
||||
import { wizard } from '$lib/stores/wizard';
|
||||
import Create from '$routes/console/project-[project]/messaging/create.svelte';
|
||||
import { topicsById } from '$routes/console/project-[project]/messaging/store';
|
||||
|
||||
let search = '';
|
||||
|
||||
let options = Object.entries(providers).map(([type, option]) => {
|
||||
return {
|
||||
label: option.name,
|
||||
icon: option.icon,
|
||||
callback() {
|
||||
if (
|
||||
type !== MessagingProviderType.Email &&
|
||||
type !== MessagingProviderType.Sms &&
|
||||
type !== MessagingProviderType.Push
|
||||
)
|
||||
return;
|
||||
$providerType = type;
|
||||
$topicsById = {};
|
||||
$targetsById = {};
|
||||
const common = {
|
||||
topics: [],
|
||||
users: [],
|
||||
targets: []
|
||||
};
|
||||
switch (type) {
|
||||
case MessagingProviderType.Email:
|
||||
$messageParams[$providerType] = {
|
||||
...common,
|
||||
subject: '',
|
||||
content: ''
|
||||
};
|
||||
break;
|
||||
case MessagingProviderType.Sms:
|
||||
$messageParams[$providerType] = {
|
||||
...common,
|
||||
content: ''
|
||||
};
|
||||
break;
|
||||
case MessagingProviderType.Push:
|
||||
$messageParams[$providerType] = {
|
||||
...common,
|
||||
title: '',
|
||||
body: '',
|
||||
data: [['', '']]
|
||||
};
|
||||
break;
|
||||
}
|
||||
wizard.start(Create);
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
$: filteredOptions = options.filter((option) => {
|
||||
return option.label.toLowerCase().includes(search.toLowerCase());
|
||||
});
|
||||
</script>
|
||||
|
||||
<Template options={filteredOptions} bind:search>
|
||||
<div class="u-flex u-cross-center u-gap-8" slot="option" let:option>
|
||||
<i class="icon-{option.icon}" />
|
||||
<span>{option.label}</span>
|
||||
</div>
|
||||
</Template>
|
||||
@@ -78,3 +78,9 @@ export const FilesPanel: SubPanel = {
|
||||
name: 'Files',
|
||||
component: Files
|
||||
};
|
||||
|
||||
import CreateMessage from './createMessage.svelte';
|
||||
export const CreateMessagePanel: SubPanel = {
|
||||
name: 'Create Message',
|
||||
component: CreateMessage
|
||||
};
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
This is the root command panel. It precedes all other command panels.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { base } from '$app/paths';
|
||||
import { app } from '$lib/stores/app';
|
||||
import { debounce } from '$lib/helpers/debounce';
|
||||
import { isMac } from '$lib/helpers/platform';
|
||||
import { commands, searchers, type Command, isKeyedCommand } from '../commands';
|
||||
@@ -54,7 +56,13 @@
|
||||
<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">
|
||||
<i class="icon-{command.icon ?? 'arrow-sm-right'}" />
|
||||
{#if command.image}
|
||||
<img
|
||||
src={`${base}/icons/${$app.themeInUse}/color/${command.image}.svg`}
|
||||
alt={command.label} />
|
||||
{:else}
|
||||
<i class="icon-{command.icon ?? 'arrow-sm-right'}" />
|
||||
{/if}
|
||||
<span>
|
||||
{command.label}
|
||||
</span>
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
import { goto } from '$app/navigation';
|
||||
import { project } from '$routes/console/project-[project]/store';
|
||||
import { get } from 'svelte/store';
|
||||
import { type Searcher } from '../commands';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { MessagingProviderType } from '@appwrite.io/console';
|
||||
|
||||
const getLabel = (message) => {
|
||||
switch (message.providerType) {
|
||||
case MessagingProviderType.Push:
|
||||
return message.data.title;
|
||||
case MessagingProviderType.Sms:
|
||||
return message.data.content;
|
||||
case MessagingProviderType.Email:
|
||||
return message.data.subject;
|
||||
default:
|
||||
return 'null';
|
||||
}
|
||||
};
|
||||
|
||||
const getIcon = (message) => {
|
||||
switch (message.providerType) {
|
||||
case MessagingProviderType.Push:
|
||||
return 'device-mobile';
|
||||
case MessagingProviderType.Sms:
|
||||
return 'annotation';
|
||||
case MessagingProviderType.Email:
|
||||
return 'mail';
|
||||
default:
|
||||
return 'send';
|
||||
}
|
||||
};
|
||||
|
||||
export const messagesSearcher = (async (query: string) => {
|
||||
const { messages } = await sdk.forProject.messaging.listMessages([], query || undefined);
|
||||
|
||||
const projectId = get(project).$id;
|
||||
|
||||
return messages
|
||||
.filter((message) => getLabel(message).toLowerCase().includes(query.toLowerCase()))
|
||||
.map(
|
||||
(message) =>
|
||||
({
|
||||
group: 'messages',
|
||||
label: getLabel(message),
|
||||
callback: () => {
|
||||
goto(`/console/project-${projectId}/messaging/message-${message.$id}`);
|
||||
},
|
||||
icon: getIcon(message)
|
||||
}) as const
|
||||
);
|
||||
}) satisfies Searcher;
|
||||
@@ -0,0 +1,33 @@
|
||||
import { goto } from '$app/navigation';
|
||||
import { project } from '$routes/console/project-[project]/store';
|
||||
import { get } from 'svelte/store';
|
||||
import type { Searcher } from '../commands';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { getProviderDisplayNameAndIcon } from '$routes/console/project-[project]/messaging/provider.svelte';
|
||||
|
||||
const getIcon = (provider: string) => {
|
||||
const { icon } = getProviderDisplayNameAndIcon(provider);
|
||||
return icon;
|
||||
};
|
||||
|
||||
export const providersSearcher = (async (query: string) => {
|
||||
const { providers } = await sdk.forProject.messaging.listProviders([], query || undefined);
|
||||
|
||||
const projectId = get(project).$id;
|
||||
|
||||
return providers
|
||||
.filter((provider) => provider.name.toLowerCase().includes(query.toLowerCase()))
|
||||
.map(
|
||||
(provider) =>
|
||||
({
|
||||
group: 'providers',
|
||||
label: provider.name,
|
||||
callback: () => {
|
||||
goto(
|
||||
`/console/project-${projectId}/messaging/providers/provider-${provider.$id}`
|
||||
);
|
||||
},
|
||||
image: getIcon(provider.provider)
|
||||
}) as const
|
||||
);
|
||||
}) satisfies Searcher;
|
||||
@@ -0,0 +1,25 @@
|
||||
import { goto } from '$app/navigation';
|
||||
import { project } from '$routes/console/project-[project]/store';
|
||||
import { get } from 'svelte/store';
|
||||
import type { Searcher } from '../commands';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
|
||||
export const topicsSearcher = (async (query: string) => {
|
||||
const { topics } = await sdk.forProject.messaging.listTopics([], query || undefined);
|
||||
|
||||
const projectId = get(project).$id;
|
||||
|
||||
return topics
|
||||
.filter((topic) => topic.name.toLowerCase().includes(query.toLowerCase()))
|
||||
.map(
|
||||
(topic) =>
|
||||
({
|
||||
group: 'topics',
|
||||
label: topic.name,
|
||||
callback: () => {
|
||||
goto(`/console/project-${projectId}/messaging/topics/topic-${topic.$id}`);
|
||||
},
|
||||
icon: 'send'
|
||||
}) as const
|
||||
);
|
||||
}) satisfies Searcher;
|
||||
@@ -0,0 +1,25 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/stores';
|
||||
import { BillingPlan } from '$lib/constants';
|
||||
import { Button } from '$lib/elements/forms';
|
||||
import { HeaderAlert } from '$lib/layout';
|
||||
import { orgMissingPaymentMethod } from '$routes/console/store';
|
||||
</script>
|
||||
|
||||
{#if ($orgMissingPaymentMethod.billingPlan === BillingPlan.PRO || $orgMissingPaymentMethod.billingPlan === BillingPlan.SCALE) && !$orgMissingPaymentMethod.paymentMethodId && !$orgMissingPaymentMethod.backupPaymentMethodId && !$page.url.pathname.includes('/console/account')}
|
||||
<HeaderAlert
|
||||
type="error"
|
||||
title={`Payment method required for ${$orgMissingPaymentMethod.name}`}>
|
||||
<svelte:fragment>
|
||||
Add a payment method to {$orgMissingPaymentMethod.name} to avoid service interruptions to
|
||||
your projects.
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="buttons">
|
||||
<Button
|
||||
secondary
|
||||
href={`/console/organization-${$orgMissingPaymentMethod.$id}/billing`}>
|
||||
Add payment method
|
||||
</Button>
|
||||
</svelte:fragment>
|
||||
</HeaderAlert>
|
||||
{/if}
|
||||
@@ -0,0 +1,26 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/stores';
|
||||
import { Button } from '$lib/elements/forms';
|
||||
import { HeaderAlert } from '$lib/layout';
|
||||
import { paymentMissingMandate } from '$lib/stores/billing';
|
||||
import { organization } from '$lib/stores/organization';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { confirmSetup } from '$lib/stores/stripe';
|
||||
|
||||
async function verifyPaymentMethod() {
|
||||
const method = await sdk.forConsole.billing.setupPaymentMandate(
|
||||
$organization.$id,
|
||||
$paymentMissingMandate.$id
|
||||
);
|
||||
await confirmSetup(method.clientSecret, method.$id);
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if $paymentMissingMandate && $paymentMissingMandate.country === 'in' && $paymentMissingMandate.mandateId === null && !$page.url.pathname.includes('/console/account')}
|
||||
<HeaderAlert title="Authorization required" type="info">
|
||||
The payment method for {$organization.name} needs to be verified.
|
||||
<svelte:fragment slot="buttons">
|
||||
<Button secondary on:click={verifyPaymentMethod}>Verify payment method</Button>
|
||||
</svelte:fragment>
|
||||
</HeaderAlert>
|
||||
{/if}
|
||||
@@ -1,13 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { Button } from '$lib/elements/forms';
|
||||
import { HeaderAlert } from '$lib/layout';
|
||||
</script>
|
||||
|
||||
<HeaderAlert title="You are limited to one free organization per account">
|
||||
All but one organization will be automatically upgraded to a Pro plan on <b>31 January 2024</b>.
|
||||
You can add a payment method or transfer projects from your settings.
|
||||
|
||||
<svelte:fragment slot="buttons">
|
||||
<Button secondary href="/console/account/organizations">View organizations</Button>
|
||||
</svelte:fragment>
|
||||
</HeaderAlert>
|
||||
@@ -1,5 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { Button, FormList, 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 { createEventDispatcher } from 'svelte';
|
||||
@@ -55,7 +56,7 @@
|
||||
<div>
|
||||
<span class="icon-exclamation-circle u-color-text-danger" />
|
||||
<span>
|
||||
{couponData.code} is not a valid promo code
|
||||
{couponData.code.toUpperCase()} is not a valid promo code
|
||||
</span>
|
||||
</div>
|
||||
{:else if couponData?.status === 'active'}
|
||||
@@ -64,11 +65,13 @@
|
||||
<span class="icon-tag u-color-text-success" />
|
||||
<slot data={couponData}>
|
||||
<span>
|
||||
{couponData.code} applied (-${couponData.credits})
|
||||
{couponData.code.toUpperCase()} applied (-{formatCurrency(
|
||||
couponData.credits
|
||||
)})
|
||||
</span>
|
||||
</slot>
|
||||
</div>
|
||||
<Button text on:click={removeCoupon}>Remove</Button>
|
||||
<Button round text on:click={removeCoupon}><span class="icon-x"></span></Button>
|
||||
</div>
|
||||
{/if}
|
||||
</FormList>
|
||||
|
||||
@@ -1,12 +1,18 @@
|
||||
<script lang="ts">
|
||||
import { FormList, InputText } from '$lib/elements/forms';
|
||||
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';
|
||||
|
||||
export let methods: Record<string, unknown>[];
|
||||
export let group: string;
|
||||
export let name: string;
|
||||
export let defaultMethod: string = null;
|
||||
export let backupMethod: string = null;
|
||||
export let disabledCondition: string = null;
|
||||
export let setAsDefault = false;
|
||||
export let showSetAsDefault = false;
|
||||
|
||||
let element: HTMLDivElement;
|
||||
let loader: HTMLDivElement;
|
||||
@@ -40,15 +46,43 @@
|
||||
$: if (element) {
|
||||
observer.observe(element, { childList: true });
|
||||
}
|
||||
|
||||
//Set setAsDefault as false when group changes
|
||||
$: if (group || group === null) {
|
||||
setAsDefault = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
<RadioBoxes elements={methods} total={methods?.length} variableName="$id" name="payment" bind:group>
|
||||
<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-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 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>
|
||||
{/if}
|
||||
</span>
|
||||
</slot>
|
||||
</svelte:fragment>
|
||||
@@ -73,6 +107,14 @@
|
||||
<!-- Stripe will create form elements here -->
|
||||
</div>
|
||||
</div>
|
||||
{#if showSetAsDefault}
|
||||
<ul>
|
||||
<InputChoice
|
||||
bind:value={setAsDefault}
|
||||
id="default"
|
||||
label="Set as default payment method for this organization" />
|
||||
</ul>
|
||||
{/if}
|
||||
</FormList>
|
||||
</RadioBoxes>
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
export let danger = false;
|
||||
export let hideOverflow = false;
|
||||
export let hideFooter = false;
|
||||
</script>
|
||||
|
||||
<Card {danger}>
|
||||
@@ -14,7 +15,7 @@
|
||||
<slot name="aside" />
|
||||
</div>
|
||||
</div>
|
||||
{#if $$slots.actions}
|
||||
{#if $$slots.actions && !hideFooter}
|
||||
<div class="common-section card-separator u-flex u-main-end">
|
||||
<slot name="actions" />
|
||||
</div>
|
||||
|
||||
@@ -1,18 +1,39 @@
|
||||
<script lang="ts">
|
||||
export let href: string;
|
||||
export let href: string = null;
|
||||
export let external: boolean = false;
|
||||
</script>
|
||||
|
||||
<li class="clickable-list-item">
|
||||
<a {href} class="clickable-list-button" on:click>
|
||||
{#if $$slots.default}
|
||||
<h5 class="clickable-list-title u-trim-1">
|
||||
<slot />
|
||||
</h5>
|
||||
{/if}
|
||||
{#if $$slots.desc}
|
||||
<div class="clickable-list-desc">
|
||||
<p class="text u-margin-block-start-8"><slot name="desc" /></p>
|
||||
</div>
|
||||
{/if}
|
||||
</a>
|
||||
{#if href}
|
||||
<a
|
||||
{href}
|
||||
class="clickable-list-button"
|
||||
target={external ? '_blank' : ''}
|
||||
rel={external ? 'noopener noreferrer' : ''}
|
||||
on:click>
|
||||
{#if $$slots.default}
|
||||
<h5 class="clickable-list-title u-trim-1">
|
||||
<slot />
|
||||
</h5>
|
||||
{/if}
|
||||
{#if $$slots.desc}
|
||||
<div class="clickable-list-desc">
|
||||
<p class="text u-margin-block-start-8"><slot name="desc" /></p>
|
||||
</div>
|
||||
{/if}
|
||||
</a>
|
||||
{:else}
|
||||
<button class="clickable-list-button u-width-full-line" on:click>
|
||||
{#if $$slots.default}
|
||||
<h5 class="clickable-list-title u-trim-1">
|
||||
<slot />
|
||||
</h5>
|
||||
{/if}
|
||||
{#if $$slots.desc}
|
||||
<div class="clickable-list-desc">
|
||||
<p class="text u-margin-block-start-8"><slot name="desc" /></p>
|
||||
</div>
|
||||
{/if}
|
||||
</button>
|
||||
{/if}
|
||||
</li>
|
||||
|
||||
@@ -3,10 +3,11 @@
|
||||
|
||||
export let withIndentation = false;
|
||||
export let open = false;
|
||||
export let disabled = false;
|
||||
</script>
|
||||
|
||||
<li class="collapsible-item">
|
||||
<details class="collapsible-wrapper" {open}>
|
||||
<details class="collapsible-wrapper" class:is-disabled={disabled} bind:open>
|
||||
<!-- svelte-ignore a11y-no-redundant-roles -->
|
||||
<summary
|
||||
class="collapsible-button u-position-relative"
|
||||
@@ -16,13 +17,18 @@
|
||||
tabindex="0">
|
||||
<slot name="beforetitle" />
|
||||
<div>
|
||||
<span class="text"><slot name="title" /></span>
|
||||
<span class="text" class:u-color-text-disabled={disabled}
|
||||
><slot name="title" /></span>
|
||||
{#if $$slots.subtitle}
|
||||
<span class="collapsible-button-optional"><slot name="subtitle" /></span>
|
||||
<span class="collapsible-button-optional" class:u-color-text-disabled={disabled}
|
||||
><slot name="subtitle" /></span>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="icon">
|
||||
<span class="icon-cheveron-down" aria-hidden="true" />
|
||||
<span
|
||||
class="icon-cheveron-down u-font-size-20"
|
||||
class:u-color-text-disabled={disabled}
|
||||
aria-hidden="true" />
|
||||
</div>
|
||||
</summary>
|
||||
<div
|
||||
@@ -33,3 +39,24 @@
|
||||
</div>
|
||||
</details>
|
||||
</li>
|
||||
|
||||
<style lang="scss">
|
||||
// TODO: remove once pink is updated
|
||||
.collapsible-item {
|
||||
.collapsible-wrapper.is-disabled {
|
||||
&[open] .icon-cheveron-down {
|
||||
rotate: unset;
|
||||
}
|
||||
|
||||
cursor: not-allowed;
|
||||
* {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.collapsible {
|
||||
&-content {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,149 @@
|
||||
<script lang="ts" context="module">
|
||||
type Consent = {
|
||||
key: string;
|
||||
accepted: Record<string, boolean>;
|
||||
};
|
||||
export const settings = writable<boolean>(false);
|
||||
export const show = writable<boolean>(false);
|
||||
export const consent = writable<Consent>(
|
||||
JSON.parse(globalThis?.localStorage?.getItem('consent') ?? null)
|
||||
);
|
||||
consent.subscribe((value) => {
|
||||
if (browser) {
|
||||
globalThis.localStorage.setItem('consent', JSON.stringify(value));
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher, onMount } from 'svelte';
|
||||
import { Modal } from '.';
|
||||
import { Button } from '$lib/elements/forms';
|
||||
import { writable } from 'svelte/store';
|
||||
import { browser } from '$app/environment';
|
||||
|
||||
const key = new Date('2023-11-07');
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
let selected = {};
|
||||
|
||||
$: if ($settings) {
|
||||
selected = $consent?.accepted ?? {};
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
if ($consent) {
|
||||
const date = new Date($consent.key);
|
||||
if (key > date) {
|
||||
show.set(true);
|
||||
}
|
||||
} else {
|
||||
show.set(true);
|
||||
}
|
||||
});
|
||||
|
||||
function saveSettings(obj: Consent) {
|
||||
consent.set(obj);
|
||||
}
|
||||
|
||||
function confirmChoices(choices: Consent['accepted']) {
|
||||
const consent = {
|
||||
key: key.toISOString(),
|
||||
accepted: choices
|
||||
};
|
||||
saveSettings(consent);
|
||||
dispatch('confirm', consent);
|
||||
show.set(false);
|
||||
settings.set(false);
|
||||
}
|
||||
|
||||
function acceptAll() {
|
||||
confirmChoices({
|
||||
analytics: true
|
||||
});
|
||||
}
|
||||
|
||||
function rejectAll() {
|
||||
confirmChoices({});
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if $show}
|
||||
<div class="card is-consent">
|
||||
<p>
|
||||
By clicking "Accept all", you agree to the storing of cookies on your device to analyze
|
||||
site usage.
|
||||
</p>
|
||||
|
||||
<div
|
||||
class="is-consent-buttons u-flex u-margin-block-start-16 u-main-space-between u-cross-center">
|
||||
<Button class="u-padding-inline-0" text on:click={() => settings.set(true)}>
|
||||
Cookie settings
|
||||
</Button>
|
||||
<div class="u-flex u-gap-16">
|
||||
<Button secondary on:click={rejectAll}>Only required</Button>
|
||||
<Button secondary on:click={acceptAll}>Accept all</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<Modal bind:show={$settings} title="Cookie Preferences">
|
||||
<p>
|
||||
We use cookies to improve your site experience. The "strictly necessary" cookies are
|
||||
required for Appwrite to function.
|
||||
</p>
|
||||
<div class="u-flex-vertical u-gap-24 u-width-full-line" style:margin-block-end="24px">
|
||||
<div class="u-flex u-gap-8">
|
||||
<input type="checkbox" checked disabled />
|
||||
<div>
|
||||
<span class="text u-bold">Strictly necessary cookies</span>
|
||||
<p class="text u-margin-block-start-8">
|
||||
These are the cookies required for Appwrite to function.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="u-flex u-gap-8">
|
||||
<input id="analytics" type="checkbox" bind:checked={selected['analytics']} />
|
||||
<div>
|
||||
<label for="analytics" class="text u-bold">Product analytics</label>
|
||||
<span class="">(optional)</span>
|
||||
<p class="text u-margin-block-start-8">
|
||||
We include analytics cookies to understand how you use our product and design
|
||||
better experiences.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<svelte:fragment slot="footer">
|
||||
<Button text external href="https://appwrite.io/privacy">Privacy Policy</Button>
|
||||
<Button on:click={() => confirmChoices(selected)}>Save preferences</Button>
|
||||
</svelte:fragment>
|
||||
</Modal>
|
||||
|
||||
<style lang="scss">
|
||||
@import '@appwrite.io/pink/src/abstract/variables/_devices.scss';
|
||||
|
||||
.card {
|
||||
position: fixed;
|
||||
padding: 1.5rem;
|
||||
bottom: 1rem;
|
||||
right: 1rem;
|
||||
z-index: 100;
|
||||
max-width: 600px;
|
||||
}
|
||||
|
||||
@media #{$break1} {
|
||||
.card {
|
||||
bottom: 0.5rem;
|
||||
left: 0.5rem;
|
||||
right: 0.5rem;
|
||||
max-width: 100%;
|
||||
|
||||
.is-consent-buttons {
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,12 +1,14 @@
|
||||
<script lang="ts">
|
||||
import { isValueOfStringEnum } from '$lib/helpers/types';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { CreditCard } from '@appwrite.io/console';
|
||||
|
||||
export let brand: string;
|
||||
export let width = 23;
|
||||
export let height = 16;
|
||||
|
||||
function getCreditCardImage(brand: string, width = 46, height = 32) {
|
||||
if (!brand) return '';
|
||||
if (!isValueOfStringEnum(CreditCard, brand)) return '';
|
||||
return sdk.forConsole.avatars.getCreditCard(brand, width, height).toString();
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -12,7 +12,11 @@
|
||||
export let childStart = false;
|
||||
export let noStyle = false;
|
||||
export let fullWidth = false;
|
||||
export let wrapperFullWidth = false;
|
||||
export let fixed = false;
|
||||
export let display = 'block';
|
||||
export let arrowSize = 10;
|
||||
export let isPopover = false;
|
||||
|
||||
const dispatch = createEventDispatcher<{
|
||||
blur: undefined;
|
||||
@@ -31,13 +35,14 @@
|
||||
{
|
||||
name: 'arrow',
|
||||
options: {
|
||||
element: arrow
|
||||
element: arrow,
|
||||
padding: arrowSize * 1.75
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'offset',
|
||||
options: {
|
||||
offset: [0, noArrow ? 0 : 6]
|
||||
offset: [noArrow ? 0 : -arrowSize, noArrow ? 0 : arrowSize / 1.5]
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -100,16 +105,26 @@
|
||||
|
||||
<svelte:window on:click={onBlur} on:keydown={onKeyDown} />
|
||||
|
||||
<div class:drop-wrapper={!noStyle} class:u-cross-child-start={childStart} bind:this={element}>
|
||||
<div
|
||||
class:drop-wrapper={!noStyle}
|
||||
class:u-cross-child-start={childStart}
|
||||
class:u-width-full-line={wrapperFullWidth}
|
||||
bind:this={element}
|
||||
style:display>
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="drop-tooltip"
|
||||
class:u-width-full-line={fullWidth}
|
||||
bind:this={tooltip}
|
||||
class:u-width-full-line={fullWidth}
|
||||
style:--arrow-size={`${arrowSize}px`}
|
||||
style:z-index="10">
|
||||
<div class="drop-arrow" class:u-hide={!show || (show && noArrow)} bind:this={arrow} />
|
||||
<div
|
||||
class="drop-arrow"
|
||||
class:is-popover={isPopover}
|
||||
class:u-hide={!show || (show && noArrow)}
|
||||
bind:this={arrow} />
|
||||
{#if show}
|
||||
<slot name="list" />
|
||||
{/if}
|
||||
@@ -117,36 +132,50 @@
|
||||
|
||||
<!-- svelte-ignore css-unused-selector -->
|
||||
<style global lang="scss">
|
||||
.drop-tooltip[data-popper-placement^='top'] > .drop-arrow {
|
||||
bottom: -4px;
|
||||
}
|
||||
|
||||
.drop-tooltip[data-popper-placement^='bottom'] > .drop-arrow {
|
||||
top: -4px;
|
||||
}
|
||||
|
||||
.drop-tooltip[data-popper-placement^='left'] > .drop-arrow {
|
||||
right: -4px;
|
||||
}
|
||||
|
||||
.drop-tooltip[data-popper-placement^='right'] > .drop-arrow {
|
||||
left: -4px;
|
||||
.drop-arrow.is-popover {
|
||||
--drop-arrow-pop-over-bg-color: var(--color-neutral-90);
|
||||
body.theme-light & {
|
||||
--drop-arrow-pop-over-bg-color: var(--color-neutral-0);
|
||||
}
|
||||
}
|
||||
.drop-arrow,
|
||||
.drop-arrow::before {
|
||||
position: absolute;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
z-index: -1;
|
||||
width: var(--arrow-size);
|
||||
height: var(--arrow-size);
|
||||
z-index: 1;
|
||||
|
||||
--drop-arrow-border: 1px solid hsl(var(--color-neutral-85));
|
||||
--drop-arrow-bg-color: hsl(var(--drop-arrow-pop-over-bg-color, var(--color-neutral-105)));
|
||||
body.theme-light & {
|
||||
--drop-arrow-border: 1px solid hsl(var(--color-neutral-10));
|
||||
--drop-arrow-bg-color: hsl(var(--drop-arrow-pop-over-bg-color, var(--color-neutral-0)));
|
||||
}
|
||||
}
|
||||
|
||||
.drop-arrow::before {
|
||||
content: '';
|
||||
transform: rotate(45deg);
|
||||
background: hsl(var(--color-neutral-85));
|
||||
background: var(--drop-arrow-bg-color);
|
||||
}
|
||||
|
||||
body.theme-light & {
|
||||
background: hsl(var(--color-neutral-10));
|
||||
.drop-tooltip[data-popper-placement^='top'] > .drop-arrow {
|
||||
bottom: calc(var(--arrow-size) / -2);
|
||||
|
||||
&::before {
|
||||
border-bottom: var(--drop-arrow-border);
|
||||
border-right: var(--drop-arrow-border);
|
||||
border-bottom-right-radius: 25%;
|
||||
}
|
||||
}
|
||||
|
||||
.drop-tooltip[data-popper-placement^='bottom'] > .drop-arrow {
|
||||
top: calc(var(--arrow-size) / -2);
|
||||
|
||||
&::before {
|
||||
border-top: var(--drop-arrow-border);
|
||||
border-left: var(--drop-arrow-border);
|
||||
border-top-left-radius: 25%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -11,10 +11,20 @@
|
||||
export let noStyle = false;
|
||||
export let width: string = null;
|
||||
export let fullWidth = false;
|
||||
export let wrapperFullWidth = false;
|
||||
export let position: 'relative' | 'static' = 'relative';
|
||||
</script>
|
||||
|
||||
<Drop bind:show {placement} {childStart} {noArrow} {noStyle} {fullWidth} {fixed} on:blur>
|
||||
<Drop
|
||||
bind:show
|
||||
{placement}
|
||||
{childStart}
|
||||
{noArrow}
|
||||
{noStyle}
|
||||
{fullWidth}
|
||||
{wrapperFullWidth}
|
||||
{fixed}
|
||||
on:blur>
|
||||
<slot />
|
||||
<svelte:fragment slot="list">
|
||||
<div
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
{#if single}
|
||||
<article class="card u-grid u-cross-center u-width-full-line common-section">
|
||||
<div
|
||||
class="u-flex u-flex-vertical u-cross-center u-gap-24 u-width-full-line u-overflow-hidden">
|
||||
class="u-flex u-flex-vertical u-cross-center u-gap-24 u-width-full-line u-overflow-hidden u-padding-block-8">
|
||||
{#if !noMedia}
|
||||
<button
|
||||
type="button"
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
<script lang="ts">
|
||||
import { Button } from '$lib/elements/forms';
|
||||
import { EmptySearch } from '.';
|
||||
import { queries } from './filters';
|
||||
|
||||
export let resource;
|
||||
</script>
|
||||
|
||||
<EmptySearch hidePages>
|
||||
<div class="common-section">
|
||||
<div class="u-text-center common-section">
|
||||
<b class="body-text-2 u-bold">Sorry, we couldn't find any {resource}.</b>
|
||||
<p>There are no {resource} that match your filters.</p>
|
||||
</div>
|
||||
<div class="u-flex common-section u-main-center">
|
||||
<Button
|
||||
secondary
|
||||
on:click={() => {
|
||||
queries.clearAll();
|
||||
queries.apply();
|
||||
}}>
|
||||
Clear filters
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</EmptySearch>
|
||||
@@ -332,7 +332,7 @@
|
||||
</div>
|
||||
{:else}
|
||||
<div class="input-text-wrapper" style="--amount-of-buttons:2" bind:this={copyParent}>
|
||||
<!--
|
||||
<!--
|
||||
This object syntax avoids TS erroring because 'type' isn't a valid HTMLDivElement attribute
|
||||
(we need to set it to 'text' to add styling)
|
||||
-->
|
||||
|
||||
@@ -110,17 +110,19 @@
|
||||
</div>
|
||||
</header>
|
||||
<div class="modal-content">
|
||||
{#if error}
|
||||
<Alert
|
||||
dismissible
|
||||
type="warning"
|
||||
on:dismiss={() => {
|
||||
error = null;
|
||||
}}>
|
||||
{error}
|
||||
</Alert>
|
||||
{/if}
|
||||
<slot />
|
||||
<div class="modal-content-spacer u-flex-vertical u-gap-24 u-width-full-line">
|
||||
{#if error}
|
||||
<Alert
|
||||
dismissible
|
||||
type="warning"
|
||||
on:dismiss={() => {
|
||||
error = null;
|
||||
}}>
|
||||
{error}
|
||||
</Alert>
|
||||
{/if}
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if $$slots.footer}
|
||||
|
||||
@@ -0,0 +1,544 @@
|
||||
<script lang="ts">
|
||||
import { Id, ModalWrapper, Trim } from '.';
|
||||
import { Button, Form } from '$lib/elements/forms';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { ID, Query, Permission, Role } from '@appwrite.io/console';
|
||||
import type { Models } from '@appwrite.io/console';
|
||||
import { calculateSize } from '$lib/helpers/sizeConvertion';
|
||||
import { toLocaleDate } from '$lib/helpers/date';
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableRowButton,
|
||||
TableHeader,
|
||||
TableCell,
|
||||
TableCellText,
|
||||
TableCellHead
|
||||
} from '$lib/elements/table';
|
||||
import InputSearch from '$lib/elements/forms/inputSearch.svelte';
|
||||
import InputSelect from '$lib/elements/forms/inputSelect.svelte';
|
||||
import FormList from '$lib/elements/forms/formList.svelte';
|
||||
import { writable } from 'svelte/store';
|
||||
import { onMount } from 'svelte';
|
||||
import Heading from './heading.svelte';
|
||||
import { clickOnEnter } from '$lib/helpers/a11y';
|
||||
import Empty from './empty.svelte';
|
||||
import { base } from '$app/paths';
|
||||
import { page } from '$app/stores';
|
||||
|
||||
export let show: boolean;
|
||||
export let mimeTypeQuery: string = 'image/';
|
||||
export let selectedBucket: string = null;
|
||||
export let selectedFile: string = null;
|
||||
export let onSelect: (e: Models.File) => void;
|
||||
|
||||
let search = writable('');
|
||||
let searchEnabled = false;
|
||||
let fileSelector: HTMLInputElement;
|
||||
let uploading = false;
|
||||
let view: 'grid' | 'list' = 'list';
|
||||
|
||||
onMount(() => {
|
||||
selectedBucket = currentBucket?.$id;
|
||||
});
|
||||
|
||||
function submitForm() {
|
||||
onSelect(currentFile);
|
||||
closeModal();
|
||||
}
|
||||
|
||||
function getPreview(bucketId: string, fileId: string, size: number = 64) {
|
||||
return (
|
||||
sdk.forProject.storage.getFilePreview(bucketId, fileId, size, size).toString() +
|
||||
'&mode=admin'
|
||||
);
|
||||
}
|
||||
|
||||
async function uploadFile() {
|
||||
try {
|
||||
uploading = true;
|
||||
const file = await sdk.forProject.storage.createFile(
|
||||
selectedBucket,
|
||||
ID.unique(),
|
||||
fileSelector.files[0],
|
||||
[Permission.read(Role.any())]
|
||||
);
|
||||
search.set($search === null ? '' : null);
|
||||
selectFile(file);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
} finally {
|
||||
uploading = false;
|
||||
}
|
||||
}
|
||||
|
||||
function selectBucket(bucket: Models.Bucket | null) {
|
||||
currentBucket = bucket;
|
||||
selectedBucket = bucket?.$id ?? null;
|
||||
resetFile();
|
||||
}
|
||||
|
||||
function selectFile(file: Models.File) {
|
||||
currentFile = file;
|
||||
selectedBucket = currentFile.bucketId;
|
||||
selectedFile = currentFile.$id;
|
||||
}
|
||||
|
||||
function resetFile() {
|
||||
selectedFile = null;
|
||||
}
|
||||
|
||||
function resetBucket() {
|
||||
selectedBucket = null;
|
||||
}
|
||||
|
||||
function closeModal() {
|
||||
show = false;
|
||||
resetFile();
|
||||
resetBucket();
|
||||
}
|
||||
|
||||
function handleVisibilityChange() {
|
||||
if (!document.hidden) {
|
||||
buckets = loadBuckets();
|
||||
}
|
||||
}
|
||||
|
||||
let currentBucket: Models.Bucket = null;
|
||||
let currentFile: Models.File = null;
|
||||
let buckets: Promise<Models.BucketList> = loadBuckets();
|
||||
async function loadBuckets() {
|
||||
const response = await sdk.forProject.storage.listBuckets();
|
||||
const bucket = response.buckets[0] ?? null;
|
||||
if (bucket) {
|
||||
currentBucket = bucket;
|
||||
selectedBucket = bucket.$id;
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
$: files =
|
||||
currentBucket &&
|
||||
sdk.forProject.storage
|
||||
.listFiles(
|
||||
currentBucket.$id,
|
||||
[Query.startsWith('mimeType', mimeTypeQuery), Query.orderDesc('$createdAt')],
|
||||
$search || undefined
|
||||
)
|
||||
.then((response) => {
|
||||
if ($search === '') {
|
||||
searchEnabled = response.total > 0;
|
||||
}
|
||||
|
||||
return response;
|
||||
});
|
||||
|
||||
$: if ($search) {
|
||||
resetFile();
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:document on:visibilitychange={handleVisibilityChange} />
|
||||
|
||||
<ModalWrapper bind:show size="huge">
|
||||
<Form isModal onSubmit={submitForm} class="u-stretch">
|
||||
<header class="modal-header u-margin-block-end-0">
|
||||
<div class="u-flex u-main-space-between u-cross-center u-gap-16">
|
||||
<h4 class="modal-title heading-level-5">Select file</h4>
|
||||
<button
|
||||
type="button"
|
||||
on:click={closeModal}
|
||||
class="button is-text is-small is-only-icon"
|
||||
aria-label="Close modal">
|
||||
<span class="icon-x" aria-hidden="true"></span>
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
<div
|
||||
class="modal-content u-stretch u-flex-vertical u-padding-0 u-margin-block-0 u-overflow-visible">
|
||||
<div class="u-flex u-min-height-0 u-stretch">
|
||||
<aside
|
||||
class="drop-section u-width-200 u-padding-16
|
||||
u-flex-vertical u-gap-8
|
||||
u-flex-shrink-0 u-margin-inline-0 u-overflow-y-auto is-not-mobile">
|
||||
<h6
|
||||
class="eyebrow-heading-3"
|
||||
style:--heading-text-color="var(--color-neutral-50)">
|
||||
Buckets
|
||||
</h6>
|
||||
<ul class="drop-list">
|
||||
{#await buckets}
|
||||
<div class="u-flex u-main-center">
|
||||
<div class="loader" />
|
||||
</div>
|
||||
{:then response}
|
||||
{#each response.buckets as bucket}
|
||||
{@const isSelected = bucket.$id === selectedBucket}
|
||||
<li class="drop-list-item">
|
||||
<button
|
||||
type="button"
|
||||
class="drop-button"
|
||||
class:is-selected={isSelected}
|
||||
on:click={() => selectBucket(bucket)}>
|
||||
<span>{bucket.name}</span>
|
||||
</button>
|
||||
</li>
|
||||
{:else}
|
||||
<li class="drop-list-item">
|
||||
<span class="drop-button">No buckets found</span>
|
||||
</li>
|
||||
{/each}
|
||||
{/await}
|
||||
</ul>
|
||||
</aside>
|
||||
<article
|
||||
style:padding-inline="calc(var(--p-modal-padding))"
|
||||
class="modal-content-main u-flex-vertical u-gap-24 u-sep-inline-start u-flex-basis-1000 u-padding-block-24 u-overflow-y-auto">
|
||||
<div class="is-only-mobile">
|
||||
{#await buckets}
|
||||
loading
|
||||
{:then response}
|
||||
{#if currentBucket?.$id}
|
||||
<FormList>
|
||||
<InputSelect
|
||||
wrapperTag="div"
|
||||
label="Buckets"
|
||||
options={response.buckets.map((n) => ({
|
||||
value: n.$id,
|
||||
label: n.name
|
||||
}))}
|
||||
bind:value={currentBucket.$id}
|
||||
id="buckets" />
|
||||
</FormList>
|
||||
{/if}
|
||||
{/await}
|
||||
</div>
|
||||
{#await buckets}
|
||||
<div class="u-flex-vertical u-stretch u-position-relative u-main-center">
|
||||
<div
|
||||
class="u-position-absolute u-width-full-line u-flex u-flex-vertical u-main-center u-cross-center u-gap-16 u-margin-block-start-32"
|
||||
style="inset-inline-start: 0;">
|
||||
<div class="loader" />
|
||||
<p class="text">Loading files...</p>
|
||||
</div>
|
||||
</div>
|
||||
{:then response}
|
||||
{#if response?.total}
|
||||
{#if currentBucket}
|
||||
<header class="u-flex-vertical u-gap-32">
|
||||
<div class="u-flex u-gap-16">
|
||||
<h5 class="heading-level-6 u-trim u-min-width-0">
|
||||
{currentBucket?.name}
|
||||
</h5>
|
||||
<Id value={currentBucket?.$id} event="bucket">
|
||||
{currentBucket?.$id}
|
||||
</Id>
|
||||
</div>
|
||||
<div
|
||||
class="u-flex u-main-space-between u-gap-16 u-flex-vertical-mobile">
|
||||
<InputSearch
|
||||
placeholder="Search files"
|
||||
bind:value={$search}
|
||||
disabled={!searchEnabled}
|
||||
style="min-inline-size: 17.5rem; block-size: 100%" />
|
||||
<div class="u-flex u-gap-16">
|
||||
<div class="toggle-button">
|
||||
<ul class="toggle-button-list">
|
||||
<li class="toggle-button-item">
|
||||
<button
|
||||
on:click={() => (view = 'list')}
|
||||
disabled={!searchEnabled}
|
||||
type="button"
|
||||
class="toggle-button-element"
|
||||
class:is-selected={view === 'list'}
|
||||
aria-label="List View">
|
||||
<span
|
||||
class="icon-view-list"
|
||||
aria-hidden="true" />
|
||||
</button>
|
||||
</li>
|
||||
<li class="toggle-button-item">
|
||||
<button
|
||||
on:click={() => (view = 'grid')}
|
||||
disabled={!searchEnabled}
|
||||
type="button"
|
||||
class="toggle-button-element"
|
||||
class:is-selected={view === 'grid'}
|
||||
aria-label="Grid View">
|
||||
<span
|
||||
class="icon-view-grid"
|
||||
aria-hidden="true" />
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
secondary
|
||||
class="is-full-width-in-stack-mobile u-height-100-percent"
|
||||
disabled={uploading}
|
||||
on:click={() => fileSelector.click()}>
|
||||
<input
|
||||
tabindex="-1"
|
||||
type="file"
|
||||
accept="image/*"
|
||||
class="u-hide"
|
||||
on:change={uploadFile}
|
||||
bind:this={fileSelector} />
|
||||
{#if uploading}
|
||||
<div class="loader is-small"></div>
|
||||
<span>Uploading</span>
|
||||
{:else}
|
||||
<span class="icon-upload" aria-hidden="true"
|
||||
></span>
|
||||
<span>Upload</span>
|
||||
{/if}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{#if files}
|
||||
{#await files}
|
||||
<div
|
||||
class="u-flex-vertical u-stretch u-position-relative u-main-center">
|
||||
<div
|
||||
class="u-position-absolute u-width-full-line u-flex u-flex-vertical u-main-center u-cross-center u-gap-16 u-margin-block-start-32"
|
||||
style="inset-inline-start: 0;">
|
||||
<div class="loader" />
|
||||
<p class="text">Loading files...</p>
|
||||
</div>
|
||||
</div>
|
||||
{:then response}
|
||||
<div class="u-flex-vertical u-stretch">
|
||||
{#if response?.files?.length}
|
||||
{#if view === 'grid'}
|
||||
<ul
|
||||
class="grid-box"
|
||||
style="--grid-gap:40px; --grid-item-size:120px; --grid-item-size-small-screens:100px;">
|
||||
{#each response?.files as file}
|
||||
<li>
|
||||
<div
|
||||
class="u-flex-vertical u-gap-8">
|
||||
<div
|
||||
role="button"
|
||||
style:background-size="cover"
|
||||
style:background-image={`url(${getPreview(
|
||||
currentBucket.$id,
|
||||
file.$id,
|
||||
360
|
||||
)})`}
|
||||
on:click={() =>
|
||||
selectFile(file)}
|
||||
on:keyup={clickOnEnter}
|
||||
tabindex="0"
|
||||
style:aspect-ratio="1/1"
|
||||
style:display="flex"
|
||||
style:align-items="flex-end"
|
||||
style:flex-direction="row-reverse"
|
||||
style:box-shadow="none"
|
||||
class="card u-height-100-percent u-gap-16"
|
||||
style="--card-padding:0.5rem;--card-padding-mobile:0.5rem; --card-border-radius:var(--border-radius-medium);">
|
||||
<input
|
||||
class="u-position-absolute is-small u-margin-block-start-2"
|
||||
type="radio"
|
||||
name="file"
|
||||
value={file.$id}
|
||||
style:pointer-events="none"
|
||||
checked={selectedFile ===
|
||||
file.$id} />
|
||||
</div>
|
||||
<span class="u-text-center"
|
||||
><Trim alternativeTrim
|
||||
>{file.name}</Trim
|
||||
></span>
|
||||
</div>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
{/if}
|
||||
{#if view === 'list'}
|
||||
<Table noMargin noStyles transparent dense>
|
||||
<TableHeader>
|
||||
<TableCellHead
|
||||
><span
|
||||
class="u-margin-inline-start-8"
|
||||
>Filename</span
|
||||
></TableCellHead>
|
||||
<TableCellHead width={140} onlyDesktop>
|
||||
ID
|
||||
</TableCellHead>
|
||||
<TableCellHead width={100} onlyDesktop>
|
||||
Type
|
||||
</TableCellHead>
|
||||
<TableCellHead width={100} onlyDesktop>
|
||||
Size
|
||||
</TableCellHead>
|
||||
<TableCellHead width={120} onlyDesktop>
|
||||
Created
|
||||
</TableCellHead>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{#each response?.files as file}
|
||||
<TableRowButton
|
||||
on:click={() =>
|
||||
selectFile(file)}>
|
||||
<TableCell title="Filename">
|
||||
<div
|
||||
class="u-inline-flex u-cross-center u-gap-12">
|
||||
<input
|
||||
type="radio"
|
||||
class="is-small u-margin-inline-start-8"
|
||||
name="file"
|
||||
value={file.$id}
|
||||
style:pointer-events="none"
|
||||
checked={selectedFile ===
|
||||
file.$id} />
|
||||
<span class="image">
|
||||
<img
|
||||
class="avatar"
|
||||
style:border-radius="var(--border-radius-xsmall)"
|
||||
width="28"
|
||||
height="28"
|
||||
src={getPreview(
|
||||
currentBucket.$id,
|
||||
file.$id
|
||||
)}
|
||||
alt={file.name} />
|
||||
</span>
|
||||
<Trim alternativeTrim>
|
||||
{file.name}
|
||||
</Trim>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCellText
|
||||
title="ID"
|
||||
onlyDesktop>
|
||||
<Id value={file.$id}
|
||||
>{file.$id}</Id>
|
||||
</TableCellText>
|
||||
<TableCellText
|
||||
title="Type"
|
||||
onlyDesktop>
|
||||
{file.mimeType}
|
||||
</TableCellText>
|
||||
<TableCellText
|
||||
title="Size"
|
||||
onlyDesktop>
|
||||
{calculateSize(
|
||||
file.sizeOriginal
|
||||
)}
|
||||
</TableCellText>
|
||||
<TableCellText
|
||||
title="Created"
|
||||
onlyDesktop>
|
||||
{toLocaleDate(
|
||||
file.$createdAt
|
||||
)}
|
||||
</TableCellText>
|
||||
</TableRowButton>
|
||||
{/each}
|
||||
</TableBody>
|
||||
</Table>
|
||||
{/if}
|
||||
{:else if $search}
|
||||
<article
|
||||
style:--card-bg-color="transparent"
|
||||
style:--shadow-small="none"
|
||||
style:--color-border="var(--color-neutral-15)"
|
||||
class="card u-grid u-cross-center u-width-full-line common-section is-border-dashed">
|
||||
<div
|
||||
class="u-flex u-flex-vertical u-cross-center u-gap-24 u-overflow-hidden">
|
||||
<div class="common-section">
|
||||
<div
|
||||
class="u-text-center common-section">
|
||||
<b class="body-text-2 u-bold"
|
||||
>Sorry we couldn't find "{$search}"</b>
|
||||
<p>
|
||||
There are no files that match
|
||||
your search.
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
class="u-flex u-gap-16 common-section u-main-center">
|
||||
<Button
|
||||
secondary
|
||||
on:click={() => ($search = '')}
|
||||
>Clear search</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
{:else}
|
||||
<Empty
|
||||
single
|
||||
noMedia
|
||||
--card-bg-color="transparent"
|
||||
--shadow-small="none"
|
||||
--color-border="var(--color-neutral-15)">
|
||||
<div class="common-section">
|
||||
<div class="u-text-center common-section">
|
||||
<Heading
|
||||
size="7"
|
||||
tag="h2"
|
||||
trimmed={false}>
|
||||
No files found within this bucket.
|
||||
</Heading>
|
||||
<p class="text u-line-height-1-5">
|
||||
Need a hand? Learn more in our <a
|
||||
class="link"
|
||||
href="https://appwrite.io/docs/products/storage"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer">
|
||||
documentation</a
|
||||
>.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Empty>
|
||||
{/if}
|
||||
</div>
|
||||
{/await}
|
||||
{/if}
|
||||
{/if}
|
||||
{:else}
|
||||
<Empty
|
||||
single
|
||||
noMedia
|
||||
--card-bg-color="transparent"
|
||||
--shadow-small="none"
|
||||
--color-border="var(--color-neutral-15)">
|
||||
<div class="u-text-center u-flex-vertical u-cross-center u-gap-24">
|
||||
<Heading size="7" tag="h2" trimmed={false}>
|
||||
No buckets found
|
||||
</Heading>
|
||||
<Button
|
||||
secondary
|
||||
external
|
||||
href={`${base}/console/project-${$page.params.project}/storage`}>
|
||||
Create bucket
|
||||
</Button>
|
||||
</div>
|
||||
</Empty>
|
||||
{/if}
|
||||
{/await}
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer u-margin-block-start-0">
|
||||
<div class="u-flex u-main-end u-gap-16">
|
||||
<Button text on:click={closeModal}>Cancel</Button>
|
||||
<Button submit disabled={selectedBucket === null || selectedFile === null}
|
||||
>Select</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Form>
|
||||
</ModalWrapper>
|
||||
|
||||
<style lang="scss">
|
||||
input[type='radio']:where(:indeterminate) {
|
||||
--p-bg-color: var(--p-bg-color-default);
|
||||
--p-border-color: var(--p-border-color-default);
|
||||
}
|
||||
</style>
|
||||
@@ -118,10 +118,12 @@
|
||||
<ul class="selects u-flex u-gap-8 u-margin-block-start-16">
|
||||
<InputSelect
|
||||
id="column"
|
||||
options={$columns.map((c) => ({
|
||||
label: c.title,
|
||||
value: c.id
|
||||
}))}
|
||||
options={$columns
|
||||
.filter((c) => c.filter !== false)
|
||||
.map((c) => ({
|
||||
label: c.title,
|
||||
value: c.id
|
||||
}))}
|
||||
placeholder="Select column"
|
||||
bind:value={columnId} />
|
||||
<InputSelect
|
||||
@@ -132,7 +134,7 @@
|
||||
bind:value={operatorKey} />
|
||||
</ul>
|
||||
{#if column && operator && !operator?.hideInput}
|
||||
<div class="u-margin-block-start-8">
|
||||
<ul class="u-margin-block-start-8">
|
||||
{#if column.type === 'integer' || column.type === 'double'}
|
||||
<InputNumber id="value" bind:value placeholder="Enter value" />
|
||||
{:else if column.type === 'boolean'}
|
||||
@@ -148,7 +150,7 @@
|
||||
{:else}
|
||||
<InputText id="value" bind:value placeholder="Enter value" />
|
||||
{/if}
|
||||
</div>
|
||||
</ul>
|
||||
{/if}
|
||||
<Button text disabled={isDisabled} class="u-margin-block-start-4" submit>
|
||||
<i class="icon-plus" />
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
|
||||
export let query = '[]';
|
||||
export let columns: Writable<Column[]>;
|
||||
export let fullWidthMobile = false;
|
||||
|
||||
const parsedQueries = queryParamToMap(query);
|
||||
queries.set(parsedQueries);
|
||||
@@ -55,7 +56,7 @@
|
||||
</div>
|
||||
|
||||
<div class="is-only-mobile">
|
||||
<Button secondary on:click={() => (showFiltersMobile = !showFiltersMobile)}>
|
||||
<Button secondary on:click={() => (showFiltersMobile = !showFiltersMobile)} {fullWidthMobile}>
|
||||
<i class="icon-filter u-opacity-50" />
|
||||
Filters
|
||||
{#if applied > 0}
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
export { default as filters } from './filters.svelte';
|
||||
export { default as Filters } from './filters.svelte';
|
||||
export { hasPageQueries, queryParamToMap, queries } from '$lib/components/filters/store';
|
||||
|
||||
@@ -47,7 +47,7 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<Copy {value} {event}>
|
||||
<Copy {value} {event} appendTo="parent">
|
||||
<div
|
||||
class="interactive-text-output is-buttons-on-top"
|
||||
class:u-text-center={centered}
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
<script lang="ts">
|
||||
import { app } from '$lib/stores/app';
|
||||
|
||||
export let darkSrc: string;
|
||||
export let lightSrc: string;
|
||||
export let alt: string;
|
||||
</script>
|
||||
|
||||
<a
|
||||
href={$app.themeInUse === 'dark' ? darkSrc : lightSrc}
|
||||
class="file-preview is-with-image"
|
||||
style="inline-size: 100%; block-size: 100%;"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
aria-label="open file in new window">
|
||||
<div class="file-preview-image">
|
||||
<img src={$app.themeInUse === 'dark' ? darkSrc : lightSrc} {alt} />
|
||||
</div>
|
||||
<div class="file-preview-content">
|
||||
<div class="avatar">
|
||||
<span class="icon-external-link" aria-hidden="true" />
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
@@ -15,6 +15,7 @@ export { default as UploadBox } from './uploadBox.svelte';
|
||||
export { default as List } from './list.svelte';
|
||||
export { default as ListItem } from './listItem.svelte';
|
||||
export { default as Empty } from './empty.svelte';
|
||||
export { default as EmptyFilter } from './emptyFilter.svelte';
|
||||
export { default as EmptySearch } from './emptySearch.svelte';
|
||||
export { default as Drop } from './drop.svelte';
|
||||
export { default as DropList } from './dropList.svelte';
|
||||
@@ -71,3 +72,4 @@ export { default as FakeModal } from './fakeModal.svelte';
|
||||
export { default as RadioBoxes } from './radioBoxes.svelte';
|
||||
export { default as ModalWrapper } from './modalWrapper.svelte';
|
||||
export { default as ModalSideCol } from './modalSideCol.svelte';
|
||||
export { default as ImagePreview } from './imagePreview.svelte';
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { tooltip } from '$lib/actions/tooltip';
|
||||
import { app } from '$lib/stores/app';
|
||||
import { base } from '$app/paths';
|
||||
|
||||
export let name: string;
|
||||
export let group: string;
|
||||
@@ -7,6 +9,7 @@
|
||||
export let disabled = false;
|
||||
export let padding = 1;
|
||||
export let icon: string = null;
|
||||
export let imageIcon: string = null;
|
||||
export let fullHeight = true;
|
||||
export let borderRadius: 'xsmall' | 'small' | 'medium' | 'large' = 'small';
|
||||
export let backgroundColor: string = null;
|
||||
@@ -57,6 +60,14 @@
|
||||
{#if icon}
|
||||
<span class={`icon-${icon} u-margin-inline-start-auto`} aria-hidden="true" />
|
||||
{/if}
|
||||
{#if imageIcon}
|
||||
<img
|
||||
class="u-margin-inline-start-auto"
|
||||
style:max-inline-size="1.25rem"
|
||||
style:max-block-size="1.25rem"
|
||||
src={`${base}/icons/${$app.themeInUse}/color/${imageIcon}.svg`}
|
||||
alt={imageIcon} />
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
</label>
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
import { disableCommands } from '$lib/commandCenter';
|
||||
|
||||
export let show = false;
|
||||
export let size: 'small' | 'big' = null;
|
||||
export let size: 'small' | 'big' | 'huge' = null;
|
||||
export let icon: string = null;
|
||||
export let state: 'success' | 'warning' | 'error' | 'info' = null;
|
||||
export let error: string = null;
|
||||
@@ -71,19 +71,21 @@
|
||||
</p>
|
||||
</header>
|
||||
<div class="modal-content">
|
||||
{#if error}
|
||||
<div bind:this={alert}>
|
||||
<Alert
|
||||
dismissible
|
||||
type="warning"
|
||||
on:dismiss={() => {
|
||||
error = null;
|
||||
}}>
|
||||
{error}
|
||||
</Alert>
|
||||
</div>
|
||||
{/if}
|
||||
<slot />
|
||||
<div class="modal-content-spacer u-flex-vertical u-gap-24 u-width-full-line">
|
||||
{#if error}
|
||||
<div bind:this={alert}>
|
||||
<Alert
|
||||
dismissible
|
||||
type="warning"
|
||||
on:dismiss={() => {
|
||||
error = null;
|
||||
}}>
|
||||
{error}
|
||||
</Alert>
|
||||
</div>
|
||||
{/if}
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if $$slots.footer}
|
||||
|
||||
@@ -41,8 +41,10 @@
|
||||
</slot>
|
||||
</p>
|
||||
</header>
|
||||
<div class="modal-content mk-content">
|
||||
<slot />
|
||||
<div class="modal-content">
|
||||
<div class="modal-content-spacer u-flex-vertical u-gap-24 u-width-full-line">
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
import { disableCommands } from '$lib/commandCenter';
|
||||
|
||||
export let show = false;
|
||||
export let size: 'small' | 'big' = null;
|
||||
export let size: 'small' | 'big' | 'huge' = null;
|
||||
export let closable = true;
|
||||
export let headerDivider = true;
|
||||
export let style = '';
|
||||
@@ -70,13 +70,30 @@
|
||||
|
||||
<dialog
|
||||
class="modal"
|
||||
class:u-hide={!show}
|
||||
class:is-small={size === 'small'}
|
||||
class:is-big={size === 'big'}
|
||||
class:is-huge={size === 'huge'}
|
||||
class:is-separate-header={headerDivider}
|
||||
{style}
|
||||
bind:this={dialog}
|
||||
on:cancel|preventDefault>
|
||||
on:cancel|preventDefault
|
||||
{style}>
|
||||
{#if show}
|
||||
<slot close={closeModal} />
|
||||
{/if}
|
||||
</dialog>
|
||||
|
||||
<style lang="scss">
|
||||
@import '@appwrite.io/pink/src/abstract/variables/_devices.scss';
|
||||
|
||||
.modal.is-huge {
|
||||
block-size: 100%;
|
||||
min-block-size: 80vh;
|
||||
|
||||
@media #{$break1}, #{$break2} {
|
||||
min-inline-size: 100%;
|
||||
min-block-size: 100%;
|
||||
border-radius: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -6,6 +6,8 @@
|
||||
import { AvatarInitials, EmptySearch, Modal, PaginationInline } from '..';
|
||||
import type { Writable } from 'svelte/store';
|
||||
import type { Permission } from './permissions.svelte';
|
||||
import Table from '$lib/elements/table/table.svelte';
|
||||
import { TableBody, TableCell, TableRow } from '$lib/elements/table';
|
||||
|
||||
export let show: boolean;
|
||||
export let groups: Writable<Map<string, Permission>>;
|
||||
@@ -77,13 +79,13 @@
|
||||
bind:value={search} />
|
||||
{#if results?.teams?.length}
|
||||
<div class="table-wrapper">
|
||||
<table class="table is-table-layout-auto is-remove-outer-styles">
|
||||
<tbody class="table-tbody">
|
||||
<Table noStyles isAutoLayout tag="table">
|
||||
<TableBody>
|
||||
{#each results.teams as team (team.$id)}
|
||||
{@const role = `team:${team.$id}`}
|
||||
{@const exists = $groups.has(role)}
|
||||
<tr class="table-row">
|
||||
<td class="table-col" data-title="Enabled" style="--p-col-width:40">
|
||||
<TableRow>
|
||||
<TableCell title="Enabled" width={40}>
|
||||
<input
|
||||
id={team.$id}
|
||||
type="checkbox"
|
||||
@@ -92,8 +94,8 @@
|
||||
checked={exists || selected.has(role)}
|
||||
disabled={exists}
|
||||
on:change={(event) => onSelection(event, role)} />
|
||||
</td>
|
||||
<td class="table-col" data-title="Team">
|
||||
</TableCell>
|
||||
<TableCell title="Team">
|
||||
<label class="u-flex u-cross-center u-gap-8" for={team.$id}>
|
||||
<AvatarInitials size={32} name={team.name} />
|
||||
<div class="u-line-height-1-5">
|
||||
@@ -101,11 +103,11 @@
|
||||
<div class="u-x-small">{team.$id}</div>
|
||||
</div>
|
||||
</label>
|
||||
</td>
|
||||
</tr>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
<div class="u-flex u-margin-block-start-32 u-main-space-between">
|
||||
<p class="text">Total results: {results?.total}</p>
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
{#if total}
|
||||
{#each elements as element}
|
||||
{@const value = element[variableName]?.toString()}
|
||||
<div class="box">
|
||||
<ul class="box" data-private>
|
||||
<InputRadio
|
||||
id={`${name}-${value}`}
|
||||
{value}
|
||||
@@ -22,11 +22,11 @@
|
||||
disabled={disabledCondition ? value === disabledCondition : false}>
|
||||
<slot name="element" {element} />
|
||||
</InputRadio>
|
||||
</div>
|
||||
</ul>
|
||||
{/each}
|
||||
{/if}
|
||||
|
||||
<div class="box">
|
||||
<ul class="box">
|
||||
{#if total}
|
||||
<InputRadio id="payment-method" value={null} {name} bind:group>
|
||||
<slot name="new">
|
||||
@@ -37,5 +37,5 @@
|
||||
{#if group === null}
|
||||
<slot />
|
||||
{/if}
|
||||
</div>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
export let disabled = false;
|
||||
export let autofocus = false;
|
||||
export let isWithEndButton = true;
|
||||
export let fullWidth = false;
|
||||
|
||||
let element: HTMLInputElement;
|
||||
let timer: ReturnType<typeof setTimeout>;
|
||||
@@ -58,7 +59,7 @@
|
||||
</script>
|
||||
|
||||
<div class="u-flex u-gap-12 common-section u-main-space-between">
|
||||
<div class="u-flex-basis-50-percent">
|
||||
<div class={fullWidth ? 'u-width-full-line' : 'u-flex-basis-50-percent'}>
|
||||
<div class="input-text-wrapper" class:is-with-end-button={isWithEndButton}>
|
||||
<input
|
||||
{placeholder}
|
||||
|
||||
@@ -11,6 +11,8 @@
|
||||
import { isCloud } from '$lib/system';
|
||||
import { organization } from '$lib/stores/organization';
|
||||
import { BillingPlan } from '$lib/constants';
|
||||
import ChangeOrganizationTierCloud from '$routes/console/changeOrganizationTierCloud.svelte';
|
||||
import { trackEvent } from '$lib/actions/analytics';
|
||||
|
||||
export let show = false;
|
||||
|
||||
@@ -43,7 +45,16 @@
|
||||
{/if}
|
||||
</div>
|
||||
{#if $organization?.billingPlan === BillingPlan.STARTER}
|
||||
<Button fullWidth href="https://appwrite.io/pricing" external>
|
||||
<Button
|
||||
fullWidth
|
||||
external
|
||||
on:click={() => {
|
||||
wizard.start(ChangeOrganizationTierCloud);
|
||||
trackEvent('click_organization_upgrade', {
|
||||
from: 'button',
|
||||
source: 'support_menu'
|
||||
});
|
||||
}}>
|
||||
<span class="text">Get Premium support</span>
|
||||
</Button>
|
||||
{:else}
|
||||
|
||||
@@ -16,16 +16,13 @@
|
||||
|
||||
<svelte:window on:resize={throttle(onResize, 250)} />
|
||||
|
||||
<span class={`text ${alternativeTrim ? 'u-trim-1' : 'u-trim'}`} bind:this={container}>
|
||||
{#if showTooltip}
|
||||
<span
|
||||
use:tooltip={{
|
||||
content: container.innerText,
|
||||
maxWidth: '30rem'
|
||||
}}>
|
||||
<slot />
|
||||
</span>
|
||||
{:else}
|
||||
<span><slot /></span>
|
||||
{/if}
|
||||
<span
|
||||
class={`text ${alternativeTrim ? 'u-trim-1' : 'u-trim'}`}
|
||||
bind:this={container}
|
||||
use:tooltip={{
|
||||
disabled: !showTooltip,
|
||||
content: container?.innerText ?? undefined,
|
||||
maxWidth: '30rem'
|
||||
}}>
|
||||
<span><slot /></span>
|
||||
</span>
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
export let hideColumns = false;
|
||||
export let allowNoColumns = false;
|
||||
export let showColsTextMobile = false;
|
||||
export let fullWidthMobile = false;
|
||||
|
||||
let showSelectColumns = false;
|
||||
|
||||
@@ -31,12 +32,15 @@
|
||||
} else {
|
||||
const prefs = preferences.get($page.route);
|
||||
|
||||
columns.set(
|
||||
$columns.map((column) => {
|
||||
column.show = prefs.columns?.includes(column.id) ?? true;
|
||||
return column;
|
||||
})
|
||||
);
|
||||
// Override the shown columns only if a preference was set
|
||||
if (prefs?.columns) {
|
||||
columns.set(
|
||||
$columns.map((column) => {
|
||||
column.show = prefs.columns?.includes(column.id) ?? true;
|
||||
return column;
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
columns.subscribe((ctx) => {
|
||||
@@ -71,8 +75,11 @@
|
||||
<div class="grid-header-col-4">
|
||||
{#if !hideColumns && view === View.Table}
|
||||
{#if $columns?.length}
|
||||
<DropList bind:show={showSelectColumns} scrollable>
|
||||
<Button secondary on:click={() => (showSelectColumns = !showSelectColumns)}>
|
||||
<DropList bind:show={showSelectColumns} scrollable wrapperFullWidth={fullWidthMobile}>
|
||||
<Button
|
||||
secondary
|
||||
on:click={() => (showSelectColumns = !showSelectColumns)}
|
||||
{fullWidthMobile}>
|
||||
<span
|
||||
class="icon-view-boards u-opacity-50"
|
||||
aria-hidden="true"
|
||||
|
||||
+66
-340
@@ -3,6 +3,7 @@ export const CARD_LIMIT = 6; // default card limit
|
||||
export const INTERVAL = 5 * 60000; // default interval to check for feedback
|
||||
|
||||
export enum Dependencies {
|
||||
FACTORS = 'dependency:factors',
|
||||
CREDIT = 'dependency:credit',
|
||||
INVOICES = 'dependency:invoices',
|
||||
ADDRESS = 'dependency:address',
|
||||
@@ -17,6 +18,7 @@ export enum Dependencies {
|
||||
ACCOUNT_SESSIONS = 'dependency:account_sessions',
|
||||
USER = 'dependency:user',
|
||||
USERS = 'dependency:users',
|
||||
USER_TARGETS = 'dependency:user_targets',
|
||||
SESSIONS = 'dependency:sessions',
|
||||
TEAM = 'dependency:team',
|
||||
TEAMS = 'dependency:teams',
|
||||
@@ -47,7 +49,14 @@ export enum Dependencies {
|
||||
MIGRATIONS = 'dependency:migrations',
|
||||
COLLECTIONS = 'dependency:collections',
|
||||
RUNTIMES = 'dependency:runtimes',
|
||||
CONSOLE_VARIABLES = 'dependency:console_variables'
|
||||
CONSOLE_VARIABLES = 'dependency:console_variables',
|
||||
MESSAGING_PROVIDERS = 'dependency:messaging_providers',
|
||||
MESSAGING_PROVIDER = 'dependency:messaging_provider',
|
||||
MESSAGING_MESSAGES = 'dependency:messaging_messages',
|
||||
MESSAGING_MESSAGE = 'dependency:messaging_message',
|
||||
MESSAGING_TOPICS = 'dependency:messaging_topics',
|
||||
MESSAGING_TOPIC = 'dependency:messaging_topic',
|
||||
MESSAGING_TOPIC_SUBSCRIBERS = 'dependency:messaging_topic_subscribers'
|
||||
}
|
||||
|
||||
export const scopes: {
|
||||
@@ -55,6 +64,11 @@ export const scopes: {
|
||||
description: string;
|
||||
category: string;
|
||||
}[] = [
|
||||
{
|
||||
scope: 'sessions.write',
|
||||
description: "Access to create, update and delete your project's sessions",
|
||||
category: 'Auth'
|
||||
},
|
||||
{
|
||||
scope: 'users.read',
|
||||
description: "Access to read your project's users",
|
||||
@@ -168,6 +182,57 @@ export const scopes: {
|
||||
description: "Access to execute your project's functions",
|
||||
category: 'Functions'
|
||||
},
|
||||
{
|
||||
scope: 'targets.read',
|
||||
description: "Access to read your project's messaging targets",
|
||||
category: 'Messaging'
|
||||
},
|
||||
{
|
||||
scope: 'targets.write',
|
||||
description: "Access to create, update, and delete your project's messaging targets",
|
||||
category: 'Messaging'
|
||||
},
|
||||
{
|
||||
scope: 'providers.read',
|
||||
description: "Access to read your project's messaging providers",
|
||||
category: 'Messaging'
|
||||
},
|
||||
{
|
||||
scope: 'providers.write',
|
||||
description: "Access to create, update, and delete your project's messaging providers",
|
||||
category: 'Messaging'
|
||||
},
|
||||
{
|
||||
scope: 'messages.read',
|
||||
description: "Access to read your project's messages",
|
||||
category: 'Messaging'
|
||||
},
|
||||
{
|
||||
scope: 'messages.write',
|
||||
description: "Access to create, update, and delete your project's messages",
|
||||
category: 'Messaging'
|
||||
},
|
||||
{
|
||||
scope: 'topics.read',
|
||||
description: "Access to read your project's messaging topics",
|
||||
category: 'Messaging'
|
||||
},
|
||||
{
|
||||
scope: 'topics.write',
|
||||
description: "Access to create, update, and delete your project's messaging topics",
|
||||
category: 'Messaging'
|
||||
},
|
||||
{
|
||||
scope: 'subscribers.read',
|
||||
description: "Access to read your project's messaging topic subscribers",
|
||||
category: 'Messaging'
|
||||
},
|
||||
{
|
||||
scope: 'subscribers.write',
|
||||
description:
|
||||
"Access to create, update, and delete your project's messaging topic subscribers",
|
||||
category: 'Messaging'
|
||||
},
|
||||
{
|
||||
scope: 'locale.read',
|
||||
description: "Access to access your project's Locale service",
|
||||
@@ -279,345 +344,6 @@ export const eventServices: Array<EventService> = [
|
||||
}
|
||||
];
|
||||
|
||||
export const usageRates = {
|
||||
'tier-0': [
|
||||
{
|
||||
id: 'members',
|
||||
resource: 'Organization members',
|
||||
amount: 1,
|
||||
unit: '',
|
||||
rate: '$20/member'
|
||||
},
|
||||
{ id: 'bandwith', resource: 'Bandwidth', amount: 10, unit: 'GB', rate: '$0.04/GB' },
|
||||
{ id: 'storage', resource: 'Storage', amount: 2, unit: 'GB', rate: '$0.025/GB' },
|
||||
{
|
||||
id: 'executions',
|
||||
resource: 'Function executions',
|
||||
amount: 750000,
|
||||
unit: 'executions',
|
||||
rate: '$2/1M executions'
|
||||
},
|
||||
{
|
||||
id: 'users',
|
||||
resource: 'Active users',
|
||||
amount: 200000,
|
||||
unit: 'AU',
|
||||
rate: '$0.0012/user'
|
||||
},
|
||||
{
|
||||
id: 'connections',
|
||||
resource: 'Concurrent connections',
|
||||
amount: 750,
|
||||
unit: 'connections',
|
||||
rate: '$5/1K connections'
|
||||
}
|
||||
],
|
||||
'tier-1': [
|
||||
{
|
||||
id: 'members',
|
||||
resource: 'Organization members',
|
||||
amount: 'Unlimited',
|
||||
unit: '',
|
||||
rate: '$20/member'
|
||||
},
|
||||
{ id: 'bandwith', resource: 'Bandwidth', amount: 1, unit: 'TB', rate: '$0.04/GB' },
|
||||
{ id: 'storage', resource: 'Storage', amount: 150, unit: 'GB', rate: '$0.025/GB' },
|
||||
{
|
||||
id: 'executions',
|
||||
resource: 'Function executions',
|
||||
amount: 3500000,
|
||||
unit: 'executions',
|
||||
rate: '$2/1M executions'
|
||||
},
|
||||
{
|
||||
id: 'users',
|
||||
resource: 'Active users',
|
||||
amount: 200000,
|
||||
unit: 'AU',
|
||||
rate: '$0.0012/user'
|
||||
},
|
||||
{
|
||||
id: 'connections',
|
||||
resource: 'Concurrent connections',
|
||||
amount: 750,
|
||||
unit: 'connections',
|
||||
rate: '$5/1K connections'
|
||||
}
|
||||
],
|
||||
'tier-2': [
|
||||
{
|
||||
id: 'members',
|
||||
resource: 'Organization members',
|
||||
amount: 'Unlimited',
|
||||
unit: '',
|
||||
rate: '$20/member'
|
||||
},
|
||||
{ id: 'bandwith', resource: 'Bandwidth', amount: 1, unit: 'TB', rate: '$0.04/GB' },
|
||||
{ id: 'storage', resource: 'Storage', amount: 150, unit: 'GB', rate: '$0.025/GB' },
|
||||
{
|
||||
id: 'executions',
|
||||
resource: 'Function executions',
|
||||
amount: 3500000,
|
||||
unit: 'executions',
|
||||
rate: '$2/1M executions'
|
||||
},
|
||||
{
|
||||
id: 'users',
|
||||
resource: 'Active users',
|
||||
amount: 200000,
|
||||
unit: 'AU',
|
||||
rate: '$0.0012/user'
|
||||
},
|
||||
{
|
||||
id: 'connections',
|
||||
resource: 'Concurrent connections',
|
||||
amount: 750,
|
||||
unit: 'connections',
|
||||
rate: '$5/1K connections'
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
//resources: bandwidth, buckets, file size limit, functions, executions, users,teams,logs, members, platforms, webhooks, databases, connections, messages
|
||||
export const limitRates = {
|
||||
'tier-0': [
|
||||
{
|
||||
id: 'bandwith',
|
||||
amount: 10,
|
||||
unit: 'GB'
|
||||
},
|
||||
{
|
||||
id: 'buckets',
|
||||
amount: 3,
|
||||
unit: ''
|
||||
},
|
||||
{
|
||||
id: 'file-size-limit',
|
||||
amount: 1,
|
||||
unit: 'MB'
|
||||
},
|
||||
{
|
||||
id: 'storage',
|
||||
amount: 5,
|
||||
unit: 'GB'
|
||||
},
|
||||
{
|
||||
id: 'functions',
|
||||
amount: 1,
|
||||
unit: ''
|
||||
},
|
||||
{
|
||||
id: 'executions',
|
||||
amount: 750000,
|
||||
unit: 'executions'
|
||||
},
|
||||
{
|
||||
id: 'users',
|
||||
amount: 200000,
|
||||
unit: 'AU'
|
||||
},
|
||||
|
||||
{
|
||||
id: 'teams',
|
||||
amount: 1,
|
||||
unit: ''
|
||||
},
|
||||
|
||||
{
|
||||
id: 'logs',
|
||||
amount: 1,
|
||||
unit: ''
|
||||
},
|
||||
|
||||
{
|
||||
id: 'members',
|
||||
amount: 1,
|
||||
unit: ''
|
||||
},
|
||||
{
|
||||
id: 'platforms',
|
||||
amount: 1,
|
||||
unit: ''
|
||||
},
|
||||
{
|
||||
id: 'webhooks',
|
||||
amount: 1,
|
||||
unit: ''
|
||||
},
|
||||
{
|
||||
id: 'databases',
|
||||
amount: 1,
|
||||
unit: ''
|
||||
},
|
||||
{
|
||||
id: 'connections',
|
||||
amount: 1,
|
||||
unit: ''
|
||||
},
|
||||
{
|
||||
id: 'messages',
|
||||
amount: 1,
|
||||
unit: ''
|
||||
}
|
||||
],
|
||||
'tier-1': [
|
||||
{
|
||||
id: 'bandwith',
|
||||
amount: 10,
|
||||
unit: 'GB'
|
||||
},
|
||||
{
|
||||
id: 'buckets',
|
||||
amount: 1,
|
||||
unit: ''
|
||||
},
|
||||
{
|
||||
id: 'file-size-limit',
|
||||
amount: 5,
|
||||
unit: 'MB'
|
||||
},
|
||||
{
|
||||
id: 'storage',
|
||||
amount: 5,
|
||||
unit: 'GB'
|
||||
},
|
||||
{
|
||||
id: 'functions',
|
||||
amount: 1,
|
||||
unit: ''
|
||||
},
|
||||
{
|
||||
id: 'executions',
|
||||
amount: 750000,
|
||||
unit: 'executions'
|
||||
},
|
||||
{
|
||||
id: 'users',
|
||||
amount: 200000,
|
||||
unit: 'AU'
|
||||
},
|
||||
|
||||
{
|
||||
id: 'teams',
|
||||
amount: 1,
|
||||
unit: ''
|
||||
},
|
||||
{
|
||||
id: 'logs',
|
||||
amount: 1,
|
||||
unit: ''
|
||||
},
|
||||
{
|
||||
id: 'members',
|
||||
amount: 1,
|
||||
unit: ''
|
||||
},
|
||||
{
|
||||
id: 'platforms',
|
||||
amount: 1,
|
||||
unit: ''
|
||||
},
|
||||
{
|
||||
id: 'webhooks',
|
||||
amount: 1,
|
||||
unit: ''
|
||||
},
|
||||
{
|
||||
id: 'databases',
|
||||
amount: 1,
|
||||
unit: ''
|
||||
},
|
||||
{
|
||||
id: 'connections',
|
||||
amount: 1,
|
||||
unit: ''
|
||||
},
|
||||
{
|
||||
id: 'messages',
|
||||
amount: 1,
|
||||
unit: ''
|
||||
}
|
||||
],
|
||||
'tier-2': [
|
||||
{
|
||||
id: 'bandwith',
|
||||
amount: 10,
|
||||
unit: 'GB'
|
||||
},
|
||||
{
|
||||
id: 'buckets',
|
||||
amount: 1,
|
||||
unit: ''
|
||||
},
|
||||
{
|
||||
id: 'file-size-limit',
|
||||
amount: 5,
|
||||
unit: 'MB'
|
||||
},
|
||||
{
|
||||
id: 'storage',
|
||||
amount: 5,
|
||||
unit: 'GB'
|
||||
},
|
||||
{
|
||||
id: 'functions',
|
||||
amount: 1,
|
||||
unit: ''
|
||||
},
|
||||
{
|
||||
id: 'executions',
|
||||
amount: 750000,
|
||||
unit: 'executions'
|
||||
},
|
||||
{
|
||||
id: 'users',
|
||||
amount: 200000,
|
||||
unit: 'AU'
|
||||
},
|
||||
|
||||
{
|
||||
id: 'teams',
|
||||
amount: 1,
|
||||
unit: ''
|
||||
},
|
||||
{
|
||||
id: 'logs',
|
||||
amount: 1,
|
||||
unit: ''
|
||||
},
|
||||
{
|
||||
id: 'members',
|
||||
amount: 1,
|
||||
unit: ''
|
||||
},
|
||||
{
|
||||
id: 'platforms',
|
||||
amount: 1,
|
||||
unit: ''
|
||||
},
|
||||
{
|
||||
id: 'webhooks',
|
||||
amount: 1,
|
||||
unit: ''
|
||||
},
|
||||
{
|
||||
id: 'databases',
|
||||
amount: 1,
|
||||
unit: ''
|
||||
},
|
||||
{
|
||||
id: 'connections',
|
||||
amount: 1,
|
||||
unit: ''
|
||||
},
|
||||
{
|
||||
id: 'messages',
|
||||
amount: 1,
|
||||
unit: ''
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
export enum BillingPlan {
|
||||
STARTER = 'tier-0',
|
||||
PRO = 'tier-1',
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { isValueOfStringEnum } from '$lib/helpers/types';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { Flag } from '@appwrite.io/console';
|
||||
export let flag: string;
|
||||
export let name: string = flag;
|
||||
export let width = 40;
|
||||
@@ -9,6 +11,7 @@
|
||||
export { classes as class };
|
||||
|
||||
export function getFlag(country: string, width: number, height: number, quality: number) {
|
||||
if (!isValueOfStringEnum(Flag, country)) return '';
|
||||
let flag = sdk.forProject.avatars
|
||||
.getFlag(country, width * 2, height * 2, quality)
|
||||
?.toString();
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
export let disabled = false;
|
||||
export let external = false;
|
||||
export let href: string = null;
|
||||
export let download: string = undefined;
|
||||
export let fullWidth = false;
|
||||
export let fullWidthMobile = false;
|
||||
export let ariaLabel: string = null;
|
||||
@@ -60,8 +61,10 @@
|
||||
|
||||
{#if href}
|
||||
<a
|
||||
on:click
|
||||
on:click={track}
|
||||
{href}
|
||||
{download}
|
||||
target={external ? '_blank' : ''}
|
||||
rel={external ? 'noopener noreferrer' : ''}
|
||||
class={resolvedClasses}
|
||||
|
||||
@@ -29,3 +29,6 @@ export { default as Label } from './label.svelte';
|
||||
export { default as InputProjectId } from './inputProjectId.svelte';
|
||||
export { default as InputDate } from './inputDate.svelte';
|
||||
export { default as InputDateRange } from './inputDateRange.svelte';
|
||||
export { default as InputTime } from './inputTime.svelte';
|
||||
export { default as InputDigits } from './inputDigits.svelte';
|
||||
export { default as InputFilePicker } from './inputFilePicker.svelte';
|
||||
|
||||
@@ -1,32 +1,27 @@
|
||||
<script lang="ts">
|
||||
import { FormItem, Helper, Label } from '.';
|
||||
import { FormItem, Helper } from '.';
|
||||
import type { FormItemTag } from './formItem.svelte';
|
||||
|
||||
interface $$Props extends Partial<HTMLLabelElement> {
|
||||
id: string;
|
||||
label?: string;
|
||||
optionalText?: string;
|
||||
tooltip?: string;
|
||||
showLabel?: boolean;
|
||||
checked?: boolean;
|
||||
required?: boolean;
|
||||
disabled?: boolean;
|
||||
element?: HTMLInputElement | undefined;
|
||||
indeterminate?: boolean;
|
||||
wrapperTag?: FormItemTag;
|
||||
size?: 'small' | 'medium';
|
||||
}
|
||||
|
||||
export let id: string;
|
||||
export let label: string | undefined = undefined;
|
||||
export let optionalText: string | undefined = undefined;
|
||||
export let tooltip: string = null;
|
||||
export let showLabel = true;
|
||||
export let checked = false;
|
||||
export let required = false;
|
||||
export let disabled = false;
|
||||
export let element: HTMLInputElement | undefined = undefined;
|
||||
export let wrapperTag: FormItemTag = 'li';
|
||||
|
||||
export let size: $$Props['size'] = 'medium';
|
||||
let error: string;
|
||||
|
||||
const handleInvalid = (event: Event) => {
|
||||
@@ -44,25 +39,28 @@
|
||||
</script>
|
||||
|
||||
<FormItem tag={wrapperTag}>
|
||||
{#if label}
|
||||
<Label {required} {tooltip} {optionalText} hide={!showLabel} for={id}>
|
||||
{label}
|
||||
</Label>
|
||||
{/if}
|
||||
|
||||
<div class="input-text-wrapper">
|
||||
<input
|
||||
{id}
|
||||
{disabled}
|
||||
{required}
|
||||
{...$$restProps}
|
||||
type="checkbox"
|
||||
bind:this={element}
|
||||
bind:checked
|
||||
on:invalid={handleInvalid}
|
||||
on:click
|
||||
on:change />
|
||||
</div>
|
||||
<label class="choice-item" for={id}>
|
||||
<div class="input-text-wrapper">
|
||||
<input
|
||||
{id}
|
||||
{disabled}
|
||||
{required}
|
||||
{...$$restProps}
|
||||
class:is-small={size === 'small'}
|
||||
type="checkbox"
|
||||
bind:this={element}
|
||||
bind:checked
|
||||
on:invalid={handleInvalid}
|
||||
on:click
|
||||
on:change />
|
||||
</div>
|
||||
<div class="choice-item-content">
|
||||
{#if label}
|
||||
<div class="choice-item-title">{label}</div>
|
||||
{/if}
|
||||
<slot name="description" />
|
||||
</div>
|
||||
</label>
|
||||
{#if error}
|
||||
<Helper type="warning">{error}</Helper>
|
||||
{/if}
|
||||
|
||||
@@ -39,6 +39,7 @@
|
||||
aria-checked={value}
|
||||
bind:this={element}
|
||||
bind:checked={value}
|
||||
on:change
|
||||
on:invalid={handleInvalid} />
|
||||
|
||||
<div class="choice-item-content" class:u-width-full-line={fullWidth}>
|
||||
|
||||
@@ -10,6 +10,8 @@
|
||||
export let value = '';
|
||||
export let required = false;
|
||||
export let nullable = false;
|
||||
export let min: string | number | undefined = undefined;
|
||||
export let max: string | number | undefined = undefined;
|
||||
export let disabled = false;
|
||||
export let readonly = false;
|
||||
export let autofocus = false;
|
||||
@@ -65,12 +67,18 @@
|
||||
{readonly}
|
||||
{required}
|
||||
step=".001"
|
||||
{min}
|
||||
{max}
|
||||
autocomplete={autocomplete ? 'on' : 'off'}
|
||||
type="date"
|
||||
style={disabled ? '' : 'cursor: pointer;'}
|
||||
class="input-text"
|
||||
bind:value
|
||||
bind:this={element}
|
||||
on:invalid={handleInvalid}
|
||||
on:click={function () {
|
||||
this.showPicker();
|
||||
}}
|
||||
style:--amount-of-buttons={isNullable ? 2.75 : 1}
|
||||
style:--button-size={isNullable ? '2rem' : '1rem'} />
|
||||
{#if isNullable}
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { FormItem } from '.';
|
||||
import { createPinInput, melt } from '@melt-ui/svelte';
|
||||
|
||||
export let length: number = 6;
|
||||
export let value: string = '';
|
||||
export let required = false;
|
||||
export let disabled = false;
|
||||
export let readonly = false;
|
||||
export let autofocus = false;
|
||||
export let fullWidth = false;
|
||||
|
||||
let element: HTMLOListElement;
|
||||
|
||||
let autoSubmitted = false;
|
||||
|
||||
const {
|
||||
elements: { root, input }
|
||||
} = createPinInput({
|
||||
placeholder: '',
|
||||
defaultValue: value.split(''),
|
||||
onValueChange: ({ next }) => {
|
||||
value = next.join('');
|
||||
|
||||
if (value.length === 6 && !autoSubmitted) {
|
||||
autoSubmitted = true;
|
||||
const firstInputElement = element.querySelector('input');
|
||||
firstInputElement?.form.requestSubmit();
|
||||
}
|
||||
|
||||
return next;
|
||||
}
|
||||
});
|
||||
|
||||
onMount(() => {
|
||||
const interval = setInterval(() => {
|
||||
const input = element.querySelector('input');
|
||||
if (element) {
|
||||
if (input && autofocus) {
|
||||
input.focus();
|
||||
}
|
||||
clearInterval(interval);
|
||||
}
|
||||
}, 10);
|
||||
});
|
||||
</script>
|
||||
|
||||
<FormItem {fullWidth}>
|
||||
<ol class="u-flex u-main-center u-gap-16" use:melt={$root} bind:this={element}>
|
||||
{#each Array.from({ length }) as _}
|
||||
<li>
|
||||
<input
|
||||
type="number"
|
||||
class="verification-code-input u-remove-input-number-buttons"
|
||||
maxlength="1"
|
||||
style:--p-input-size="3.75rem"
|
||||
style:font-size="2rem"
|
||||
use:melt={$input()}
|
||||
{required}
|
||||
{readonly}
|
||||
{disabled} />
|
||||
</li>
|
||||
{/each}
|
||||
</ol>
|
||||
</FormItem>
|
||||
@@ -1,6 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { SvelteComponent, onMount } from 'svelte';
|
||||
import { FormItem, Helper, Label } from '.';
|
||||
import { Drop } from '$lib/components';
|
||||
|
||||
export let label: string;
|
||||
export let showLabel = true;
|
||||
@@ -13,12 +14,16 @@
|
||||
export let autofocus = false;
|
||||
export let autocomplete = false;
|
||||
export let maxlength: number = null;
|
||||
export let popover: typeof SvelteComponent<unknown> = null;
|
||||
export let popoverProps: Record<string, unknown> = {};
|
||||
export let fullWidth = false;
|
||||
|
||||
// https://www.geeksforgeeks.org/how-to-validate-a-domain-name-using-regular-expression/
|
||||
const pattern = String.raw`^(?!-)[A-Za-z0-9-]+([\-\.]{1}[a-z0-9]+)*\.[A-Za-z]{2,18}$`;
|
||||
const pattern = String.raw`(?!-)[A-Za-z0-9\-]+([\-\.]{1}[a-z0-9]+)*\.[A-Za-z]{2,18}`;
|
||||
|
||||
let element: HTMLInputElement;
|
||||
let error: string;
|
||||
let show = false;
|
||||
|
||||
onMount(() => {
|
||||
if (element && autofocus) {
|
||||
@@ -46,9 +51,32 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<FormItem>
|
||||
<FormItem {fullWidth}>
|
||||
<Label {required} hide={!showLabel} for={id}>
|
||||
{label}
|
||||
{label}{#if popover}
|
||||
<Drop isPopover bind:show display="inline-block">
|
||||
<!-- TODO: make unclicked icon greyed out and hover and clicked filled -->
|
||||
<button
|
||||
type="button"
|
||||
on:click={() => (show = !show)}
|
||||
class="tooltip"
|
||||
aria-label="input tooltip">
|
||||
<span
|
||||
class="icon-info"
|
||||
aria-hidden="true"
|
||||
style="font-size: var(--icon-size-small)" />
|
||||
</button>
|
||||
<svelte:fragment slot="list">
|
||||
<div
|
||||
class="dropped card u-max-width-250"
|
||||
style:--card-border-radius="var(--border-radius-small)"
|
||||
style:--p-card-padding=".75rem"
|
||||
style:box-shadow="var(--shadow-large)">
|
||||
<svelte:component this={popover} {...popoverProps} />
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
</Drop>
|
||||
{/if}
|
||||
</Label>
|
||||
|
||||
<div class="input-text-wrapper">
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { SvelteComponent, onMount } from 'svelte';
|
||||
import { FormItem, Helper, Label } from '.';
|
||||
import NullCheckbox from './nullCheckbox.svelte';
|
||||
import { Drop } from '$lib/components';
|
||||
|
||||
export let label: string;
|
||||
export let optionalText: string | undefined = undefined;
|
||||
@@ -16,9 +17,13 @@
|
||||
export let autofocus = false;
|
||||
export let autocomplete = false;
|
||||
export let tooltip: string = null;
|
||||
export let popover: typeof SvelteComponent<unknown> = null;
|
||||
export let popoverProps: Record<string, unknown> = {};
|
||||
export let fullWidth = false;
|
||||
|
||||
let element: HTMLInputElement;
|
||||
let error: string;
|
||||
let show = false;
|
||||
|
||||
onMount(() => {
|
||||
if (element && autofocus) {
|
||||
@@ -55,9 +60,32 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<FormItem>
|
||||
<FormItem {fullWidth}>
|
||||
<Label {required} {optionalText} {tooltip} hide={!showLabel} for={id}>
|
||||
{label}
|
||||
{label}{#if popover}
|
||||
<Drop isPopover bind:show display="inline-block">
|
||||
<!-- TODO: make unclicked icon greyed out and hover and clicked filled -->
|
||||
<button
|
||||
type="button"
|
||||
on:click={() => (show = !show)}
|
||||
class="tooltip"
|
||||
aria-label="input tooltip">
|
||||
<span
|
||||
class="icon-info"
|
||||
aria-hidden="true"
|
||||
style="font-size: var(--icon-size-small)" />
|
||||
</button>
|
||||
<svelte:fragment slot="list">
|
||||
<div
|
||||
class="dropped card u-max-width-250"
|
||||
style:--card-border-radius="var(--border-radius-small)"
|
||||
style:--p-card-padding=".75rem"
|
||||
style:box-shadow="var(--shadow-large)">
|
||||
<svelte:component this={popover} {...popoverProps} />
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
</Drop>
|
||||
{/if}
|
||||
</Label>
|
||||
|
||||
<div
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { Trim } from '$lib/components';
|
||||
import { Drop, Trim } from '$lib/components';
|
||||
import { humanFileSize } from '$lib/helpers/sizeConvertion';
|
||||
import { onMount } from 'svelte';
|
||||
import { SvelteComponent, onMount } from 'svelte';
|
||||
import { Helper, Label } from '.';
|
||||
|
||||
export let label: string = null;
|
||||
@@ -13,9 +13,12 @@
|
||||
export let optionalText: string = null;
|
||||
export let tooltip: string = null;
|
||||
export let error: string = null;
|
||||
export let popover: typeof SvelteComponent<unknown> = null;
|
||||
export let popoverProps: Record<string, unknown> = {};
|
||||
|
||||
let input: HTMLInputElement;
|
||||
let hovering = false;
|
||||
let show = false;
|
||||
|
||||
function setFiles(value: FileList) {
|
||||
if (!value) return;
|
||||
@@ -97,7 +100,30 @@
|
||||
<div>
|
||||
{#if label}
|
||||
<Label {required} {optionalText} {tooltip} hide={!label}>
|
||||
{label}
|
||||
{label}{#if popover}
|
||||
<Drop isPopover bind:show display="inline-block">
|
||||
<!-- TODO: make unclicked icon greyed out and hover and clicked filled -->
|
||||
<button
|
||||
type="button"
|
||||
on:click={() => (show = !show)}
|
||||
class="tooltip"
|
||||
aria-label="input tooltip">
|
||||
<span
|
||||
class="icon-info"
|
||||
aria-hidden="true"
|
||||
style="font-size: var(--icon-size-small)" />
|
||||
</button>
|
||||
<svelte:fragment slot="list">
|
||||
<div
|
||||
class="dropped card u-max-width-250"
|
||||
style:--card-border-radius="var(--border-radius-small)"
|
||||
style:--p-card-padding=".75rem"
|
||||
style:box-shadow="var(--shadow-large)">
|
||||
<svelte:component this={popover} {...popoverProps} />
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
</Drop>
|
||||
{/if}
|
||||
</Label>
|
||||
{/if}
|
||||
<div
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
<script lang="ts">
|
||||
import { Drop, Trim } from '$lib/components';
|
||||
import FilePicker from '$lib/components/filePicker.svelte';
|
||||
import { humanFileSize } from '$lib/helpers/sizeConvertion';
|
||||
import type { Models } from '@appwrite.io/console';
|
||||
import { Label } from '.';
|
||||
|
||||
export let label: string = null;
|
||||
export let value: Models.File = null;
|
||||
export let disabled = false;
|
||||
|
||||
export let optionalText: string = null;
|
||||
export let tooltip: string = null;
|
||||
export let isPopoverDefined = true;
|
||||
|
||||
let show = false;
|
||||
|
||||
function onSelect(file: Models.File) {
|
||||
value = file;
|
||||
}
|
||||
</script>
|
||||
|
||||
<div>
|
||||
{#if label}
|
||||
<Label {optionalText} {tooltip} hide={!label}>
|
||||
{label}{#if $$slots.popover && isPopoverDefined}
|
||||
<Drop bind:show display="inline-block">
|
||||
<!-- TODO: make unclicked icon greyed out and hover and clicked filled -->
|
||||
<button
|
||||
type="button"
|
||||
on:click={() => (show = !show)}
|
||||
class="tooltip"
|
||||
aria-label="input tooltip">
|
||||
<span
|
||||
class="icon-info"
|
||||
aria-hidden="true"
|
||||
style="font-size: var(--icon-size-small)" />
|
||||
</button>
|
||||
<svelte:fragment slot="list">
|
||||
<div
|
||||
class="dropped card u-max-width-250"
|
||||
style="--p-card-padding: .75rem; box-shadow:var(--shadow-large);">
|
||||
<slot name="popover" />
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
</Drop>
|
||||
{/if}
|
||||
</Label>
|
||||
{/if}
|
||||
<div
|
||||
role="region"
|
||||
class="box is-no-shadow u-padding-24 is-border-dashed"
|
||||
style:--box-border-radius="var(--border-radius-xsmall)">
|
||||
<div class="upload-file-box">
|
||||
<div class="upload-file-box-image">
|
||||
<span class="icon-upload" aria-hidden="true" />
|
||||
</div>
|
||||
<div class="u-min-width-0 u-text-center">
|
||||
<h5 class="upload-file-box-title heading-level-7 u-inline">
|
||||
<span class="is-only-desktop">Select a file to upload</span>
|
||||
<span class="is-not-desktop">Select a file to upload</span>
|
||||
</h5>
|
||||
</div>
|
||||
<div class="u-flex u-main-center u-cross-center u-gap-16 u-flex-vertical-mobile">
|
||||
Max file size: 1MB
|
||||
<button
|
||||
class="button is-secondary is-full-width-mobile"
|
||||
type="button"
|
||||
{disabled}
|
||||
on:click={() => (show = true)}>
|
||||
<span class="text">Browse</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{#if value}
|
||||
{@const fileSize = humanFileSize(value.sizeOriginal)}
|
||||
<ul class="upload-file-box-list u-min-width-0">
|
||||
<li class="u-flex u-cross-center u-min-width-0">
|
||||
<span class="icon-document" aria-hidden="true" />
|
||||
<span class="upload-file-box-name u-min-width-0">
|
||||
<Trim alternativeTrim>{value.name}</Trim>
|
||||
</span>
|
||||
<span
|
||||
class="upload-file-box-size u-margin-inline-start-4 u-margin-inline-end-16">
|
||||
{fileSize.value + fileSize.unit}
|
||||
</span>
|
||||
<button
|
||||
on:click={() => (value = null)}
|
||||
type="button"
|
||||
class="button is-text is-only-icon u-margin-inline-start-auto"
|
||||
aria-label="remove file"
|
||||
style="--button-size:1.5rem;">
|
||||
<span class="icon-x" aria-hidden="true" />
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if show}
|
||||
<FilePicker selectedFile={value?.$id} selectedBucket={value?.bucketId} bind:show {onSelect} />
|
||||
{/if}
|
||||
@@ -1,6 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { SvelteComponent, onMount } from 'svelte';
|
||||
import { FormItem, Helper, Label } from '.';
|
||||
import { Drop } from '$lib/components';
|
||||
|
||||
export let id: string;
|
||||
export let label: string;
|
||||
@@ -15,10 +16,14 @@
|
||||
export let showPasswordButton = false;
|
||||
export let minlength = 8;
|
||||
export let maxlength: number = null;
|
||||
export let popover: typeof SvelteComponent<unknown> = null;
|
||||
export let popoverProps: Record<string, unknown> = {};
|
||||
export let fullWidth = false;
|
||||
|
||||
let element: HTMLInputElement;
|
||||
let error: string;
|
||||
let showInPlainText = false;
|
||||
let showPopover = false;
|
||||
|
||||
onMount(() => {
|
||||
if (element && autofocus) {
|
||||
@@ -46,9 +51,32 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<FormItem>
|
||||
<FormItem {fullWidth}>
|
||||
<Label {required} hide={!showLabel} for={id}>
|
||||
{label}
|
||||
{label}{#if popover}
|
||||
<Drop isPopover bind:show={showPopover} display="inline-block">
|
||||
<!-- TODO: make unclicked icon greyed out and hover and clicked filled -->
|
||||
<button
|
||||
type="button"
|
||||
on:click={() => (showPopover = !showPopover)}
|
||||
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:--card-border-radius="var(--border-radius-small)"
|
||||
style:--p-card-padding=".75rem"
|
||||
style:box-shadow="var(--shadow-large)">
|
||||
<svelte:component this={popover} {...popoverProps} />
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
</Drop>
|
||||
{/if}
|
||||
</Label>
|
||||
|
||||
<div class="input-text-wrapper" style={showPasswordButton ? '--amount-of-buttons: 1' : ''}>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { SvelteComponent, onMount } from 'svelte';
|
||||
import { FormItem, Helper, Label } from '.';
|
||||
import { Drop } from '$lib/components';
|
||||
|
||||
export let label: string;
|
||||
export let showLabel = true;
|
||||
@@ -13,11 +14,15 @@
|
||||
export let autofocus = false;
|
||||
export let autocomplete = false;
|
||||
export let maxlength: number = null;
|
||||
export let popover: typeof SvelteComponent<unknown> = null;
|
||||
export let popoverProps: Record<string, unknown> = {};
|
||||
export let fullWidth = false;
|
||||
|
||||
const pattern = String.raw`^\+?[1-9]\d{1,14}$`;
|
||||
|
||||
let element: HTMLInputElement;
|
||||
let error: string;
|
||||
let show = false;
|
||||
|
||||
onMount(() => {
|
||||
if (element && autofocus) {
|
||||
@@ -45,9 +50,32 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<FormItem>
|
||||
<FormItem {fullWidth}>
|
||||
<Label {required} hide={!showLabel} for={id}>
|
||||
{label}
|
||||
{label}{#if popover}
|
||||
<Drop isPopover bind:show display="inline-block">
|
||||
<!-- TODO: make unclicked icon greyed out and hover and clicked filled -->
|
||||
<button
|
||||
type="button"
|
||||
on:click={() => (show = !show)}
|
||||
class="tooltip"
|
||||
aria-label="input tooltip">
|
||||
<span
|
||||
class="icon-info"
|
||||
aria-hidden="true"
|
||||
style="font-size: var(--icon-size-small)" />
|
||||
</button>
|
||||
<svelte:fragment slot="list">
|
||||
<div
|
||||
class="dropped card u-max-width-250"
|
||||
style:--card-border-radius="var(--border-radius-small)"
|
||||
style:--p-card-padding=".75rem"
|
||||
style:box-shadow="var(--shadow-large)">
|
||||
<svelte:component this={popover} {...popoverProps} />
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
</Drop>
|
||||
{/if}
|
||||
</Label>
|
||||
<div class="input-text-wrapper">
|
||||
<input
|
||||
|
||||
@@ -47,6 +47,24 @@
|
||||
<slot />
|
||||
{/if}
|
||||
</Label>
|
||||
<!-- <label class="choice-item" for={id}>
|
||||
<input
|
||||
{id}
|
||||
{name}
|
||||
{disabled}
|
||||
{required}
|
||||
{value}
|
||||
type="radio"
|
||||
bind:group
|
||||
bind:this={element}
|
||||
on:invalid={handleInvalid} />
|
||||
<div
|
||||
class="choice-item-content u-cross-child-center"
|
||||
class:u-width-full-line={fullWidth}>
|
||||
<div class="choice-item-title">{label}</div>
|
||||
<slot name="description" />
|
||||
</div>
|
||||
</label> -->
|
||||
</div>
|
||||
{#if error}
|
||||
<Helper type="warning">{error}</Helper>
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
export let disabled = false;
|
||||
export let autofocus = false;
|
||||
export let isWithEndButton = true;
|
||||
export let style: string = '';
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
let element: HTMLInputElement;
|
||||
@@ -53,6 +54,7 @@
|
||||
{placeholder}
|
||||
{disabled}
|
||||
{required}
|
||||
{style}
|
||||
type="search"
|
||||
class="input-text"
|
||||
bind:this={element}
|
||||
|
||||
@@ -24,7 +24,6 @@
|
||||
export let hideRequired = false;
|
||||
export let disabled = false;
|
||||
export let fullWidth = false;
|
||||
export let fullWidthDrop = true;
|
||||
export let autofocus = false;
|
||||
export let interactiveOutput = false;
|
||||
// stretch is used inside of a flex container to give the element flex:1
|
||||
@@ -120,7 +119,6 @@
|
||||
scrollable
|
||||
placement="bottom-end"
|
||||
position="static"
|
||||
fullWidth={fullWidthDrop}
|
||||
fixed>
|
||||
<Label {required} {hideRequired} {optionalText} hide={!showLabel} for={id} {tooltip}>
|
||||
{label}
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
export let label: string;
|
||||
export let id: string;
|
||||
export let value = false;
|
||||
export let required = false;
|
||||
export let disabled = false;
|
||||
|
||||
let element: HTMLInputElement;
|
||||
@@ -13,10 +12,6 @@
|
||||
const handleInvalid = (event: Event) => {
|
||||
event.preventDefault();
|
||||
|
||||
if (element.validity.valueMissing) {
|
||||
error = 'This field is required';
|
||||
return;
|
||||
}
|
||||
error = element.validationMessage;
|
||||
};
|
||||
|
||||
@@ -31,7 +26,6 @@
|
||||
<input
|
||||
{id}
|
||||
{disabled}
|
||||
{required}
|
||||
type="checkbox"
|
||||
class="switch"
|
||||
role="switch"
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
export let disabled = false;
|
||||
export let readonly = false;
|
||||
export let required = false;
|
||||
export let tooltip: string = null;
|
||||
|
||||
let value = '';
|
||||
let element: HTMLInputElement;
|
||||
@@ -77,7 +78,7 @@
|
||||
value={tags.join(',')}
|
||||
{required}
|
||||
on:invalid={handleInvalid} />
|
||||
<Label {required} hide={!showLabel} for={id}>
|
||||
<Label {required} {tooltip} hide={!showLabel} for={id}>
|
||||
{label}
|
||||
</Label>
|
||||
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { SvelteComponent, onMount } from 'svelte';
|
||||
import { FormItem, FormItemPart, Helper, Label } from '.';
|
||||
import NullCheckbox from './nullCheckbox.svelte';
|
||||
import TextCounter from './textCounter.svelte';
|
||||
import { Drop } from '$lib/components';
|
||||
|
||||
export let label: string = undefined;
|
||||
export let optionalText: string | undefined = undefined;
|
||||
@@ -22,9 +23,12 @@
|
||||
export let maxlength: number = null;
|
||||
export let tooltip: string = null;
|
||||
export let isMultiple = false;
|
||||
export let popover: typeof SvelteComponent<unknown> = null;
|
||||
export let popoverProps: Record<string, unknown> = {};
|
||||
|
||||
let element: HTMLInputElement;
|
||||
let error: string;
|
||||
let show = false;
|
||||
|
||||
onMount(() => {
|
||||
if (element && autofocus) {
|
||||
@@ -74,7 +78,30 @@
|
||||
<svelte:component this={wrapper} {fullWidth}>
|
||||
{#if label}
|
||||
<Label {required} {hideRequired} {tooltip} {optionalText} hide={!showLabel} for={id}>
|
||||
{label}
|
||||
{label}{#if popover}
|
||||
<Drop isPopover bind:show display="inline-block">
|
||||
<!-- TODO: make unclicked icon greyed out and hover and clicked filled -->
|
||||
<button
|
||||
type="button"
|
||||
on:click={() => (show = !show)}
|
||||
class="tooltip"
|
||||
aria-label="input tooltip">
|
||||
<span
|
||||
class="icon-info"
|
||||
aria-hidden="true"
|
||||
style:font-size="var(--icon-size-small)" />
|
||||
</button>
|
||||
<svelte:fragment slot="list">
|
||||
<div
|
||||
class="dropped card u-max-width-250"
|
||||
style:--p-card-padding=".75rem"
|
||||
style:--card-border-radius="var(--border-radius-small)"
|
||||
style:box-shadow="var(--shadow-large)">
|
||||
<svelte:component this={popover} {...popoverProps} />
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
</Drop>
|
||||
{/if}
|
||||
</Label>
|
||||
{/if}
|
||||
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { FormItem, Helper, Label } from '.';
|
||||
|
||||
export let label: string;
|
||||
export let showLabel = true;
|
||||
export let optionalText: string | undefined = undefined;
|
||||
export let id: string;
|
||||
export let value = '';
|
||||
export let required = false;
|
||||
export let min: string | number | undefined = undefined;
|
||||
export let max: string | number | undefined = undefined;
|
||||
export let disabled = false;
|
||||
export let readonly = false;
|
||||
export let autofocus = false;
|
||||
export let autocomplete = false;
|
||||
|
||||
let element: HTMLInputElement;
|
||||
let error: string;
|
||||
|
||||
onMount(() => {
|
||||
if (element && autofocus) {
|
||||
element.focus();
|
||||
}
|
||||
});
|
||||
|
||||
function handleInvalid(event: Event) {
|
||||
event.preventDefault();
|
||||
|
||||
if (element.validity.valueMissing) {
|
||||
error = 'This field is required';
|
||||
return;
|
||||
}
|
||||
|
||||
error = element.validationMessage;
|
||||
}
|
||||
|
||||
$: if (value) {
|
||||
error = null;
|
||||
}
|
||||
</script>
|
||||
|
||||
<FormItem>
|
||||
<Label {required} {optionalText} hide={!showLabel} for={id}>
|
||||
{label}
|
||||
</Label>
|
||||
|
||||
<div class="input-text-wrapper" style="--amount-of-buttons:1; --button-size: 1rem">
|
||||
<input
|
||||
{id}
|
||||
{disabled}
|
||||
{readonly}
|
||||
{required}
|
||||
{min}
|
||||
{max}
|
||||
step="60"
|
||||
autocomplete={autocomplete ? 'on' : 'off'}
|
||||
type="time"
|
||||
class="input-text"
|
||||
style={disabled ? '' : 'cursor: pointer;'}
|
||||
bind:value
|
||||
bind:this={element}
|
||||
on:invalid={handleInvalid}
|
||||
on:click={function () {
|
||||
this.showPicker();
|
||||
}} />
|
||||
</div>
|
||||
{#if error}
|
||||
<Helper type="warning">{error}</Helper>
|
||||
{/if}
|
||||
</FormItem>
|
||||
@@ -1,4 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { trackEvent } from '$lib/actions/analytics';
|
||||
import { getServiceLimit, type PlanServices } from '$lib/stores/billing';
|
||||
import { wizard } from '$lib/stores/wizard';
|
||||
import { isCloud } from '$lib/system';
|
||||
@@ -10,6 +11,7 @@
|
||||
export let service: PlanServices = null;
|
||||
export let name = service;
|
||||
export let total: number = null;
|
||||
export let event: string = null;
|
||||
|
||||
let columns = 0;
|
||||
|
||||
@@ -34,7 +36,14 @@
|
||||
<span class="u-flex u-gap-24 u-main-center u-cross-center">
|
||||
<slot name="limit" {upgradeMethod} {limit}>
|
||||
<span class="text">Upgrade your plan to add {name} to your organization</span>
|
||||
<Button secondary on:click={upgradeMethod}>Upgrade plan</Button>
|
||||
<Button
|
||||
secondary
|
||||
on:click={upgradeMethod}
|
||||
on:click={() =>
|
||||
trackEvent('click_organization_upgrade', {
|
||||
from: 'button',
|
||||
source: event ?? 'table_row_limit_reached'
|
||||
})}>Upgrade plan</Button>
|
||||
</slot>
|
||||
</span>
|
||||
</td>
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
|
||||
export let id: string;
|
||||
export let selectedIds: string[] = [];
|
||||
export let disabled: boolean = false;
|
||||
let el: HTMLInputElement;
|
||||
|
||||
const handleClick = (e: Event) => {
|
||||
@@ -35,6 +36,7 @@
|
||||
id="select-{id}"
|
||||
wrapperTag="div"
|
||||
checked={selectedIds.includes(id)}
|
||||
{disabled}
|
||||
on:click={handleClick} />
|
||||
</TableCell>
|
||||
|
||||
|
||||
@@ -3,15 +3,21 @@
|
||||
export let noStyles = false;
|
||||
export let style = '';
|
||||
export let transparent = false;
|
||||
export let isAutoLayout = false;
|
||||
export let tag: 'div' | 'table' = 'div';
|
||||
export let dense = false;
|
||||
</script>
|
||||
|
||||
<div
|
||||
<svelte:element
|
||||
this={tag}
|
||||
class="table is-selected-columns-mobile"
|
||||
class:is-table-layout-auto={isAutoLayout}
|
||||
class:u-margin-block-start-32={!noMargin}
|
||||
class:is-remove-outer-styles={noStyles}
|
||||
class:is-table-row-medium-size={dense}
|
||||
{style}
|
||||
style:--p-table-bg-color={transparent ? 'var(--transparent)' : ''}
|
||||
role="table"
|
||||
data-private>
|
||||
<slot />
|
||||
</div>
|
||||
</svelte:element>
|
||||
|
||||
@@ -5,6 +5,9 @@
|
||||
export let noMargin = false;
|
||||
export let style = '';
|
||||
export let transparent = false;
|
||||
export let noStyles = false;
|
||||
export let dense = false;
|
||||
|
||||
let isOverflowing = false;
|
||||
|
||||
const hasOverflow: Action<HTMLDivElement, unknown> = (node) => {
|
||||
@@ -44,7 +47,9 @@
|
||||
<div class="table-wrapper" use:hasOverflow={(v) => (isOverflowing = v)}>
|
||||
<table
|
||||
class="table"
|
||||
class:is-remove-outer-styles={noStyles}
|
||||
class:is-sticky-scroll={isSticky && isOverflowing}
|
||||
class:is-table-row-medium-size={dense}
|
||||
{style}
|
||||
style:--p-table-bg-color={transparent ? 'var(--transparent)' : ''}>
|
||||
<slot />
|
||||
|
||||
@@ -41,6 +41,38 @@ export const toLocaleDateTime = (datetime: string | number) => {
|
||||
return date.toLocaleDateString('en', options);
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns a date string usig the local timezone in ISO format (yyyy-mm-dd)
|
||||
*
|
||||
* @param datetime date string or milliseconds since the epoch
|
||||
*/
|
||||
export const toLocaleDateISO = (datetime: string | number) => {
|
||||
const date = new Date(datetime);
|
||||
|
||||
if (isNaN(date.getTime())) {
|
||||
return 'n/a';
|
||||
}
|
||||
|
||||
// Use Sweden's locale (sv) since it matches ISO format
|
||||
return date.toLocaleDateString('sv');
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns a time string usig the local timezone in ISO format (hh:mm:ss)
|
||||
*
|
||||
* @param datetime date string or milliseconds since the epoch
|
||||
*/
|
||||
export const toLocaleTimeISO = (datetime: string | number) => {
|
||||
const date = new Date(datetime);
|
||||
|
||||
if (isNaN(date.getTime())) {
|
||||
return 'n/a';
|
||||
}
|
||||
|
||||
// Use Sweden's locale (sv) since it matches ISO format
|
||||
return date.toLocaleTimeString('sv');
|
||||
};
|
||||
|
||||
export const isSameDay = (date1: Date, date2: Date) => {
|
||||
return (
|
||||
date1.getFullYear() === date2.getFullYear() &&
|
||||
|
||||
@@ -36,7 +36,7 @@ export function getQuery(url: URL): string | undefined {
|
||||
return url.searchParams.get('query') ?? undefined;
|
||||
}
|
||||
|
||||
type TabElement = { href: string; title: string; hasChildren?: boolean };
|
||||
export type TabElement = { href: string; title: string; event: string; hasChildren?: boolean };
|
||||
|
||||
export function isTabSelected(
|
||||
tab: TabElement,
|
||||
|
||||
@@ -20,3 +20,12 @@ export function formatNumberWithCommas(number: number): string {
|
||||
const formatter = new Intl.NumberFormat('en');
|
||||
return formatter.format(number);
|
||||
}
|
||||
|
||||
export function formatCurrency(number: number, locale = 'en-US', currency = 'USD'): string {
|
||||
if (isNaN(number)) return String(number);
|
||||
const formatter = new Intl.NumberFormat(locale, {
|
||||
style: 'currency',
|
||||
currency
|
||||
});
|
||||
return formatter.format(number);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
export function omit<T, K extends keyof T>(obj: T, ...keys: K[]): Omit<T, K> {
|
||||
const ret = { ...obj };
|
||||
for (const key of keys) {
|
||||
delete ret[key];
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
@@ -24,13 +24,17 @@ export function bytesToSize(value: number, unit: Size) {
|
||||
return value / Math.pow(1024, index);
|
||||
}
|
||||
|
||||
export function humanFileSize(bytes: number): {
|
||||
export function humanFileSize(
|
||||
bytes: number,
|
||||
useBits = false
|
||||
): {
|
||||
value: string;
|
||||
unit: Size;
|
||||
} {
|
||||
if (typeof bytes !== 'number') return { value: '0', unit: 'Bytes' };
|
||||
const value = prettyBytes(bytes, {
|
||||
locale: 'en'
|
||||
locale: 'en',
|
||||
bits: useBits
|
||||
}).split(' ');
|
||||
|
||||
return {
|
||||
|
||||
@@ -19,6 +19,20 @@ export type Column = {
|
||||
id: string;
|
||||
title: string;
|
||||
type: ColumnType;
|
||||
/**
|
||||
* Set to false to hide by default
|
||||
*/
|
||||
show: boolean;
|
||||
width?: number;
|
||||
/**
|
||||
* Set to false to disable filtering for this column
|
||||
*/
|
||||
filter?: boolean;
|
||||
};
|
||||
|
||||
export function isValueOfStringEnum<T extends Record<string, string>>(
|
||||
enumType: T,
|
||||
value: string
|
||||
): value is T[keyof T] {
|
||||
return Object.values<string>(enumType).includes(value);
|
||||
}
|
||||
|
||||
@@ -73,12 +73,12 @@ export function createTimeUnitPair(initialValue = 0) {
|
||||
return { ...createValueUnitPair(initialValue, units), units };
|
||||
}
|
||||
|
||||
export function createByteUnitPair(initialValue = 0) {
|
||||
export function createByteUnitPair(initialValue = 0, base = 1024) {
|
||||
const units: Unit[] = [
|
||||
{ name: 'Bytes', value: 1 },
|
||||
{ name: 'Kilobytes', value: 1024 },
|
||||
{ name: 'Megabytes', value: 1024 ** 2 },
|
||||
{ name: 'Gigabytes', value: 1024 ** 3 }
|
||||
{ name: 'Kilobytes', value: base },
|
||||
{ name: 'Megabytes', value: base ** 2 },
|
||||
{ name: 'Gigabytes', value: base ** 3 }
|
||||
];
|
||||
return { ...createValueUnitPair(initialValue, units), units };
|
||||
}
|
||||
|
||||
@@ -1,77 +0,0 @@
|
||||
import { page } from '$app/stores';
|
||||
import { get } from 'svelte/store';
|
||||
import type { Metric } from 'web-vitals';
|
||||
|
||||
type Options = {
|
||||
params:
|
||||
| {
|
||||
[s: string]: string;
|
||||
}
|
||||
| ArrayLike<string>;
|
||||
path: string;
|
||||
analyticsId: string;
|
||||
debug?: boolean;
|
||||
};
|
||||
|
||||
const vitalsUrl = 'https://vitals.vercel-analytics.com/v1/vitals';
|
||||
|
||||
function getConnectionSpeed() {
|
||||
return 'connection' in navigator &&
|
||||
navigator['connection'] &&
|
||||
'effectiveType' in (navigator['connection'] as Record<string, unknown>)
|
||||
? navigator['connection']['effectiveType']
|
||||
: '';
|
||||
}
|
||||
|
||||
function sendToAnalytics(metric: Metric, options: Options) {
|
||||
const page = Object.entries(options.params).reduce(
|
||||
(acc, [key, value]) => acc.replace(value, `[${key}]`),
|
||||
options.path
|
||||
);
|
||||
|
||||
const body = {
|
||||
dsn: options.analyticsId,
|
||||
id: metric.id,
|
||||
page,
|
||||
href: location.href,
|
||||
event_name: metric.name,
|
||||
value: metric.value.toString(),
|
||||
speed: getConnectionSpeed()
|
||||
};
|
||||
|
||||
if (options.debug) {
|
||||
console.debug('[Analytics]', metric.name, JSON.stringify(body, null, 2));
|
||||
}
|
||||
|
||||
const blob = new Blob([new URLSearchParams(body).toString()], {
|
||||
// This content type is necessary for `sendBeacon`
|
||||
type: 'application/x-www-form-urlencoded'
|
||||
});
|
||||
|
||||
if (navigator.sendBeacon) {
|
||||
navigator.sendBeacon(vitalsUrl, blob);
|
||||
} else
|
||||
fetch(vitalsUrl, {
|
||||
body: blob,
|
||||
method: 'POST',
|
||||
credentials: 'omit',
|
||||
keepalive: true
|
||||
});
|
||||
}
|
||||
|
||||
export function reportWebVitals(metric: Metric) {
|
||||
try {
|
||||
sendToAnalytics(metric, createWebVitalsOptions());
|
||||
} catch (err) {
|
||||
console.error('[Analytics]', err);
|
||||
}
|
||||
}
|
||||
|
||||
function createWebVitalsOptions(): Options {
|
||||
const ctx = get(page);
|
||||
return {
|
||||
path: ctx.url.pathname,
|
||||
params: ctx.params,
|
||||
analyticsId: window.VERCEL_ANALYTICS_ID || ''
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
<svg width="220" height="221" viewBox="0 0 220 221" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<mask id="path-1-inside-1_5272_24665" fill="white">
|
||||
<path d="M0 1C0 0.447715 0.447715 0 1 0H32V32H0V1Z"/>
|
||||
</mask>
|
||||
<path d="M-1 1C-1 -0.104569 -0.104569 -1 1 -1H32V1H1H-1ZM32 32H0H32ZM-1 32V1C-1 -0.104569 -0.104569 -1 1 -1V1V32H-1ZM32 0V32V0Z" fill="#C3C3C6" mask="url(#path-1-inside-1_5272_24665)"/>
|
||||
<mask id="path-3-inside-2_5272_24665" fill="white">
|
||||
<path d="M219 -4.37114e-08C219.552 -1.95703e-08 220 0.447715 220 1L220 32L188 32L188 -1.39876e-06L219 -4.37114e-08Z"/>
|
||||
</mask>
|
||||
<path d="M219 -1C220.105 -1 221 -0.104569 221 1L221 32L219 32L219 1L219 -1ZM188 32L188 -1.39876e-06L188 32ZM188 -1L219 -1C220.105 -1 221 -0.104569 221 1L219 1L188 0.999999L188 -1ZM220 32L188 32L220 32Z" fill="#C3C3C6" mask="url(#path-3-inside-2_5272_24665)"/>
|
||||
<mask id="path-5-inside-3_5272_24665" fill="white">
|
||||
<path d="M1 221C0.447715 221 -1.95703e-08 220.552 -4.37114e-08 220L-1.39876e-06 189L32 189L32 221L1 221Z"/>
|
||||
</mask>
|
||||
<path d="M1 222C-0.104569 222 -1 221.105 -1 220L-1 189L0.999999 189L1 220L1 222ZM32 189L32 221L32 189ZM32 222L1 222C-0.104569 222 -1 221.105 -1 220L1 220L32 220L32 222ZM-1.39876e-06 189L32 189L-1.39876e-06 189Z" fill="#C3C3C6" mask="url(#path-5-inside-3_5272_24665)"/>
|
||||
<mask id="path-7-inside-4_5272_24665" fill="white">
|
||||
<path d="M220 220C220 220.552 219.552 221 219 221L188 221L188 189L220 189L220 220Z"/>
|
||||
</mask>
|
||||
<path d="M221 220C221 221.105 220.105 222 219 222L188 222L188 220L219 220L221 220ZM188 189L220 189L188 189ZM221 189L221 220C221 221.105 220.105 222 219 222L219 220L219 189L221 189ZM188 221L188 189L188 221Z" fill="#C3C3C6" mask="url(#path-7-inside-4_5272_24665)"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.7 KiB |
@@ -0,0 +1,18 @@
|
||||
<svg width="220" height="221" viewBox="0 0 220 221" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<mask id="path-1-inside-1_5602_13938" fill="white">
|
||||
<path d="M0 1.00006C0 0.447776 0.447715 6.10352e-05 1 6.10352e-05H32V32.0001H0V1.00006Z"/>
|
||||
</mask>
|
||||
<path d="M-1 1.00006C-1 -0.104508 -0.104569 -0.999939 1 -0.999939H32V1.00006H1H-1ZM32 32.0001H0H32ZM-1 32.0001V1.00006C-1 -0.104508 -0.104569 -0.999939 1 -0.999939V1.00006V32.0001H-1ZM32 6.10352e-05V32.0001V6.10352e-05Z" fill="#C3C3C6" mask="url(#path-1-inside-1_5602_13938)"/>
|
||||
<mask id="path-3-inside-2_5602_13938" fill="white">
|
||||
<path d="M219 6.09914e-05C219.552 6.10156e-05 220 0.447776 220 1.00006L220 32.0001L188 32.0001L188 5.96364e-05L219 6.09914e-05Z"/>
|
||||
</mask>
|
||||
<path d="M219 -0.999939C220.105 -0.999939 221 -0.104508 221 1.00006L221 32.0001L219 32.0001L219 1.00006L219 -0.999939ZM188 32.0001L188 5.96364e-05L188 32.0001ZM188 -0.99994L219 -0.999939C220.105 -0.999939 221 -0.104508 221 1.00006L219 1.00006L188 1.00006L188 -0.99994ZM220 32.0001L188 32.0001L220 32.0001Z" fill="#C3C3C6" mask="url(#path-3-inside-2_5602_13938)"/>
|
||||
<mask id="path-5-inside-3_5602_13938" fill="white">
|
||||
<path d="M1 221C0.447715 221 -1.95703e-08 220.552 -4.37114e-08 220L-1.39876e-06 189L32 189L32 221L1 221Z"/>
|
||||
</mask>
|
||||
<path d="M1 222C-0.104569 222 -1 221.105 -1 220L-1 189L0.999999 189L1 220L1 222ZM32 189L32 221L32 189ZM32 222L1 222C-0.104569 222 -1 221.105 -1 220L1 220L32 220L32 222ZM-1.39876e-06 189L32 189L-1.39876e-06 189Z" fill="#C3C3C6" mask="url(#path-5-inside-3_5602_13938)"/>
|
||||
<mask id="path-7-inside-4_5602_13938" fill="white">
|
||||
<path d="M220 220C220 220.552 219.552 221 219 221L188 221L188 189L220 189L220 220Z"/>
|
||||
</mask>
|
||||
<path d="M221 220C221 221.105 220.105 222 219 222L188 222L188 220L219 220L221 220ZM188 189L220 189L188 189ZM221 189L221 220C221 221.105 220.105 222 219 222L219 220L219 189L221 189ZM188 221L188 189L188 221Z" fill="#C3C3C6" mask="url(#path-7-inside-4_5602_13938)"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.9 KiB |
@@ -1,23 +1,24 @@
|
||||
<script lang="ts">
|
||||
import {
|
||||
tierToPlan,
|
||||
getServiceLimit,
|
||||
type PlanServices,
|
||||
showUsageRatesModal,
|
||||
checkForUsageFees,
|
||||
readOnly,
|
||||
checkForProjectLimitation
|
||||
} from '$lib/stores/billing';
|
||||
import { Alert, DropList, Heading } from '$lib/components';
|
||||
import { Pill } from '$lib/elements';
|
||||
import { organization } from '$lib/stores/organization';
|
||||
import { GRACE_PERIOD_OVERRIDE, isCloud } from '$lib/system';
|
||||
import { createEventDispatcher, onMount } from 'svelte';
|
||||
import { wizard } from '$lib/stores/wizard';
|
||||
import ChangeOrganizationTierCloud from '$routes/console/changeOrganizationTierCloud.svelte';
|
||||
import { ContainerButton } from '.';
|
||||
import { Button } from '$lib/elements/forms';
|
||||
import { BillingPlan } from '$lib/constants';
|
||||
import { Pill } from '$lib/elements';
|
||||
import { Button } from '$lib/elements/forms';
|
||||
import {
|
||||
checkForProjectLimitation,
|
||||
checkForUsageFees,
|
||||
getServiceLimit,
|
||||
readOnly,
|
||||
showUsageRatesModal,
|
||||
tierToPlan,
|
||||
type PlanServices
|
||||
} from '$lib/stores/billing';
|
||||
import { organization } from '$lib/stores/organization';
|
||||
import { wizard } from '$lib/stores/wizard';
|
||||
import { GRACE_PERIOD_OVERRIDE, isCloud } from '$lib/system';
|
||||
import ChangeOrganizationTierCloud from '$routes/console/changeOrganizationTierCloud.svelte';
|
||||
import { createEventDispatcher, onMount } from 'svelte';
|
||||
import { ContainerButton } from '.';
|
||||
import { trackEvent } from '$lib/actions/analytics';
|
||||
|
||||
export let isFlex = true;
|
||||
export let title: string;
|
||||
@@ -35,7 +36,14 @@
|
||||
|
||||
let showDropdown = false;
|
||||
|
||||
const { bandwidth, documents, storage, users, executions } = $organization?.billingLimits ?? {};
|
||||
// TODO: remove the default billing limits when backend is updated with billing code
|
||||
const { bandwidth, documents, storage, users, executions } = $organization?.billingLimits ?? {
|
||||
bandwidth: 1,
|
||||
documents: 1,
|
||||
storage: 1,
|
||||
users: 1,
|
||||
executions: 1
|
||||
};
|
||||
const limitedServices = [
|
||||
{ name: 'bandwidth', value: bandwidth },
|
||||
{ name: 'documents', value: documents },
|
||||
@@ -92,8 +100,12 @@
|
||||
<span class="text">
|
||||
You've reached the {services} limit for the {tier} plan. <Button
|
||||
link
|
||||
on:click={upgradeMethod}>Upgrade</Button> your organization for additional
|
||||
resources.
|
||||
on:click={upgradeMethod}
|
||||
on:click={() =>
|
||||
trackEvent('click_organization_upgrade', {
|
||||
from: 'button',
|
||||
source: 'inline_alert'
|
||||
})}>Upgrade</Button> your organization for additional resources.
|
||||
</span>
|
||||
</Alert>
|
||||
{/if}
|
||||
@@ -108,24 +120,28 @@
|
||||
<DropList bind:show={showDropdown} width="16">
|
||||
{#if hasProjectLimitation}
|
||||
<Pill button on:click={() => (showDropdown = !showDropdown)}>
|
||||
<span class="icon-info" />{total}/{limit}
|
||||
{title} used
|
||||
<span class="icon-info" />{total}/{limit} created
|
||||
</Pill>
|
||||
{:else}
|
||||
<Pill button on:click={() => (showDropdown = !showDropdown)}>
|
||||
<span class="icon-info" />{title} limited
|
||||
<span class="icon-info" />Limits applied
|
||||
</Pill>
|
||||
{/if}
|
||||
<svelte:fragment slot="list">
|
||||
<slot name="tooltip" {limit} {tier} {title} {upgradeMethod} {hasUsageFees}>
|
||||
{#if hasProjectLimitation}
|
||||
<p class="text">
|
||||
Your are limited to {limit}
|
||||
You are limited to {limit}
|
||||
{title.toLocaleLowerCase()} per project on the {tier} plan.
|
||||
{#if $organization?.billingPlan === BillingPlan.STARTER}<Button
|
||||
link
|
||||
on:click={upgradeMethod}>Upgrade</Button>
|
||||
for addtional {title.toLocaleLowerCase()}.
|
||||
on:click={upgradeMethod}
|
||||
on:click={() =>
|
||||
trackEvent('click_organization_upgrade', {
|
||||
from: 'button',
|
||||
source: 'resource_limit_tag'
|
||||
})}>Upgrade</Button>
|
||||
for additional {title.toLocaleLowerCase()}.
|
||||
{/if}
|
||||
</p>
|
||||
{:else if hasUsageFees}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
<script>
|
||||
import { settings } from '$lib/components/consent.svelte';
|
||||
import { clickOnEnter } from '$lib/helpers/a11y';
|
||||
import { isCloud } from '$lib/system';
|
||||
import { version } from '$routes/console/store';
|
||||
|
||||
@@ -39,6 +41,18 @@
|
||||
<span class="text">Privacy</span>
|
||||
</a>
|
||||
</li>
|
||||
{#if isCloud}
|
||||
<li class="inline-links-item">
|
||||
<span
|
||||
style:cursor="pointer"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
on:keyup={clickOnEnter}
|
||||
on:click={() => settings.set(true)}>
|
||||
<span class="text">Cookies</span>
|
||||
</span>
|
||||
</li>
|
||||
{/if}
|
||||
</ul>
|
||||
</div>
|
||||
<div class="main-footer-end">
|
||||
|
||||
@@ -121,7 +121,13 @@
|
||||
{#if isCloud && $organization?.billingPlan === BillingPlan.STARTER && !$page.url.pathname.startsWith('/console/account')}
|
||||
<Button
|
||||
disabled={$organization?.markedForDeletion}
|
||||
on:click={() => wizard.start(ChangeOrganizationTierCloud)}>
|
||||
on:click={() => {
|
||||
wizard.start(ChangeOrganizationTierCloud);
|
||||
trackEvent('click_organization_upgrade', {
|
||||
from: 'button',
|
||||
source: 'top_nav'
|
||||
});
|
||||
}}>
|
||||
Upgrade
|
||||
</Button>
|
||||
{/if}
|
||||
|
||||
@@ -124,6 +124,23 @@
|
||||
<span class="text">Functions</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="drop-list-item">
|
||||
<a
|
||||
class="drop-button"
|
||||
class:is-selected={$page.url.pathname.startsWith(
|
||||
`${projectPath}/messaging`
|
||||
)}
|
||||
on:click={() => trackEvent('click_menu_messaging')}
|
||||
href={`${projectPath}/messaging`}
|
||||
use:tooltip={{
|
||||
content: 'Messaging',
|
||||
placement: 'right',
|
||||
disabled: !narrow
|
||||
}}>
|
||||
<span class="icon-send" aria-hidden="true" />
|
||||
<span class="text">Messaging</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="drop-list-item">
|
||||
<a
|
||||
class="drop-button"
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
class={icon ? `icon-${icon}` : ''}
|
||||
aria-hidden="true" />
|
||||
</div>
|
||||
<div class="alert-sticky-content">
|
||||
<div class="alert-sticky-content" data-private>
|
||||
{#if title}
|
||||
<h4 class="alert-sticky-title">{title}</h4>
|
||||
{/if}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
import { page } from '$app/stores';
|
||||
import { log } from '$lib/stores/logs';
|
||||
import { wizard } from '$lib/stores/wizard';
|
||||
import { activeHeaderAlert } from '$routes/console/store';
|
||||
|
||||
export let isOpen = false;
|
||||
export let showSideNavigation = false;
|
||||
@@ -47,9 +48,9 @@
|
||||
class:grid-with-side={showSideNavigation}
|
||||
class:is-open={isOpen}
|
||||
class:u-hide={$wizard.show || $log.show || $wizard.cover}
|
||||
class:is-fixed-layout={$$slots.alert}>
|
||||
{#if $$slots.alert}
|
||||
<slot name="alert" />
|
||||
class:is-fixed-layout={$activeHeaderAlert?.show}>
|
||||
{#if $activeHeaderAlert?.show}
|
||||
<svelte:component this={$activeHeaderAlert.component} />
|
||||
{/if}
|
||||
<header class="main-header u-padding-inline-end-0">
|
||||
<button
|
||||
|
||||
@@ -11,98 +11,27 @@
|
||||
</script>
|
||||
|
||||
<main class="grid-1-1 is-full-page" id="main">
|
||||
<section class="u-flex u-flex-vertical side-bg">
|
||||
<div class="logo u-flex u-gap-16 u-margin-inline-auto is-not-mobile">
|
||||
<a href={$user ? '/console' : '/'}>
|
||||
<section
|
||||
class="u-flex u-flex-vertical"
|
||||
style:--url={`url(${$app.themeInUse === 'dark' ? imgDark : imgLight})`}>
|
||||
<div class="logo u-flex u-gap-16">
|
||||
<a href={user ? '/console' : '/'}>
|
||||
{#if $app.themeInUse === 'dark'}
|
||||
<img
|
||||
src={AppwriteLogoDark}
|
||||
width="123"
|
||||
width="160"
|
||||
class="u-block u-only-dark"
|
||||
alt="Appwrite Logo" />
|
||||
{:else}
|
||||
<img
|
||||
src={AppwriteLogoLight}
|
||||
width="123"
|
||||
width="160"
|
||||
class="u-block u-only-light"
|
||||
alt="Appwrite Logo" />
|
||||
{/if}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="appwrite-pro">
|
||||
<span class="text">APPWRITE</span>
|
||||
<span class="appwrite-pro-text">
|
||||
<span class="appwrite-pro-text-letter">P</span><span
|
||||
class="appwrite-pro-text-letter">R</span
|
||||
><span class="appwrite-pro-text-letter">O</span></span>
|
||||
</div>
|
||||
|
||||
<div class="now-available">Now available</div>
|
||||
</section>
|
||||
<section class="grid-1-1-col-2 u-flex u-main-center u-cross-center _u-padding-16-mobile">
|
||||
<div class="container u-flex u-flex-vertical u-cross-center u-main-center">
|
||||
<div class="u-max-width-500 u-width-full-line">
|
||||
<h1 class="heading-level-2 u-margin-block-start-auto">
|
||||
<slot name="title" />
|
||||
</h1>
|
||||
<div class="u-margin-block-start-24">
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
<ul class="inline-links is-center is-with-sep u-margin-block-start-32">
|
||||
<slot name="links" />
|
||||
</ul>
|
||||
</div>
|
||||
<div
|
||||
class="logo u-flex u-gap-16 u-margin-inline-auto is-only-mobile u-margin-block-start-32">
|
||||
<a href={user ? '/console' : '/'}>
|
||||
{#if $app.themeInUse === 'dark'}
|
||||
<img
|
||||
src={AppwriteLogoDark}
|
||||
width="93"
|
||||
class="u-block u-only-dark"
|
||||
alt="Appwrite Logo" />
|
||||
{:else}
|
||||
<img
|
||||
src={AppwriteLogoLight}
|
||||
width="93"
|
||||
class="u-block u-only-light"
|
||||
alt="Appwrite Logo" />
|
||||
{/if}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<!-- OLD one -->
|
||||
<!--
|
||||
<main class="grid-1-1 is-full-page" id="main">
|
||||
<section
|
||||
class="u-flex u-flex-vertical"
|
||||
style:--url={`url(${$app.themeInUse === 'dark' ? imgDark : imgLight})`}>
|
||||
<div class="logo u-flex u-gap-16">
|
||||
<a href={user ? '/console' : '/'}>
|
||||
{#if $app.themeInUse === 'dark'}
|
||||
<img
|
||||
src={AppwriteLogoDark}
|
||||
width="160"
|
||||
class="u-block u-only-dark"
|
||||
alt="Appwrite Logo" />
|
||||
{:else}
|
||||
<img
|
||||
src={AppwriteLogoLight}
|
||||
width="160"
|
||||
class="u-block u-only-light"
|
||||
alt="Appwrite Logo" />
|
||||
{/if}
|
||||
</a>
|
||||
{#if isCloud}
|
||||
<span class="aw-badges aw-eyebrow">Cloud Beta</span>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="u-flex u-stretch" />
|
||||
|
||||
<div class="tag-line is-not-mobile">
|
||||
@@ -126,254 +55,10 @@
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
<style lang="scss">
|
||||
@import '@appwrite.io/pink/src/abstract/variables/_devices.scss';
|
||||
|
||||
/* Default (including mobile) */
|
||||
#main section:first-child {
|
||||
padding-block-start: 2.25rem;
|
||||
padding-block-end: 2rem;
|
||||
|
||||
div {
|
||||
padding-inline-start: 1rem;
|
||||
padding-inline-end: 1rem;
|
||||
}
|
||||
|
||||
.tag-line {
|
||||
font-family: 'Aeonik Pro';
|
||||
font-size: 4rem;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 100%; /* 80px */
|
||||
letter-spacing: -1.6px;
|
||||
backdrop-filter: blur(0.5 rem);
|
||||
.underscore {
|
||||
-webkit-text-fill-color: #f02e65;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* for smaller screens */
|
||||
@media #{$break2open} {
|
||||
#main section:first-child {
|
||||
background: var(--url);
|
||||
background-repeat: no-repeat;
|
||||
background-position: top;
|
||||
background-size: cover;
|
||||
|
||||
padding-block-start: 6.25rem;
|
||||
padding-block-end: 6.875rem;
|
||||
|
||||
div {
|
||||
padding-inline-start: 2.625rem;
|
||||
padding-inline-end: 2rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* for larger screens */
|
||||
@media #{$break3open} {
|
||||
#main section:first-child {
|
||||
div {
|
||||
padding-inline-start: 5.625rem;
|
||||
padding-inline-end: 5rem;
|
||||
}
|
||||
.tag-line {
|
||||
font-size: 5rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:global(.theme-dark) .tag-line {
|
||||
background: linear-gradient(45deg, white, white 60%, #fd376f);
|
||||
background-clip: text;
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
}
|
||||
|
||||
:global(.theme-light) .tag-line {
|
||||
background: linear-gradient(45deg, #19191d, #19191d 60%, #fd376f);
|
||||
background-clip: text;
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
}
|
||||
|
||||
.aw-eyebrow {
|
||||
font-family: 'Source Code Pro', monospace;
|
||||
line-height: 0.9rem;
|
||||
font-size: 0.78rem;
|
||||
letter-spacing: 0.08em;
|
||||
font-weight: 400;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.aw-badges {
|
||||
--p-badges-shadow-bg-color: #f2c8d6;
|
||||
--p-badges-shadow-border-color: #f69db7;
|
||||
--p-badges-shadowopacity: 0.4;
|
||||
align-self: center;
|
||||
color: hsl(var(--color-neutral-0));
|
||||
padding-block: 0.375rem;
|
||||
padding-inline: 0.75rem;
|
||||
border-radius: 0.375rem;
|
||||
background: radial-gradient(
|
||||
98.13% 199.7% at 98.13% 100%,
|
||||
#fe6947 0%,
|
||||
#fd366e 41.6%,
|
||||
#fd366e 100%
|
||||
);
|
||||
-webkit-backdrop-filter: blur(2.5rem);
|
||||
backdrop-filter: blur(2.5rem);
|
||||
box-shadow:
|
||||
0.1875rem 0.1875rem var(--p-badges-shadow-bg-color),
|
||||
0.25rem 0.1875rem var(--p-badges-shadow-border-color),
|
||||
0.1875rem 0.25rem var(--p-badges-shadow-border-color),
|
||||
0.125rem 0.1875rem var(--p-badges-shadow-border-color),
|
||||
0.1875rem 0.125rem var(--p-badges-shadow-border-color);
|
||||
|
||||
:global(.theme-dark) & {
|
||||
--p-badges-shadow-bg-color: #2c2c2f;
|
||||
--p-badges-shadow-border-color: #39393c;
|
||||
--p-badges-shadowopacity: 0.13;
|
||||
}
|
||||
|
||||
:global(.theme-light) & {
|
||||
--p-badges-shadow-bg-color: #f2c8d6;
|
||||
--p-badges-shadow-border-color: #f69db7;
|
||||
--p-badges-shadowopacity: 0.4;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
-->
|
||||
|
||||
<style lang="scss">
|
||||
@import '@appwrite.io/pink/src/abstract/variables/_common.scss';
|
||||
@import '@appwrite.io/pink/src/abstract/variables/_devices.scss';
|
||||
@import '@appwrite.io/pink/src/abstract/functions/_pxToRem.scss';
|
||||
|
||||
/* mobile utility class */
|
||||
@media #{$break1} {
|
||||
._u-padding-16-mobile {
|
||||
padding: pxToRem(16);
|
||||
}
|
||||
}
|
||||
|
||||
.appwrite-pro {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: baseline;
|
||||
color: hsl(var(--color-neutral-100));
|
||||
|
||||
@media #{$break1} {
|
||||
gap: pxToRem(10);
|
||||
font-size: pxToRem(18);
|
||||
letter-spacing: pxToRem(4);
|
||||
}
|
||||
@media #{$break2open} {
|
||||
gap: pxToRem(24);
|
||||
font-size: pxToRem(40);
|
||||
letter-spacing: pxToRem(8);
|
||||
line-height: 120%;
|
||||
margin-block-start: pxToRem(160);
|
||||
}
|
||||
|
||||
&-text {
|
||||
padding: pxToRem(18) pxToRem(28);
|
||||
border: pxToRem(2) solid hsl(343 98% 60% / 0.2);
|
||||
border-radius: pxToRem(16);
|
||||
background: rgba(253, 54, 110, 0.1);
|
||||
box-shadow: 0 -12.173px 20.289px 0px rgba(253, 54, 110, 0.08) inset;
|
||||
text-align: center;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
&-letter {
|
||||
width: 2rem;
|
||||
@media #{$break1} {
|
||||
width: 1rem;
|
||||
}
|
||||
}
|
||||
@media #{$break1} {
|
||||
padding: pxToRem(8) pxToRem(12);
|
||||
border-radius: pxToRem(8);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:global(.theme-dark) .appwrite-pro {
|
||||
color: hsl(var(--color-neutral-10));
|
||||
}
|
||||
|
||||
.now-available {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
background: linear-gradient(70deg, #fb5491 -35.72%, #19191d 79.96%);
|
||||
background-clip: text;
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
margin-inline: auto;
|
||||
|
||||
@media #{$break1} {
|
||||
font-size: pxToRem(18);
|
||||
margin-block-start: pxToRem(12);
|
||||
}
|
||||
@media #{$break2open} {
|
||||
font-size: pxToRem(30);
|
||||
margin-block-start: pxToRem(36);
|
||||
}
|
||||
}
|
||||
:global(.theme-dark) .now-available {
|
||||
background: linear-gradient(89deg, #fb5491 -29.25%, #fff 43.27%);
|
||||
background-clip: text;
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
}
|
||||
|
||||
.side-bg {
|
||||
position: relative;
|
||||
background-color: #ededf0;
|
||||
overflow: hidden;
|
||||
}
|
||||
.side-bg::before {
|
||||
position: absolute;
|
||||
inset-block-start: -950px;
|
||||
inset-inline-end: -650px;
|
||||
content: '';
|
||||
display: block;
|
||||
inline-size: 100%;
|
||||
block-size: 100%;
|
||||
background: radial-gradient(49.55% 43.54% at 47% 50.69%, #e7f8f7 0%, #85dbd8 100%);
|
||||
filter: blur(250px);
|
||||
@media #{$break1} {
|
||||
inset-block-start: -200px;
|
||||
inset-inline-end: -400px;
|
||||
filter: blur(100px);
|
||||
}
|
||||
}
|
||||
.side-bg::after {
|
||||
position: absolute;
|
||||
inset-block-end: -850px;
|
||||
inset-inline-start: -600px;
|
||||
content: '';
|
||||
display: block;
|
||||
inline-size: 100%;
|
||||
block-size: 100%;
|
||||
background: radial-gradient(50% 46.73% at 50% 53.27%, #fe9567 28.17%, #fd366e 59.38%);
|
||||
filter: blur(250px);
|
||||
|
||||
@media #{$break1} {
|
||||
inset-block-end: -200px;
|
||||
inset-inline-start: -400px;
|
||||
filter: blur(100px);
|
||||
}
|
||||
}
|
||||
:global(.theme-dark) .side-bg {
|
||||
background-color: #19191d;
|
||||
}
|
||||
|
||||
/****** OLD ******/
|
||||
/* Default (including mobile) */
|
||||
#main section:first-child {
|
||||
padding-block-start: 2.25rem;
|
||||
@@ -384,23 +69,28 @@
|
||||
padding-inline-end: 1rem;
|
||||
}
|
||||
|
||||
// .tag-line {
|
||||
// font-family: 'Aeonik Pro';
|
||||
// font-size: 4rem;
|
||||
// font-style: normal;
|
||||
// font-weight: 400;
|
||||
// line-height: 100%; /* 80px */
|
||||
// letter-spacing: -1.6px;
|
||||
// backdrop-filter: blur(0.5 rem);
|
||||
// .underscore {
|
||||
// -webkit-text-fill-color: #f02e65;
|
||||
// }
|
||||
// }
|
||||
.tag-line {
|
||||
font-family: 'Aeonik Pro';
|
||||
font-size: 4rem;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 100%; /* 80px */
|
||||
letter-spacing: -1.6px;
|
||||
backdrop-filter: blur(0.5 rem);
|
||||
.underscore {
|
||||
-webkit-text-fill-color: #f02e65;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* for smaller screens */
|
||||
@media #{$break2open} {
|
||||
#main section:first-child {
|
||||
background: var(--url);
|
||||
background-repeat: no-repeat;
|
||||
background-position: top;
|
||||
background-size: cover;
|
||||
|
||||
padding-block-start: 6.25rem;
|
||||
padding-block-end: 6.875rem;
|
||||
|
||||
@@ -418,9 +108,23 @@
|
||||
padding-inline-start: 5.625rem;
|
||||
padding-inline-end: 5rem;
|
||||
}
|
||||
// .tag-line {
|
||||
// font-size: 5rem;
|
||||
// }
|
||||
.tag-line {
|
||||
font-size: 5rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:global(.theme-dark) .tag-line {
|
||||
background: linear-gradient(45deg, white, white 60%, #fd376f);
|
||||
background-clip: text;
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
}
|
||||
|
||||
:global(.theme-light) .tag-line {
|
||||
background: linear-gradient(45deg, #19191d, #19191d 60%, #fd376f);
|
||||
background-clip: text;
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
export function periodToDates(period: UsagePeriods): {
|
||||
start: string;
|
||||
end: string;
|
||||
period: '1h' | '1d';
|
||||
period: ProjectUsageRange;
|
||||
} {
|
||||
const start = new Date();
|
||||
switch (period) {
|
||||
@@ -26,7 +26,7 @@
|
||||
return {
|
||||
start: start.toISOString(),
|
||||
end: end.toISOString(),
|
||||
period: period === '24h' ? '1h' : '1d'
|
||||
period: period === '24h' ? ProjectUsageRange.OneHour : ProjectUsageRange.OneDay
|
||||
};
|
||||
}
|
||||
|
||||
@@ -45,8 +45,8 @@
|
||||
): Array<[string, number]> {
|
||||
return metrics.reduceRight(
|
||||
(acc, curr) => {
|
||||
acc.total -= curr.value;
|
||||
acc.data.unshift([curr.date, acc.total]);
|
||||
acc.total -= curr.value;
|
||||
return acc;
|
||||
},
|
||||
{
|
||||
@@ -62,7 +62,7 @@
|
||||
import { BarChart } from '$lib/charts';
|
||||
import { formatNumberWithCommas } from '$lib/helpers/numbers';
|
||||
import { Card, SecondaryTabs, SecondaryTabsItem, Heading } from '$lib/components';
|
||||
import type { Models } from '@appwrite.io/console';
|
||||
import { ProjectUsageRange, type Models } from '@appwrite.io/console';
|
||||
import { page } from '$app/stores';
|
||||
|
||||
type MetricMetadata = {
|
||||
|
||||
@@ -6,6 +6,10 @@
|
||||
component: typeof SvelteComponent<unknown>;
|
||||
optional?: boolean;
|
||||
disabled?: boolean;
|
||||
actions?: {
|
||||
label: string;
|
||||
onClick: () => Promise<void>;
|
||||
}[];
|
||||
}
|
||||
>;
|
||||
</script>
|
||||
@@ -128,6 +132,7 @@
|
||||
|
||||
$: sortedSteps = [...steps].sort(([a], [b]) => (a > b ? 1 : -1));
|
||||
$: isLastStep = $wizard.step === steps.size;
|
||||
$: currentStep = steps.get($wizard.step);
|
||||
</script>
|
||||
|
||||
<svelte:window on:keydown={handleKeydown} />
|
||||
@@ -176,7 +181,7 @@
|
||||
{/each}
|
||||
<div class="form-footer">
|
||||
<div class="u-flex u-main-end u-gap-12">
|
||||
{#if !isLastStep && sortedSteps[$wizard.step - 1]?.[1]?.optional}
|
||||
{#if !isLastStep && currentStep?.optional}
|
||||
<Button text on:click={() => dispatch('finish')}>
|
||||
Skip optional steps
|
||||
</Button>
|
||||
@@ -188,6 +193,13 @@
|
||||
<Button secondary on:click={previousStep}>Back</Button>
|
||||
{/if}
|
||||
|
||||
{#if currentStep?.actions}
|
||||
{#each currentStep.actions as action}
|
||||
<Button secondary on:click={action.onClick}>
|
||||
{action.label}</Button>
|
||||
{/each}
|
||||
{/if}
|
||||
|
||||
<Button submit disabled={$wizard.nextDisabled}>
|
||||
{isLastStep ? finalAction : 'Next'}
|
||||
</Button>
|
||||
|
||||
@@ -1,10 +1,3 @@
|
||||
<script context="module" lang="ts">
|
||||
export enum ProxyTypes {
|
||||
API = 'api',
|
||||
FUNCTION = 'function'
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { DropList, DropListItem, Empty, Heading, Modal, Trim } from '$lib/components';
|
||||
import {
|
||||
@@ -22,14 +15,14 @@
|
||||
import { toLocaleDate } from '$lib/helpers/date';
|
||||
import { wizard } from '$lib/stores/wizard';
|
||||
import type { Dependencies } from '$lib/constants';
|
||||
import type { Models } from '@appwrite.io/console';
|
||||
import type { Models, ResourceType } from '@appwrite.io/console';
|
||||
import Create from './create.svelte';
|
||||
import Delete from './delete.svelte';
|
||||
import Retry from './wizard/retry.svelte';
|
||||
import { Pill } from '$lib/elements';
|
||||
|
||||
export let rules: Models.ProxyRuleList;
|
||||
export let type: ProxyTypes;
|
||||
export let type: ResourceType;
|
||||
export let dependency: Dependencies;
|
||||
|
||||
let showDomainsDropdown = [];
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
export { default as ProxyRulesPage, ProxyTypes } from './index.svelte';
|
||||
export { default as ProxyRulesPage } from './index.svelte';
|
||||
export { default as Retry } from './wizard/retry.svelte';
|
||||
|
||||
@@ -6,9 +6,9 @@
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { isSelfHosted } from '$lib/system';
|
||||
import { func } from '$routes/console/project-[project]/functions/function-[function]/store';
|
||||
import { ProxyTypes } from '../index.svelte';
|
||||
import { domain, typeStore } from './store';
|
||||
import { consoleVariables } from '$routes/console/store';
|
||||
import { ResourceType } from '@appwrite.io/console';
|
||||
|
||||
let error = null;
|
||||
const isDomainsEnabled = $consoleVariables?._APP_DOMAIN_ENABLED === true;
|
||||
@@ -22,7 +22,7 @@
|
||||
$domain = await sdk.forProject.proxy.createRule(
|
||||
$domain.domain,
|
||||
$typeStore,
|
||||
$typeStore === ProxyTypes.FUNCTION ? $func.$id : undefined
|
||||
$typeStore === ResourceType.Function ? $func.$id : undefined
|
||||
);
|
||||
|
||||
trackEvent(Submit.DomainCreate);
|
||||
@@ -34,7 +34,7 @@
|
||||
</script>
|
||||
|
||||
<WizardStep beforeSubmit={createDomain}>
|
||||
<svelte:fragment slot="title">Add function domain</svelte:fragment>
|
||||
<svelte:fragment slot="title">Domain</svelte:fragment>
|
||||
<svelte:fragment slot="subtitle">
|
||||
Use your self-owned domain as the endpoint of your Appwrite API. <a
|
||||
href="https://appwrite.io/docs/advanced/platform/custom-domains"
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import type { Models } from '@appwrite.io/console';
|
||||
import type { Models, ResourceType } from '@appwrite.io/console';
|
||||
import { writable } from 'svelte/store';
|
||||
import type { ProxyTypes } from '../index.svelte';
|
||||
import type { Dependencies } from '$lib/constants';
|
||||
|
||||
export const domain = writable<Partial<Models.ProxyRule>>({ $id: '', domain: '' });
|
||||
export const typeStore = writable<ProxyTypes>();
|
||||
export const typeStore = writable<ResourceType>();
|
||||
export const dependencyStore = writable<Dependencies>();
|
||||
|
||||
+63
-4
@@ -1,5 +1,5 @@
|
||||
import type { Client, Models, Query } from '@appwrite.io/console';
|
||||
import type { Organization } from '../stores/organization';
|
||||
import type { Client, Models } from '@appwrite.io/console';
|
||||
import type { Organization, OrganizationList } from '../stores/organization';
|
||||
import type { PaymentMethod } from '@stripe/stripe-js';
|
||||
import type { Tier } from '$lib/stores/billing';
|
||||
|
||||
@@ -18,6 +18,7 @@ export type PaymentMethodData = {
|
||||
clientSecret: string;
|
||||
failed: boolean;
|
||||
name: string;
|
||||
mandateId?: string;
|
||||
};
|
||||
|
||||
export type PaymentList = {
|
||||
@@ -274,6 +275,22 @@ export class Billing {
|
||||
this.client = client;
|
||||
}
|
||||
|
||||
async listOrganization(queries: string[] = []): Promise<OrganizationList> {
|
||||
const path = `/organizations`;
|
||||
const params = {
|
||||
queries
|
||||
};
|
||||
const uri = new URL(this.client.config.endpoint + path);
|
||||
return await this.client.call(
|
||||
'GET',
|
||||
uri,
|
||||
{
|
||||
'content-type': 'application/json'
|
||||
},
|
||||
params
|
||||
);
|
||||
}
|
||||
|
||||
async createOrganization(
|
||||
organizationId: string,
|
||||
name: string,
|
||||
@@ -448,7 +465,7 @@ export class Billing {
|
||||
);
|
||||
}
|
||||
|
||||
async listInvoices(organizationId: string, queries: Query[] = []): Promise<InvoiceList> {
|
||||
async listInvoices(organizationId: string, queries: string[] = []): Promise<InvoiceList> {
|
||||
const path = `/organizations/${organizationId}/invoices`;
|
||||
const params = {
|
||||
organizationId,
|
||||
@@ -523,6 +540,28 @@ export class Billing {
|
||||
);
|
||||
}
|
||||
|
||||
async retryPayment(
|
||||
organizationId: string,
|
||||
invoiceId: string,
|
||||
paymentMethodId: string
|
||||
): Promise<Invoice> {
|
||||
const path = `/organizations/${organizationId}/invoices/${invoiceId}/payments`;
|
||||
const params = {
|
||||
organizationId,
|
||||
invoiceId,
|
||||
paymentMethodId
|
||||
};
|
||||
const uri = new URL(this.client.config.endpoint + path);
|
||||
return await this.client.call(
|
||||
'post',
|
||||
uri,
|
||||
{
|
||||
'content-type': 'application/json'
|
||||
},
|
||||
params
|
||||
);
|
||||
}
|
||||
|
||||
async listUsage(
|
||||
organizationId: string,
|
||||
startDate: string = undefined,
|
||||
@@ -827,7 +866,27 @@ export class Billing {
|
||||
);
|
||||
}
|
||||
|
||||
async listAddresses(queries: Query[] = []): Promise<AddressesList> {
|
||||
async setupPaymentMandate(
|
||||
organizationId: string,
|
||||
paymentMethodId: string
|
||||
): Promise<PaymentMethodData> {
|
||||
const path = `/account/payment-methods/${paymentMethodId}/setup`;
|
||||
const params = {
|
||||
organizationId,
|
||||
paymentMethodId
|
||||
};
|
||||
const uri = new URL(this.client.config.endpoint + path);
|
||||
return await this.client.call(
|
||||
'post',
|
||||
uri,
|
||||
{
|
||||
'content-type': 'application/json'
|
||||
},
|
||||
params
|
||||
);
|
||||
}
|
||||
|
||||
async listAddresses(queries: string[] = []): Promise<AddressesList> {
|
||||
const path = `/account/billing-addresses`;
|
||||
const params = {
|
||||
queries
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { writable } from 'svelte/store';
|
||||
import type { Models } from '@appwrite.io/console';
|
||||
import { type Models, AuthMethod as AuthMethodEnum } from '@appwrite.io/console';
|
||||
|
||||
export type AuthMethod = {
|
||||
label: string;
|
||||
method: string;
|
||||
method: AuthMethodEnum;
|
||||
value: boolean | null;
|
||||
};
|
||||
|
||||
@@ -11,32 +11,37 @@ const setAuthMethod = (project: Models.Project): AuthMethod[] => {
|
||||
return [
|
||||
{
|
||||
label: 'Email/Password',
|
||||
method: 'email-password',
|
||||
method: AuthMethodEnum.Emailpassword,
|
||||
value: project?.authEmailPassword
|
||||
},
|
||||
{
|
||||
label: 'Phone',
|
||||
method: 'phone',
|
||||
method: AuthMethodEnum.Phone,
|
||||
value: project?.authPhone
|
||||
},
|
||||
{
|
||||
label: 'Magic URL',
|
||||
method: 'magic-url',
|
||||
method: AuthMethodEnum.Magicurl,
|
||||
value: project?.authUsersAuthMagicURL
|
||||
},
|
||||
{
|
||||
label: 'Email OTP',
|
||||
method: AuthMethodEnum.Emailotp,
|
||||
value: project?.authEmailOtp
|
||||
},
|
||||
{
|
||||
label: 'Anonymous',
|
||||
method: 'anonymous',
|
||||
method: AuthMethodEnum.Anonymous,
|
||||
value: project?.authAnonymous
|
||||
},
|
||||
{
|
||||
label: 'Team Invites',
|
||||
method: 'invites',
|
||||
method: AuthMethodEnum.Invites,
|
||||
value: project?.authInvites
|
||||
},
|
||||
{
|
||||
label: 'JWT',
|
||||
method: 'jwt',
|
||||
method: AuthMethodEnum.Jwt,
|
||||
value: project?.authJWT
|
||||
}
|
||||
];
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user