Merge branch 'appwrite:main' into bug-7362-file-extension-validation

This commit is contained in:
Sourab Pramanik
2024-03-11 18:41:45 +05:30
committed by GitHub
486 changed files with 15217 additions and 11347 deletions
+1 -1
View File
@@ -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=
+4 -9
View File
@@ -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
+4
View File
@@ -0,0 +1,4 @@
# Ignore files for PNPM, NPM and YARN
pnpm-lock.yaml
package-lock.json
yarn.lock
+1465 -1090
View File
File diff suppressed because it is too large Load Diff
+18 -19
View File
@@ -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"
}
-7647
View File
File diff suppressed because it is too large Load Diff
+3
View File
@@ -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"
+1 -3
View File
@@ -1,4 +1,2 @@
/// <reference types="@sveltejs/kit" />
interface Window {
VERCEL_ANALYTICS_ID: string | false;
}
interface Window {}
+13
View File
@@ -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
};
};
+21 -1
View File
@@ -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'
}
+26 -4
View File
@@ -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>
+6
View File
@@ -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
};
+9 -1
View File
@@ -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;
+25
View File
@@ -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>
+48 -6
View File
@@ -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>
+2 -1
View File
@@ -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>
+34 -13
View File
@@ -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>
+31 -4
View File
@@ -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>
+149
View File
@@ -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>
+54 -25
View File
@@ -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 -1
View File
@@ -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
+1 -1
View File
@@ -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"
+26
View File
@@ -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>
+1 -1
View File
@@ -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)
-->
+13 -11
View File
@@ -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}
+544
View File
@@ -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>
+8 -6
View File
@@ -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" />
+2 -1
View File
@@ -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}
+2 -1
View File
@@ -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';
+1 -1
View File
@@ -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}
+24
View File
@@ -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>
+2
View File
@@ -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';
+11
View File
@@ -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>
+16 -14
View File
@@ -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}
+4 -2
View File
@@ -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>
+20 -3
View File
@@ -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>
+12 -10
View File
@@ -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>
+4 -4
View File
@@ -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>
+2 -1
View File
@@ -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}
+12 -1
View File
@@ -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}
+9 -12
View File
@@ -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>
+15 -8
View File
@@ -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
View File
@@ -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',
+3
View File
@@ -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();
+3
View File
@@ -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}
+3
View File
@@ -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';
+25 -27
View File
@@ -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}>
+8
View File
@@ -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}
+66
View File
@@ -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>
+32 -4
View File
@@ -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 -->
&nbsp;<button
type="button"
on:click={() => (show = !show)}
class="tooltip"
aria-label="input tooltip">
<span
class="icon-info"
aria-hidden="true"
style="font-size: var(--icon-size-small)" />
</button>
<svelte:fragment slot="list">
<div
class="dropped card u-max-width-250"
style:--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">
+31 -3
View File
@@ -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 -->
&nbsp;<button
type="button"
on:click={() => (show = !show)}
class="tooltip"
aria-label="input tooltip">
<span
class="icon-info"
aria-hidden="true"
style="font-size: var(--icon-size-small)" />
</button>
<svelte:fragment slot="list">
<div
class="dropped card u-max-width-250"
style:--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
+29 -3
View File
@@ -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 -->
&nbsp;<button
type="button"
on:click={() => (show = !show)}
class="tooltip"
aria-label="input tooltip">
<span
class="icon-info"
aria-hidden="true"
style="font-size: var(--icon-size-small)" />
</button>
<svelte:fragment slot="list">
<div
class="dropped card u-max-width-250"
style:--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 -->
&nbsp;<button
type="button"
on:click={() => (show = !show)}
class="tooltip"
aria-label="input tooltip">
<span
class="icon-info"
aria-hidden="true"
style="font-size: var(--icon-size-small)" />
</button>
<svelte:fragment slot="list">
<div
class="dropped card u-max-width-250"
style="--p-card-padding: .75rem; 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}
+31 -3
View File
@@ -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 -->
&nbsp;<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' : ''}>
+31 -3
View File
@@ -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 -->
&nbsp;<button
type="button"
on:click={() => (show = !show)}
class="tooltip"
aria-label="input tooltip">
<span
class="icon-info"
aria-hidden="true"
style="font-size: var(--icon-size-small)" />
</button>
<svelte:fragment slot="list">
<div
class="dropped card u-max-width-250"
style:--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
+18
View File
@@ -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"
+2 -1
View File
@@ -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>
+29 -2
View File
@@ -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 -->
&nbsp;<button
type="button"
on:click={() => (show = !show)}
class="tooltip"
aria-label="input tooltip">
<span
class="icon-info"
aria-hidden="true"
style:font-size="var(--icon-size-small)" />
</button>
<svelte:fragment slot="list">
<div
class="dropped card u-max-width-250"
style:--p-card-padding=".75rem"
style:--card-border-radius="var(--border-radius-small)"
style:box-shadow="var(--shadow-large)">
<svelte:component this={popover} {...popoverProps} />
</div>
</svelte:fragment>
</Drop>
{/if}
</Label>
{/if}
+71
View File
@@ -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>
+10 -1
View File
@@ -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>
+2
View File
@@ -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>
+8 -2
View File
@@ -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 />
+32
View File
@@ -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() &&
+1 -1
View File
@@ -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,
+9
View File
@@ -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);
}
+7
View File
@@ -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;
}
+6 -2
View File
@@ -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 {
+14
View File
@@ -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);
}
+4 -4
View File
@@ -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 };
}
-77
View File
@@ -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 || ''
};
}
+18
View File
@@ -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

+18
View File
@@ -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

+42 -26
View File
@@ -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}
+14
View File
@@ -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">
+7 -1
View File
@@ -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}
+17
View File
@@ -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"
+1 -1
View File
@@ -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 -3
View File
@@ -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
+41 -337
View File
@@ -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 -4
View File
@@ -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 = {
+13 -1
View File
@@ -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>
+2 -9
View File
@@ -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 -1
View File
@@ -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';
+3 -3
View File
@@ -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"
+2 -3
View File
@@ -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
View File
@@ -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
+13 -8
View File
@@ -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