Merge remote-tracking branch 'origin/main' into feat-budget-nullable

This commit is contained in:
Damodar Lohani
2025-02-23 05:53:13 +00:00
25 changed files with 341 additions and 235 deletions
+2 -2
View File
@@ -179,7 +179,7 @@
{#if $isLoading || answer}
<div class="content">
<div class="u-flex u-gap-8 u-cross-center">
<div class="avatar is-size-x-small">{getInitials($user.name)}</div>
<div class="avatar is-size-x-small">{getInitials($user.name || $user.email)}</div>
<p class="u-opacity-75">{previousQuestion}</p>
</div>
<div class="u-flex u-gap-8 u-margin-block-start-24">
@@ -229,7 +229,7 @@
<div class="footer" slot="footer">
<div class="u-flex u-cross-center u-gap-4">
<AvatarInitials size={32} name={$user.name} />
<AvatarInitials size={32} name={$user.name || $user.email} />
<form
class="input-text-wrapper u-width-full-line"
style="--amount-of-buttons: 1;"
+3 -6
View File
@@ -12,13 +12,10 @@
export function getFlag(country: string, width: number, height: number, quality: number) {
if (!isValueOfStringEnum(Flag, country)) return '';
let flag = sdk.forConsole.avatars
return sdk.forConsole.avatars
.getFlag(country, width * 2, height * 2, quality)
?.toString();
flag?.includes('&project=')
? (flag = flag.replace('&project=', '&mode=admin'))
: flag + '&mode=admin';
return flag;
?.toString()
?.replace('&project=console', '&mode=admin');
}
</script>
+4 -10
View File
@@ -8,7 +8,7 @@
import type { Coupon } from '$lib/sdk/billing';
import { app } from '$lib/stores/app';
import type { Campaign } from '$lib/stores/campaigns';
import { getApiEndpoint } from '$lib/stores/sdk';
import { getCampaignImageUrl } from '$routes/(public)/card/helpers';
export const imgLight = LoginLight;
export const imgDark = LoginDark;
@@ -38,12 +38,6 @@
return campaign.description;
}
}
function getImage(image: string) {
const endpoint = getApiEndpoint();
const url = new URL(image, endpoint);
return url.toString();
}
</script>
<main class="grid-1-1 is-full-page" id="main">
@@ -86,7 +80,7 @@
<section
class="u-flex u-flex-vertical u-main-center u-cross-center u-height-100-percent u-width-full-line">
<img
src={getImage(campaign?.image[$app.themeInUse])}
src={getCampaignImageUrl(campaign?.image[$app.themeInUse])}
class="u-block u-image-object-fit-cover side-bg-img"
alt="promo" />
@@ -151,7 +145,7 @@
<div class="u-margin-block-start-16 u-flex u-gap-16">
{#if currentReview?.image}
<Avatar
src={getImage(currentReview?.image)}
src={getCampaignImageUrl(currentReview?.image)}
name={currentReview.name}
size={40} />
{:else}
@@ -170,7 +164,7 @@
<p class="u-bold" style:text-transform="uppercase">provided to you by</p>
<img
style:max-block-size="2.5rem"
src={getImage(campaign?.image[$app.themeInUse])}
src={getCampaignImageUrl(campaign?.image[$app.themeInUse])}
alt={coupon?.campaign ?? campaign.$id} />
</div>
{/if}
@@ -18,10 +18,10 @@
<svelte:fragment slot="aside">
<BoxAvatar>
<svelte:fragment slot="image">
<AvatarInitials size={48} name={$user.name} />
<AvatarInitials size={48} name={$user.name || $user.email} />
</svelte:fragment>
<svelte:fragment slot="title">
<span class="u-bold u-trim-1" data-private>{$user.name}</span>
<span class="u-bold u-trim-1" data-private>{$user.name || 'User'}</span>
</svelte:fragment>
</BoxAvatar>
</svelte:fragment>
@@ -12,7 +12,7 @@
let name: string = null;
onMount(async () => {
name ??= $user.name;
name ??= $user.name ?? '';
});
async function updateName() {
+222 -147
View File
@@ -1,28 +1,36 @@
<script lang="ts">
import { afterNavigate, goto } from '$app/navigation';
import { afterNavigate, goto, invalidate } from '$app/navigation';
import { base } from '$app/paths';
import { page } from '$app/stores';
import { Button, Form, FormList, InputSelect } from '$lib/elements/forms';
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
import { CreditsApplied, EstimatedTotal, SelectPaymentMethod } from '$lib/components/billing';
import { BillingPlan, Dependencies } from '$lib/constants';
import { Button, Form, FormList, InputSelect, InputTags, InputText } from '$lib/elements/forms';
import { toLocaleDate } from '$lib/helpers/date';
import {
WizardSecondaryContainer,
WizardSecondaryContent,
WizardSecondaryFooter
} from '$lib/layout';
import { type PaymentList } from '$lib/sdk/billing';
import { app } from '$lib/stores/app';
import { isOrganization } from '$lib/stores/billing.js';
import { addNotification } from '$lib/stores/notifications';
import { organizationList, type Organization } from '$lib/stores/organization';
import { BillingPlan, ID } from '@appwrite.io/console';
import {
organizationList,
type Organization,
type OrganizationError
} from '$lib/stores/organization';
import { sdk } from '$lib/stores/sdk';
import { confirmPayment } from '$lib/stores/stripe.js';
import { ID } from '@appwrite.io/console';
import { onMount } from 'svelte';
import { writable } from 'svelte/store';
import { CreditsApplied } from '$lib/components/billing';
import { sdk } from '$lib/stores/sdk';
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
import { Helper } from '$lib/elements/forms/index.js';
import { getCampaignImageUrl } from '$routes/(public)/card/helpers';
export let data;
const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,63}$/i;
let previousPage: string = base;
let showExitModal = false;
let canSelectOrg = true;
@@ -41,7 +49,13 @@
let selectedOrgId: string = null;
let formComponent: Form;
let couponForm: Form;
let isSubmitting = writable(false);
let methods: PaymentList;
let paymentMethodId: string;
let collaborators: string[];
let taxId: string;
let billingBudget: number;
let newOrgId = ID.unique();
let options = [
...($organizationList?.teams?.map((team) => ({
@@ -53,16 +67,14 @@
label: 'Create new organization'
}
];
let name: string;
let coupon: string;
let couponData = data?.couponData;
let campaign = data?.campaign;
$: selectedAction = selectedOrgId === newOrgId ? 'create' : 'update';
let billingPlan = BillingPlan.PRO;
onMount(async () => {
if (!$organizationList?.total && couponData?.code) {
goto(`${base}/create-organization?coupon=${couponData.code}&plan=${BillingPlan.Tier1}`);
}
await loadPaymentMethods();
if (!$organizationList?.total || campaign?.onlyNewOrgs) {
selectedOrgId = newOrgId;
}
@@ -70,59 +82,128 @@
selectedOrgId = $page.url.searchParams.get('org');
canSelectOrg = false;
}
if (campaign?.plan) {
billingPlan = campaign.plan;
}
if ($page.url.searchParams.has('type')) {
const type = $page.url.searchParams.get('type');
if (type === 'payment_confirmed') {
const organizationId = $page.url.searchParams.get('id');
collaborators = $page.url.searchParams.get('invites').split(',');
await sdk.forConsole.billing.validateOrganization(organizationId, collaborators);
}
}
if (!selectedOrgId && $organizationList?.total) {
selectedOrgId = $organizationList.teams[0].$id;
}
});
async function applyCouponCredit(org: Organization) {
$isSubmitting = true;
let error = null;
try {
await sdk.forConsole.billing.addCredit(org.$id, couponData.code);
trackEvent(Submit.CreditRedeem);
} catch (e) {
error = e;
$isSubmitting = false;
trackError(error, Submit.CreditRedeem);
}
addNotification({
type: error ? 'error' : 'success',
message: error ? error.message : 'Credit applied successfully'
});
if (!error) {
await goto(`${base}/organization-${selectedOrgId}`);
}
async function loadPaymentMethods() {
const methodList = await sdk.forConsole.billing.listPaymentMethods();
const filteredMethods = methodList.paymentMethods.filter((method) => !!method?.last4);
methods = { paymentMethods: filteredMethods, total: filteredMethods.length };
paymentMethodId =
selectedOrg?.paymentMethodId ??
methods.paymentMethods.find((method) => !!method?.last4)?.$id ??
null;
}
async function handleSubmit() {
if (couponData) {
const createOrganization = selectedAction === 'create';
const isScalePlanUpgrade =
campaign?.plan && selectedOrg?.billingPlan !== campaign?.plan;
if (!couponForm.checkValidity()) return;
isSubmitting.set(true);
try {
let org: Organization | OrganizationError;
// Create new org
if (selectedOrgId === newOrgId) {
org = await sdk.forConsole.billing.createOrganization(
newOrgId,
name,
billingPlan,
paymentMethodId,
null,
couponData.code ? couponData.code : null,
collaborators,
billingBudget,
taxId
);
}
// on create-org, its `coupon`
if (createOrganization) {
await goto(`${base}/create-organization?coupon=${couponData.code}`);
return;
}
if (!campaign?.plan || campaign.plan === selectedOrg?.billingPlan) {
await applyCouponCredit(selectedOrg);
} else if (isScalePlanUpgrade) {
await goto(
`${base}/create-organization?coupon=${couponData.code}&plan=${campaign.plan}`
);
} else if (selectedAction === 'update') {
// on change-plan, its `code`
await goto(
`${base}/organization-${selectedOrgId}/change-plan?code=${couponData.code}`
// Upgrade existing org
else if (selectedOrg?.billingPlan !== billingPlan) {
org = await sdk.forConsole.billing.updatePlan(
selectedOrg.$id,
billingPlan,
paymentMethodId,
null,
couponData.code ? couponData.code : null,
collaborators
);
}
} else {
// we might not reach here but still, just being safe.
// Existing pro org
else {
org = selectedOrg;
}
if (!isOrganization(org) && org.status === 402) {
let clientSecret = org.clientSecret;
let params = new URLSearchParams();
params.append('type', 'payment_confirmed');
params.append('org', org.teamId);
for (const [key, value] of $page.url.searchParams.entries()) {
if (key !== 'type' && key !== 'id') {
params.append(key, value);
}
}
params.append('invites', collaborators.join(','));
await confirmPayment(
'',
clientSecret,
paymentMethodId,
'/console/apply-credit?' + params.toString()
);
org = await sdk.forConsole.billing.validateOrganization(org.teamId, collaborators);
}
if (isOrganization(org)) {
trackEvent(Submit.CreditRedeem, {
coupon: couponData.code,
campaign: couponData?.campaign
});
await invalidate(Dependencies.ORGANIZATION);
await goto(`${base}/organization-${org.$id}`);
addNotification({
type: 'success',
message: 'Credits applied successfully'
});
await invalidate(Dependencies.ACCOUNT);
}
} catch (e) {
trackError(e, Submit.CreditRedeem);
addNotification({
type: 'error',
message: 'Coupon code is not valid'
message: e.message
});
} finally {
isSubmitting.set(false);
}
}
async function addCoupon() {
try {
const response = await sdk.forConsole.billing.getCoupon(coupon);
couponData = response;
coupon = null;
addNotification({
type: 'success',
message: 'Credits applied successfully'
});
} catch (e) {
addNotification({
type: 'error',
message: e.message
});
}
}
@@ -130,6 +211,11 @@
$: selectedOrg = $organizationList?.teams?.find(
(team) => team.$id === selectedOrgId
) as Organization;
$: billingPlan =
selectedOrg?.billingPlan === BillingPlan.SCALE
? BillingPlan.SCALE
: (campaign?.plan ?? BillingPlan.PRO);
</script>
<svelte:head>
@@ -138,9 +224,9 @@
<WizardSecondaryContainer href={previousPage} bind:showExitModal confirmExit>
<svelte:fragment slot="title">Apply credits</svelte:fragment>
<WizardSecondaryContent hideSidebar={!$organizationList?.total}>
<WizardSecondaryContent>
<Form bind:this={formComponent} onSubmit={handleSubmit} bind:isSubmitting>
<FormList gap={8}>
<FormList>
{#if $organizationList?.total && !campaign?.onlyNewOrgs && canSelectOrg}
<InputSelect
bind:value={selectedOrgId}
@@ -150,69 +236,44 @@
placeholder="Select organization"
id="organization" />
{/if}
<!-- TEMP FIX -->
{#if !$organizationList?.total}
{#if campaign?.template === 'card'}
<div
class="box card-container u-position-relative"
style:--box-border-radius="var(--border-radius-small)">
<div class="card-bg"></div>
<div
class="u-flex u-flex-vertical u-gap-24 u-cross-center u-position-relative">
<img
src={campaign?.image[$app.themeInUse]}
class="u-block u-image-object-fit-cover card-img"
alt="promo" />
<p class="text">
{#if couponData?.credits}
{campaign.title.replace(
'VALUE',
couponData.credits.toString()
)}
{:else}
{campaign.title}
{/if}
</p>
</div>
</div>
{#if selectedOrgId && (selectedOrg?.billingPlan === BillingPlan.FREE || !selectedOrg?.paymentMethodId)}
{#if selectedOrgId === newOrgId}
<InputText
label="Organization name"
placeholder="Enter organization name"
id="name"
required
bind:value={name} />
{/if}
<section
class="card u-margin-block-start-24"
style:--p-card-padding="1.5rem"
style:--p-card-border-radius="var(--border-radius-small)">
{#if couponData?.code && couponData?.status === 'active'}
{@const dateAvailable = selectedOrg?.billingNextInvoiceDate}
<CreditsApplied
bind:couponData
fixedCoupon={!!data?.couponData?.code} />
<p class="text u-margin-block-start-12">
{#if !dateAvailable}
Credits will automatically be applied to your next invoice.
{:else}
Credits will automatically be applied to your next invoice on <b
>{toLocaleDate(selectedOrg?.billingNextInvoiceDate)}</b
>.
{/if}
</p>
{:else}
<p class="text">
Add a coupon code to apply credits to your organization.
</p>
{/if}
</section>
<InputTags
bind:tags={collaborators}
label="Invite members by email"
tooltip="Invited members will have access to all services and payment data within your organization"
placeholder="Enter email address(es)"
validityRegex={emailRegex}
validityMessage="Invalid email address"
id="members" />
<SelectPaymentMethod bind:methods bind:value={paymentMethodId} bind:taxId />
{/if}
{#if campaign?.plan && selectedOrg && selectedOrg.billingPlan !== campaign?.plan}
<Helper type="neutral">
This coupon code is only valid when upgrading to Scale plan.
</Helper>
</FormList>
</Form>
<Form bind:this={couponForm} onSubmit={addCoupon}>
<FormList>
{#if !data?.couponData?.code && selectedOrgId}
<InputText
required
disabled={!!couponData?.credits}
bind:value={coupon}
placeholder="Enter coupon code"
id="code"
label="Coupon code">
<Button submit secondary disabled={!!couponData?.credits}>
<span class="text">Apply</span>
</Button>
</InputText>
{/if}
</FormList>
</Form>
<!-- this is the side card and is okay -->
<svelte:fragment slot="aside">
{#if campaign?.template === 'card'}
<div
@@ -221,7 +282,7 @@
<div class="card-bg"></div>
<div class="u-flex u-flex-vertical u-gap-24 u-cross-center u-position-relative">
<img
src={campaign?.image[$app.themeInUse]}
src={getCampaignImageUrl(campaign?.image[$app.themeInUse])}
class="u-block u-image-object-fit-cover card-img"
alt="promo" />
<p class="text">
@@ -234,27 +295,44 @@
</div>
</div>
{/if}
<section
class="card u-margin-block-start-24"
style:--p-card-padding="1.5rem"
style:--p-card-border-radius="var(--border-radius-small)">
{#if couponData?.code && couponData?.status === 'active'}
{@const dateAvailable = selectedOrg?.billingNextInvoiceDate}
<CreditsApplied bind:couponData fixedCoupon={!!data?.couponData?.code} />
<p class="text u-margin-block-start-12">
{#if !dateAvailable}
Credits will automatically be applied to your next invoice.
{:else}
{#if selectedOrg?.$id && selectedOrg?.billingPlan !== BillingPlan.FREE && selectedOrg?.billingPlan !== BillingPlan.GITHUB_EDUCATION}
<section
class="card u-margin-block-start-24"
style:--p-card-padding="1.5rem"
style:--p-card-border-radius="var(--border-radius-small)">
{#if couponData?.code && couponData?.status === 'active'}
<CreditsApplied bind:couponData fixedCoupon={!!data?.couponData?.code} />
<p class="text u-margin-block-start-12">
Credits will automatically be applied to your next invoice on <b
>{toLocaleDate(selectedOrg?.billingNextInvoiceDate)}</b
>.
>{toLocaleDate(selectedOrg?.billingNextInvoiceDate)}.</b>
</p>
{:else}
<p class="text">Add a coupon code to apply credits to your organization.</p>
{/if}
</section>
{:else if selectedOrgId}
<div class:u-margin-block-start-24={campaign?.template === 'card'}>
<EstimatedTotal
{billingBudget}
organizationId={selectedOrgId === newOrgId ? undefined : selectedOrgId}
{billingPlan}
{collaborators}
{couponData}>
{#if campaign?.template === 'review' && (campaign?.cta || campaign?.claimed || campaign?.unclaimed)}
<div class="u-margin-block-end-24">
<p class="body-text-1 u-bold">{campaign?.cta}</p>
<p class="text u-margin-block-start-8">
{#if couponData?.code && couponData?.status === 'active' && campaign?.claimed}
{campaign?.claimed}
{:else if campaign?.unclaimed}
{campaign?.unclaimed}
{/if}
</p>
</div>
{/if}
</p>
{:else}
<p class="text">Add a coupon code to apply credits to your organization.</p>
{/if}
</section>
</EstimatedTotal>
</div>
{/if}
</svelte:fragment>
</WizardSecondaryContent>
@@ -263,22 +341,19 @@
<Button
fullWidthMobile
on:click={() => {
if (formComponent.checkValidity()) {
if (formComponent.checkValidity() && couponForm.checkValidity()) {
handleSubmit();
}
}}
disabled={$isSubmitting || !selectedOrgId}>
disabled={$isSubmitting}>
{#if $isSubmitting}
<span class="loader is-small is-transparent u-line-height-1-5" aria-hidden="true" />
{/if}
<!-- {#if selectedOrgId === newOrgId}
Create organization
{:else if campaign?.plan && selectedOrg && selectedOrg.billingPlan !== campaign?.plan}
Upgrade plan
{#if selectedOrgId === newOrgId}
Create Organization
{:else}
Apply credits
{/if} -->
Apply credits
Apply credits
{/if}
</Button>
</WizardSecondaryFooter>
<svelte:fragment slot="exit">
+1 -2
View File
@@ -3,9 +3,8 @@ import type { Coupon } from '$lib/sdk/billing.js';
import type { Campaign } from '$lib/stores/campaigns.js';
import { sdk } from '$lib/stores/sdk.js';
import { redirect } from '@sveltejs/kit';
import type { PageLoad } from './$types';
export const load: PageLoad = async ({ url }) => {
export const load = async ({ url }) => {
// Has promo code
if (url.searchParams.has('code')) {
let couponData: Coupon;
@@ -91,7 +91,7 @@
const type = $page.url.searchParams.get('type');
if (type === 'payment_confirmed') {
const organizationId = $page.url.searchParams.get('id');
const invites = $page.url.searchParams.getAll('invites');
const invites = $page.url.searchParams.get('invites').split(',');
await validate(organizationId, invites);
}
}
@@ -152,10 +152,12 @@
let params = new URLSearchParams();
params.append('type', 'payment_confirmed');
params.append('id', org.teamId);
for (let index = 0; index < collaborators.length; index++) {
const invite = collaborators[index];
params.append('invites', invite);
for (const [key, value] of $page.url.searchParams.entries()) {
if (key !== 'type' && key !== 'id') {
params.append(key, value);
}
}
params.append('invites', collaborators.join(','));
await confirmPayment(
'',
clientSecret,
@@ -28,16 +28,16 @@
import { ID, Region } from '@appwrite.io/console';
import { openImportWizard } from '../project-[project]/settings/migrations/(import)';
import { readOnly } from '$lib/stores/billing';
import type { RegionList } from '$lib/sdk/billing';
import { onMount } from 'svelte';
import { organization } from '$lib/stores/organization';
import { canWriteProjects } from '$lib/stores/roles';
import { checkPricingRefAndRedirect } from '$lib/helpers/pricingRedirect';
import { regions as regionsStore } from '$routes/(console)/organization-[organization]/store';
export let data;
let addOrganization = false;
let showCreate = false;
let addOrganization = false;
const getPlatformInfo = (platform: string) => {
let name: string, icon: string;
@@ -85,6 +85,7 @@
if (isCloud) wizard.start(Create);
else showCreate = true;
}
$: $registerCommands([
{
label: 'Create project',
@@ -120,17 +121,16 @@
}
};
let regions: RegionList;
onMount(async () => {
if (isCloud) {
regions = await sdk.forConsole.billing.listRegions();
const regions = await sdk.forConsole.billing.listRegions();
regionsStore.set(regions);
checkPricingRefAndRedirect($page.url.searchParams);
}
});
function findRegion(project: Models.Project) {
const region = regions.regions.find((region) => region.$id === project.region);
return region;
return $regionsStore?.regions?.find((region) => region.$id === project.region);
}
</script>
@@ -198,7 +198,7 @@
</Pill>
{/if}
<svelte:fragment slot="icons">
{#if isCloud && regions}
{#if isCloud && $regionsStore?.regions}
{@const region = findRegion(project)}
<span class="u-color-text-gray u-medium u-line-height-2">
{region?.name}
@@ -99,6 +99,15 @@
billingPlan = plan as BillingPlan;
}
}
if ($page.url.searchParams.has('type')) {
const type = $page.url.searchParams.get('type');
if (type === 'payment_confirmed') {
const organizationId = $page.url.searchParams.get('id');
const invites = $page.url.searchParams.get('invites').split(',');
await validate(organizationId, invites);
}
}
if ($currentPlan?.$id === BillingPlan.SCALE) {
billingPlan = BillingPlan.SCALE;
} else {
@@ -224,17 +233,20 @@
if (!isOrganization(org) && org.status == 402) {
let clientSecret = org.clientSecret;
let params = new URLSearchParams();
for (const [key, value] of $page.url.searchParams.entries()) {
if (key !== 'type' && key !== 'id') {
params.append(key, value);
}
}
params.append('type', 'payment_confirmed');
params.append('id', org.teamId);
for (let index = 0; index < collaborators.length; index++) {
const invite = collaborators[index];
params.append('invites', invite);
}
params.append('invites', collaborators.join(','));
params.append('plan', billingPlan);
await confirmPayment(
'',
clientSecret,
paymentMethodId,
'/console/create-organization?' + params.toString()
'/console/change-plan?' + params.toString()
);
await validate(org.teamId, collaborators);
}
@@ -1,18 +1,18 @@
<script lang="ts">
import { invalidate } from '$app/navigation';
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
import { Modal } from '$lib/components';
import { Dependencies } from '$lib/constants';
import { Button } from '$lib/elements/forms';
import { logout } from '$lib/helpers/logout';
import { checkForUsageLimit } from '$lib/stores/billing';
import { addNotification } from '$lib/stores/notifications';
import { organization } from '$lib/stores/organization';
import { sdk } from '$lib/stores/sdk';
import { user } from '$lib/stores/user';
import { isCloud } from '$lib/system';
import type { Models } from '@appwrite.io/console';
import { createEventDispatcher } from 'svelte';
import { user } from '$lib/stores/user';
import { Submit, trackEvent, trackError } from '$lib/actions/analytics';
import { Dependencies } from '$lib/constants';
import { checkForUsageLimit } from '$lib/stores/billing';
import { isCloud } from '$lib/system';
import { organization } from '$lib/stores/organization';
import { logout } from '$lib/helpers/logout';
const dispatch = createEventDispatcher();
@@ -36,7 +36,7 @@
showDelete = false;
addNotification({
type: 'success',
message: `${selectedMember.userName} was deleted from ${selectedMember.teamName}`
message: `${selectedMember.userName || 'User'} was deleted from ${selectedMember.teamName}`
});
trackEvent(Submit.MemberDelete);
} catch (error) {
@@ -1,7 +1,15 @@
<script lang="ts">
import { base } from '$app/paths';
import { page } from '$app/stores';
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
import { AvatarInitials, DropList, DropListItem, PaginationWithLimit } from '$lib/components';
import {
AvatarInitials,
Drop,
DropList,
DropListItem,
PaginationWithLimit
} from '$lib/components';
import Upgrade from '$lib/components/roles/upgrade.svelte';
import { Pill } from '$lib/elements';
import {
TableBody,
@@ -13,18 +21,15 @@
TableScroll
} from '$lib/elements/table';
import { Container, ContainerHeader } from '$lib/layout';
import { getRoleLabel } from '$lib/stores/billing';
import { addNotification } from '$lib/stores/notifications';
import { newMemberModal, organization } from '$lib/stores/organization';
import { isOwner } from '$lib/stores/roles';
import { sdk } from '$lib/stores/sdk';
import type { Models } from '@appwrite.io/console';
import type { PageData } from './$types';
import Delete from '../deleteMember.svelte';
import { base } from '$app/paths';
import { isOwner } from '$lib/stores/roles';
import type { PageData } from './$types';
import Edit from './edit.svelte';
import { getRoleLabel } from '$lib/stores/billing';
import { Drop } from '$lib/components';
import Upgrade from '$lib/components/roles/upgrade.svelte';
export let data: PageData;
@@ -113,9 +118,11 @@
<TableRow>
<TableCell title="Name">
<div class="u-flex u-gap-12 u-cross-center">
<AvatarInitials size={40} name={member.userName} />
<AvatarInitials
size={40}
name={member.userName || member.userEmail} />
<span class="text u-trim">
{member.userName ? member.userName : 'n/a'}
{member.userName || 'n/a'}
</span>
{#if member.invited && !member.joined}
<Pill warning>Pending</Pill>
@@ -1,5 +1,16 @@
import { page } from '$app/stores';
import type { Models } from '@appwrite.io/console';
import { derived } from 'svelte/store';
import { derived, writable } from 'svelte/store';
import type { RegionList } from '$lib/sdk/billing';
import { sdk } from '$lib/stores/sdk';
export const regions = writable<RegionList | undefined>(undefined);
export const regionFlagUrls = derived(regions, ($regions) => {
if (!$regions?.regions?.length) return [];
return $regions?.regions?.map((region) => {
return `${sdk.forConsole.client.config.endpoint}/avatars/flags/${region.flag}?width=80&height=60&quality=100&mode=admin`;
});
});
export const projects = derived(page, ($page) => $page.data?.projects as Models.ProjectList);
@@ -64,9 +64,11 @@
<TableRow>
<TableCell title="name">
<div class="u-flex u-gap-12 u-cross-center">
<AvatarInitials size={32} name={member.userName} />
<AvatarInitials
size={32}
name={member.userName || member.userEmail} />
<span class="text u-trim">
{member.userName ? member.userName : member.userEmail}
{member.userName || member.userEmail}
</span>
</div>
</TableCell>
@@ -4,13 +4,22 @@
import { InputText, FormList } from '$lib/elements/forms';
import { WizardStep } from '$lib/layout';
import { sdk } from '$lib/stores/sdk';
import { createProject, regions } from './store';
import { createProject } from './store';
import { regionFlagUrls, regions } from '$routes/(console)/organization-[organization]/store';
let showCustomId = false;
sdk.forConsole.billing.listRegions().then(regions.set);
if (!$regions?.regions) {
sdk.forConsole.billing.listRegions().then(regions.set);
}
</script>
<svelte:head>
{#each $regionFlagUrls as image}
<link rel="preload" as="image" href={image} />
{/each}
</svelte:head>
<WizardStep>
<svelte:fragment slot="title">Details</svelte:fragment>
<FormList>
@@ -4,11 +4,12 @@
import { WizardStep } from '$lib/layout';
import { sdk } from '$lib/stores/sdk';
import { onMount } from 'svelte';
import { createProject, regions } from './store';
import { createProject } from './store';
import type { Region } from '$lib/sdk/billing';
import { addNotification } from '$lib/stores/notifications';
import type { Models } from '@appwrite.io/console';
import { page } from '$app/stores';
import { regions } from '$routes/(console)/organization-[organization]/store';
let prefs: Models.Preferences;
@@ -1,4 +1,3 @@
import type { RegionList } from '$lib/sdk/billing';
import { writable } from 'svelte/store';
export const createProject = writable<{
@@ -10,5 +9,3 @@ export const createProject = writable<{
name: null,
region: 'fra'
});
export const regions = writable<RegionList | undefined>(undefined);
@@ -1,7 +1,7 @@
<script lang="ts">
import { invalidate } from '$app/navigation';
import { page } from '$app/stores';
import { Submit, trackEvent, trackError } from '$lib/actions/analytics';
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
import { Modal } from '$lib/components';
import { Dependencies } from '$lib/constants';
import { Button } from '$lib/elements/forms';
@@ -39,7 +39,7 @@
state="warning"
headerDivider={false}>
<p data-private>
Are you sure you want to delete <b>all of {$user.name}'s sessions?</b>
Are you sure you want to delete <b>all of {$user.name || 'User'}'s sessions?</b>
</p>
<svelte:fragment slot="footer">
<Button text on:click={() => (showDeleteAll = false)}>Cancel</Button>
@@ -2,13 +2,13 @@
import { goto } from '$app/navigation';
import { base } from '$app/paths';
import { page } from '$app/stores';
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
import { Modal } from '$lib/components';
import { Button } from '$lib/elements/forms';
import { addNotification } from '$lib/stores/notifications';
import { sdk } from '$lib/stores/sdk';
import { user } from './store';
import { project } from '../../store';
import { Submit, trackEvent, trackError } from '$lib/actions/analytics';
import { user } from './store';
export let showDelete = false;
let error: string;
@@ -37,7 +37,9 @@
state="warning"
headerDivider={false}
bind:error>
<p data-private>Are you sure you want to delete <b>{$user.name}</b> from '{$project.name}'?</p>
<p data-private>
Are you sure you want to delete <b>{$user.name || 'User'}</b> from '{$project.name}'?
</p>
<svelte:fragment slot="footer">
<Button text on:click={() => (showDelete = false)}>Cancel</Button>
<Button secondary submit>Delete</Button>
@@ -1,6 +1,6 @@
<script lang="ts">
import { invalidate } from '$app/navigation';
import { Submit, trackEvent, trackError } from '$lib/actions/analytics';
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
import { CardGrid, Heading } from '$lib/components';
import { Dependencies } from '$lib/constants';
import { Button, Form, InputText } from '$lib/elements/forms';
@@ -11,7 +11,7 @@
let userName: string = null;
onMount(async () => {
userName ??= $user.name;
userName ??= $user.name ?? '';
});
async function updateName() {
+8
View File
@@ -1,3 +1,5 @@
import { getApiEndpoint } from '$lib/stores/sdk';
export function getCardImgUrls(userId: string, endpoint: string) {
const resolved = endpoint;
@@ -7,3 +9,9 @@ export function getCardImgUrls(userId: string, endpoint: string) {
return { frontImg, backImg, ogImg };
}
export function getCampaignImageUrl(image: string) {
const endpoint = getApiEndpoint();
const url = new URL(image, endpoint);
return url.toString();
}
+3 -2
View File
@@ -47,7 +47,7 @@
.getCampaign(coupon.campaign)
.catch<null>(() => null);
if (campaign && $user) {
goto(`${base}/apply-credit?code=${code}`);
goto(`${base}/apply-credit?${$page.url.searchParams}`);
loading.set(false);
return;
}
@@ -59,7 +59,8 @@
.getCampaign(campaignId)
.catch<null>(() => null);
if (campaign) {
goto(`${base}/apply-credit?campaign=${campaign.$id}`);
goto(`${base}/apply-credit?${$page.url.searchParams}`);
loading.set(false);
return;
}
+3 -14
View File
@@ -58,18 +58,7 @@ export const load: LayoutLoad = async ({ depends, url, route }) => {
}
};
function withParams(pathname: string, searchParams: URLSearchParams): string {
const redirect = searchParams.get('redirect');
if (redirect) {
const extras = new URLSearchParams(searchParams);
extras.delete('redirect');
const extraQuery = extras.toString();
const finalRedirect = extraQuery
? `${redirect}${redirect.includes('?') ? '&' : '?'}${extraQuery}`
: redirect;
return `${pathname}?redirect=${encodeURIComponent(finalRedirect)}`;
}
const query = searchParams.toString();
return query ? `${pathname}?${query}` : pathname;
function withParams(pathname: string, searchParams: URLSearchParams) {
if (searchParams.size > 0) return `${pathname}?${searchParams.toString()}`;
return pathname;
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 416 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 384 KiB