Skip to content
Merged
251 changes: 162 additions & 89 deletions src/lib/components/support.svelte
Original file line number Diff line number Diff line change
@@ -1,63 +1,141 @@
<script lang="ts">
import { Button } from '$lib/elements/forms';
import { app } from '$lib/stores/app';
import { wizard } from '$lib/stores/wizard';
import SupportWizard from '$routes/(console)/supportWizard.svelte';
import { showSupportModal } from '$routes/(console)/wizard/support/store';
import { isCloud } from '$lib/system';
import { isSupportOnline, showSupportModal } from '$routes/(console)/wizard/support/store';
import { trackEvent } from '$lib/actions/analytics';
import { localeTimezoneName, utcHourToLocaleHour } from '$lib/helpers/date';
import { localeShortTimezoneName, utcHourToLocaleHour } from '$lib/helpers/date';
import { upgradeURL } from '$lib/stores/billing';
import { Card } from '$lib/components/index';
import { app } from '$lib/stores/app';
import { currentPlan } from '$lib/stores/organization';
import { isCloud } from '$lib/system';

export let show = false;

export let showHeader = true;

$: hasPremiumSupport = $currentPlan?.premiumSupport ?? false;

$: supportTimings = `${utcHourToLocaleHour('16:00')} - ${utcHourToLocaleHour('00:00')} ${localeTimezoneName()}`;
$: supportTimings = `${utcHourToLocaleHour('16:00')} - ${utcHourToLocaleHour('00:00')} ${localeShortTimezoneName()}`;

type SupportOption = {
cta?: string;
icon: string;
label: string;
link?: string;
description: string;
showSupport: boolean;
};

const supportOptions: SupportOption[] = [
{
showSupport: true,
icon: 'support',
label: 'Premium support',
description: 'Get priority email support from the Appwrite team'
},

{
icon: 'discord',
cta: 'Discord',
showSupport: false,
label: 'Community support',
link: 'https://appwrite.io/discord',
description: 'Get support from our community through Discord'
},
{
icon: 'github',
cta: 'Open issue',
showSupport: false,
label: 'Open GitHub issue',
link: 'https://github.com/appwrite/appwrite/issues/new/choose',
description: 'Report a bug or pitch a new feature'
}
];

const showCloudSupport = (index) => {
return (index === 0 && isCloud) || index > 0;
};
</script>

{#if isCloud}
<section class="drop-section u-grid u-gap-24 u-padding-24">
<div>
<h4 class="eyebrow-heading-3">Premium support</h4>
{#if hasPremiumSupport}
<p class="u-line-height-1-5 u-margin-block-start-8">
Get personalized support from the Appwrite team from <b>{supportTimings}</b>
</p>
{/if}
</div>
{#if !hasPremiumSupport}
<Button
fullWidth
href={$upgradeURL}
on:click={() => {
trackEvent('click_organization_upgrade', {
from: 'button',
source: 'support_menu'
});
}}>
<span class="text">Get Premium support</span>
</Button>
{:else}
<Button
secondary
fullWidth
on:click={() => {
show = false;
$showSupportModal = false;
wizard.start(SupportWizard);
}}>
<span class="text">Contact our Support Team</span>
</Button>
<section class="drop-section support-section">
{#if showHeader}
<h4 class="heading-level-6">Support</h4>
{/if}

{#each supportOptions as option, index}
{#if showCloudSupport(index)}
<Card
isTile
class="support-option-card u-flex u-flex-vertical u-gap-16"
style="border-radius: var(--border-radius-small, 8px); padding: 0.65rem;">
<div class="u-flex u-flex-vertical u-gap-4">
<h4 class="body-text-2 u-bold">{option.label}</h4>

<p class="u-line-height-1-5">
{option.description}
</p>
</div>

{#if option.showSupport}
<div class="u-flex u-gap-12 u-cross-center">
{#if !hasPremiumSupport}
<Button
href={$upgradeURL}
on:click={() => {
trackEvent('click_organization_upgrade', {
from: 'button',
source: 'support_menu'
});
}}>
<span class="text">Get Premium support</span>
</Button>
{:else}
<Button
secondary
class="secondary-button"
on:click={() => {
show = false;
$showSupportModal = false;
wizard.start(SupportWizard);
}}>
<span class="text">Contact support</span>
</Button>
{/if}

<div class="u-flex u-gap-6 u-cross-center">
<span
aria-hidden="true"
class="{isSupportOnline()
? 'icon-check-circle u-color-text-success'
: 'icon-x-circle'} u-padding-block-end-1" />

{supportTimings}
</div>
</div>
{:else}
<Button
href={option.link}
external
secondary
class="secondary-button u-flex u-cross-center u-gap-6"
on:click={() => {
trackEvent('click_organization_upgrade', {
from: 'button',
source: 'support_menu'
});
}}>
<span class={`icon-${option.icon}`} />
<span>{option.cta}</span>
</Button>
{/if}
</Card>
{/if}
</section>
{/if}
<section class="drop-section u-grid u-gap-24 u-padding-24">
<div>
<h4 class="eyebrow-heading-3">Troubleshooting</h4>
{/each}

<div class="u-margin-block-start-8 u-width-full-line">
{#if isCloud}
<div class="u-width-full-line">
{#key $app.themeInUse}
<iframe
style="color-scheme: none"
Expand All @@ -72,49 +150,44 @@
</iframe>
{/key}
</div>
</div>

<div class="u-flex u-gap-16">
<a
href="https://appwrite.io/docs"
target="_blank"
rel="noopener noreferrer"
class="button is-secondary u-padding-inline-12 u-stretch u-main-center u-gap-4 u-flex-basis-auto">
<span class="icon-book-open" aria-hidden="true" />
<span class="text">Docs</span>
</a>
<a
href="https://github.com/appwrite/appwrite/issues"
aria-label="Open issue on GitHub"
target="_blank"
rel="noopener noreferrer"
class="button is-secondary u-padding-inline-12 u-stretch u-main-center u-gap-4 u-flex-basis-auto">
<span class="icon-github" aria-hidden="true" />
<span class="text">Open issue</span>
</a>
</div>
</section>
<section class="drop-section u-grid u-gap-8 u-padding-24">
<div>
<h4 class="eyebrow-heading-3">Community support</h4>
<p class="text u-margin-block-start-8">Get help from our community</p>
</div>
<ul class="u-flex u-gap-8">
<li>
<Button href="https://github.com/appwrite" text noMargin external ariaLabel="Github">
<span class="icon-github" aria-hidden="true" />
</Button>
</li>
<li>
<Button
href="https://appwrite.io/discord"
round
text
noMargin
external
ariaLabel="Discord">
<span class="icon-discord" aria-hidden="true" />
</Button>
</li>
</ul>
{/if}
</section>

<style lang="scss">
.support-section {
gap: 1rem;
padding: 1rem;
display: flex;
flex-direction: column;

@media (max-width: 768px) {
gap: 1.25rem;
padding: 0.5rem;
}
}

:global(.u-gap-6) {
gap: 0.375rem;
}

:global(.support-option-card) {
padding: 0.75rem !important;
}

:global(.theme-dark .support-option-card) {
background: var(--color-bgColor-neutral-default, #19191c);
}

:global(.theme-dark .support-option-card .secondary-button) {
background: var(--color-bgColor-neutral-primary, #131315);
}

:global(.theme-light .support-option-card) {
border: 1px solid var(--color-border-neutral, #ededf0);
background: var(--color-bgColor-neutral-default, #fafafb);
}

:global(.theme-light .support-option-card .secondary-button) {
background: var(--color-bgColor-neutral-primary, #fff);
}
</style>
17 changes: 17 additions & 0 deletions src/lib/helpers/date.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,23 @@ export const localeTimezoneName = () => {
return dateWithTimezone.split(', ')[1];
};

/**
* This function processes the timezone name using regex to:
*
* 1. Trim and simplify the text for UI display (e.g., IST, UST, etc.).
* 2. Handle cases where `timeZoneName: 'shortGeneric'` returns names like `India Time`,
* which do not fit well in the support modal UI.
* 3. Not using `timeZoneName: 'short'`, as it can display `GMT+-` formatted timezones,
* which may not be ideal.
*
* Refer to:
* https://github.com/appwrite/console/pull/1564#discussion_r1913977798
*/
export const localeShortTimezoneName = () => {
const timezone = localeTimezoneName();
return timezone.match(/[A-Z]/g)?.join('') || timezone;
};

export const isSameDay = (date1: Date, date2: Date) => {
return (
date1.getFullYear() === date2.getFullYear() &&
Expand Down
31 changes: 20 additions & 11 deletions src/lib/layout/header.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -125,17 +125,19 @@
<Feedback />
</svelte:fragment>
</DropList>

{#if isCloud}
<DropList width="18.5" bind:show={showSupport} scrollable={true}>
<Button text on:click={() => (showSupport = !showSupport)}>
<span class="text">Support</span>
</Button>
<svelte:fragment slot="other">
<Support bind:show={showSupport} />
</svelte:fragment>
</DropList>
{/if}
<DropList
class="support-drop-section"
bind:show={showSupport}
scrollable={true}
noArrow
placement="bottom-end">
<Button text on:click={() => (showSupport = !showSupport)}>
<span class="text">Support</span>
</Button>
<svelte:fragment slot="other">
<Support bind:show={showSupport} />
</svelte:fragment>
</DropList>
<Button
actions={[
(node) => {
Expand Down Expand Up @@ -265,3 +267,10 @@
{/if}
</nav>
</div>

<style>
:global(.support-drop-section) {
width: 28.375rem;
margin-block-start: 0.15rem;
}
</style>
4 changes: 3 additions & 1 deletion src/routes/(console)/supportWizard.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
onDestroy(() => {
$supportData = {
message: null,
subject: null,
category: 'general',
file: null
};
Expand All @@ -39,8 +40,8 @@
'Content-Type': 'application/json'
},
body: JSON.stringify({
subject: 'support',
email: $user.email,
subject: $supportData.subject,
firstName: $user?.name || 'Unknown',
message: $supportData.message,
tags: ['cloud'],
Expand Down Expand Up @@ -75,6 +76,7 @@
function resetData() {
$supportData = {
message: null,
subject: null,
category: 'general',
file: null,
project: null
Expand Down
24 changes: 21 additions & 3 deletions src/routes/(console)/wizard/support/mobileSupportModal.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,24 @@
export let show: boolean;
</script>

<Modal title="Contact us" bind:show>
<Support />
</Modal>
<div class="mobile-support-dialog-modal">
<Modal title="Support" headerDivider={false} size="small" bind:show>
<div class="inner-modal-wrapper">
<Support showHeader={false} />
</div>
</Modal>
</div>

<style>
:global(.mobile-support-dialog-modal dialog) {
background-color: #fff;
}

:global(.theme-dark .mobile-support-dialog-modal dialog) {
background-color: #131315;
}

.inner-modal-wrapper {
padding-block-start: 0.5rem;
}
</style>
Loading
Loading