diff --git a/src/lib/actions/analytics.ts b/src/lib/actions/analytics.ts index 06f204128..2d674a24e 100644 --- a/src/lib/actions/analytics.ts +++ b/src/lib/actions/analytics.ts @@ -131,6 +131,7 @@ export enum Submit { ProjectCreate = 'submit_project_create', ProjectDelete = 'submit_project_delete', ProjectUpdateName = 'submit_project_update_name', + ProjectUpdateTeam = 'submit_project_update_team', ProjectService = 'submit_project_service', ProjectUpdateSMTP = 'submit_project_update_smtp', MemberCreate = 'submit_member_create', diff --git a/src/lib/components/empty.svelte b/src/lib/components/empty.svelte index 2c01f69fa..9dc0c6103 100644 --- a/src/lib/components/empty.svelte +++ b/src/lib/components/empty.svelte @@ -21,7 +21,8 @@ {#if single}
-
+
diff --git a/src/lib/components/feedback/feedbackGeneral.svelte b/src/lib/components/feedback/feedbackGeneral.svelte new file mode 100644 index 000000000..183921a56 --- /dev/null +++ b/src/lib/components/feedback/feedbackGeneral.svelte @@ -0,0 +1,19 @@ + + + + + + + diff --git a/src/lib/components/feedback/feedbackNPS.svelte b/src/lib/components/feedback/feedbackNPS.svelte new file mode 100644 index 000000000..613e10514 --- /dev/null +++ b/src/lib/components/feedback/feedbackNPS.svelte @@ -0,0 +1,30 @@ + + + + How likely are you to recommend Appwrite to a friend or colleague? + +{#if $feedbackData.value} + + + + + +{/if} diff --git a/src/lib/components/feedback/index.ts b/src/lib/components/feedback/index.ts new file mode 100644 index 000000000..15d522683 --- /dev/null +++ b/src/lib/components/feedback/index.ts @@ -0,0 +1,3 @@ +export { default as Feedback } from './feedback.svelte'; +export { default as FeedbackGeneral } from './feedbackGeneral.svelte'; +export { default as FeedbackNPS } from './feedbackNPS.svelte'; diff --git a/src/lib/components/feedbackNPS.svelte b/src/lib/components/feedbackNPS.svelte deleted file mode 100644 index a9276b4a7..000000000 --- a/src/lib/components/feedbackNPS.svelte +++ /dev/null @@ -1,78 +0,0 @@ - - -
-
-

How are we doing?

- -
-
- At Appwrite, we value the feedback of our users. That means you! We'd love to hear what you - think. -
- - - - How likely are you to recommend Appwrite to a friend or colleague? - - {#if value} - - - - - - {/if} -
- - -
- -
diff --git a/src/lib/components/index.ts b/src/lib/components/index.ts index f891b8255..f2cf6e0ee 100644 --- a/src/lib/components/index.ts +++ b/src/lib/components/index.ts @@ -31,9 +31,6 @@ export { default as Box } from './box.svelte'; export { default as Search } from './search.svelte'; export { default as SearchQuery } from './searchQuery.svelte'; export { default as GridItem1 } from './gridItem1.svelte'; -export { default as FeedbackGeneral } from './feedbackGeneral.svelte'; -export { default as FeedbackNPS } from './feedbackNPS.svelte'; -export { default as Evaluation } from './evaluation.svelte'; export { default as Steps } from './steps.svelte'; export { default as Step } from './step.svelte'; export { default as Code } from './code.svelte'; diff --git a/src/lib/components/tab.svelte b/src/lib/components/tab.svelte index 246d9db0d..f3825acbf 100644 --- a/src/lib/components/tab.svelte +++ b/src/lib/components/tab.svelte @@ -1,19 +1,95 @@
  • {#if href} - + {:else} @@ -22,7 +98,10 @@ class="tabs-button" class:is-selected={selected} on:click|preventDefault - on:click={track}> + on:click={handleClick} + tabindex={selected ? 0 : -1} + on:keydown={handleKeyDown} + role="tab"> {/if} diff --git a/src/lib/helpers/style.ts b/src/lib/helpers/style.ts new file mode 100644 index 000000000..fef15c8cc --- /dev/null +++ b/src/lib/helpers/style.ts @@ -0,0 +1,16 @@ +type Direction = 'rtl' | 'ltr'; + +function isDirection(dir: string): dir is Direction { + return dir === 'rtl' || dir === 'ltr'; +} + +function parseDirection(dir: string): Direction { + return isDirection(dir) ? dir : 'ltr'; +} + +export function getElementDir(el: HTMLElement): Direction { + if (window.getComputedStyle) { + return parseDirection(window.getComputedStyle(el, null).getPropertyValue('direction')); + } + return parseDirection(el.style.direction); +} diff --git a/src/lib/helpers/waitUntil.ts b/src/lib/helpers/waitUntil.ts new file mode 100644 index 000000000..c3b3a85e4 --- /dev/null +++ b/src/lib/helpers/waitUntil.ts @@ -0,0 +1,14 @@ +export async function waitUntil(condition: () => boolean, timeout = 1000) { + return new Promise((resolve, reject) => { + const start = Date.now(); + const interval = setInterval(() => { + if (condition()) { + clearInterval(interval); + resolve(undefined); + } else if (Date.now() - start > timeout) { + clearInterval(interval); + reject(new Error('Timeout')); + } + }, 10); + }); +} diff --git a/src/lib/images/feedback/feedback-dark.svg b/src/lib/images/feedback/feedback-dark.svg new file mode 100644 index 000000000..8f0b553d6 --- /dev/null +++ b/src/lib/images/feedback/feedback-dark.svg @@ -0,0 +1,138 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/lib/images/feedback/feedback-light.svg b/src/lib/images/feedback/feedback-light.svg new file mode 100644 index 000000000..d7ace402f --- /dev/null +++ b/src/lib/images/feedback/feedback-light.svg @@ -0,0 +1,126 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/lib/layout/header.svelte b/src/lib/layout/header.svelte index e9ec88e7f..099c1411f 100644 --- a/src/lib/layout/header.svelte +++ b/src/lib/layout/header.svelte @@ -1,26 +1,20 @@ @@ -122,6 +136,24 @@ Storage
  • +
  • + trackEvent('click_menu_settings')} + class:is-selected={$page.url.pathname.startsWith( + `${projectPath}/settings` + )} + title="Settings" + use:tooltip={{ + content: 'Settings', + placement: 'right', + disabled: !narrow + }}> + +
  • @@ -129,7 +161,7 @@ {/if} diff --git a/src/lib/stores/app.ts b/src/lib/stores/app.ts index 11afe9d6a..7c4b2a184 100644 --- a/src/lib/stores/app.ts +++ b/src/lib/stores/app.ts @@ -1,5 +1,4 @@ import { browser } from '$app/environment'; -import { VARS } from '$lib/system'; import { writable } from 'svelte/store'; export type AppStore = { @@ -7,89 +6,11 @@ export type AppStore = { themeInUse: 'light' | 'dark'; }; -export type Feedback = { - elapsed: number; - visualized: number; - notification: boolean; - type: 'nps' | 'general'; - show: boolean; -}; - export const app = writable({ theme: 'auto', themeInUse: 'light' }); -function createFeedbackStore() { - const { subscribe, update } = writable({ - elapsed: browser ? parseInt(localStorage.getItem('feedbackElapsed')) : 0, - visualized: browser ? parseInt(localStorage.getItem('feedbackVisualized')) : 0, - notification: false, - type: 'general', - show: false - }); - return { - subscribe, - update, - toggleFeedback: () => { - update((feedback) => { - feedback.show = !feedback.show; - return feedback; - }); - }, - toggleNotification: () => - update((feedback) => { - feedback.notification = !feedback.notification; - return feedback; - }), - switchType: (feedType: Feedback['type']) => - update((feedback) => { - feedback.type = feedType; - return feedback; - }), - addVisualization: () => - update((feedback) => { - feedback.visualized += 1; - localStorage.setItem('feedbackVisualized', feedback.visualized.toString()); - return feedback; - }), - - increaseElapsed: (time: number) => { - update((feedback) => { - feedback.elapsed += time; - localStorage.setItem('feedbackElapsed', feedback.elapsed.toString()); - return feedback; - }); - }, - submitFeedback: async ( - subject: string, - message: string, - firstname?: string, - email?: string, - value?: number - ) => { - if (!VARS.GROWTH_ENDPOINT) return; - const response = await fetch(`${VARS.GROWTH_ENDPOINT}/feedback`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ - subject, - message, - email, - firstname: firstname ? firstname : undefined, - customFields: value ? [{ id: '40655', value }] : undefined - }) - }); - if (response.status >= 400) { - throw new Error('Failed to submit feedback'); - } - } - }; -} -export const feedback = createFeedbackStore(); - if (browser) { app.update((n) => ({ ...n, diff --git a/src/lib/stores/feedback.ts b/src/lib/stores/feedback.ts new file mode 100644 index 000000000..d63e1d2ec --- /dev/null +++ b/src/lib/stores/feedback.ts @@ -0,0 +1,139 @@ +import { browser } from '$app/environment'; +import { VARS } from '$lib/system'; +import { writable } from 'svelte/store'; +import type { SvelteComponent } from 'svelte'; +import FeedbackGeneral from '$lib/components/feedback/feedbackGeneral.svelte'; +import FeedbackNps from '$lib/components/feedback/feedbackNPS.svelte'; + +export type Feedback = { + elapsed: number; + visualized: number; + notification: boolean; + type: 'nps' | 'general'; + show: boolean; +}; + +export type FeedbackData = { + message: string; + name?: string; + email?: string; + value?: number; +}; + +export type FeedbackOption = { + type: Feedback['type']; + title: string; + desc: string; + component: typeof SvelteComponent; +}; + +export const feedbackOptions: FeedbackOption[] = [ + { + type: 'general', + title: 'How can we improve?', + desc: 'Your feedback is important to us. Please be honest and tell us what you think.', + component: FeedbackGeneral + }, + { + type: 'nps', + title: 'How are we doing?', + desc: "At Appwrite, we value the feedback of our users. That means you! We'd love to hear what you think.", + component: FeedbackNps + } +]; + +export const selectedFeedback = writable(); + +function createFeedbackDataStore() { + const { subscribe, update } = writable({ + message: '', + name: '', + email: '', + value: 0 + }); + return { + subscribe, + update, + reset: () => { + update((feedbackData) => { + feedbackData.message = ''; + feedbackData.name = ''; + feedbackData.email = ''; + feedbackData.value = 0; + return feedbackData; + }); + } + }; +} + +export const feedbackData = createFeedbackDataStore(); + +function createFeedbackStore() { + const { subscribe, update } = writable({ + elapsed: browser ? parseInt(localStorage.getItem('feedbackElapsed')) : 0, + visualized: browser ? parseInt(localStorage.getItem('feedbackVisualized')) : 0, + notification: false, + type: 'general', + show: false + }); + return { + subscribe, + update, + toggleFeedback: () => { + update((feedback) => { + feedback.show = !feedback.show; + return feedback; + }); + }, + toggleNotification: () => + update((feedback) => { + feedback.notification = !feedback.notification; + return feedback; + }), + switchType: (feedType: Feedback['type']) => + update((feedback) => { + feedback.type = feedType; + return feedback; + }), + addVisualization: () => + update((feedback) => { + feedback.visualized += 1; + localStorage.setItem('feedbackVisualized', feedback.visualized.toString()); + return feedback; + }), + + increaseElapsed: (time: number) => { + update((feedback) => { + feedback.elapsed += time; + localStorage.setItem('feedbackElapsed', feedback.elapsed.toString()); + return feedback; + }); + }, + submitFeedback: async ( + subject: string, + message: string, + firstname?: string, + email?: string, + value?: number + ) => { + if (!VARS.GROWTH_ENDPOINT) return; + const response = await fetch(`${VARS.GROWTH_ENDPOINT}/feedback`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + subject, + message, + email, + firstname: firstname ? firstname : undefined, + customFields: value ? [{ id: '40655', value }] : undefined + }) + }); + if (response.status >= 400) { + throw new Error('Failed to submit feedback'); + } + } + }; +} +export const feedback = createFeedbackStore(); diff --git a/src/lib/stores/oauth-providers.ts b/src/lib/stores/oauth-providers.ts index 8047d6bdb..40bcff26a 100644 --- a/src/lib/stores/oauth-providers.ts +++ b/src/lib/stores/oauth-providers.ts @@ -7,6 +7,7 @@ import Okta from '../../routes/console/project-[project]/auth/oktaOAuth.svelte'; import Auth0 from '../../routes/console/project-[project]/auth/auth0OAuth.svelte'; import Authentik from '../../routes/console/project-[project]/auth/authentikOAuth.svelte'; import GitLab from '../../routes/console/project-[project]/auth/gitlabOAuth.svelte'; +import Google from '../../routes/console/project-[project]/auth/googleOAuth.svelte'; import Main from '../../routes/console/project-[project]/auth/mainOAuth.svelte'; export type Provider = Models.Provider & { @@ -81,6 +82,7 @@ const setProviders = (project: Models.Project): Provider[] => { break; case 'google': docs = 'https://support.google.com/googleapi/answer/6158849'; + component = Google; break; case 'linkedin': docs = 'https://developer.linkedin.com/'; diff --git a/src/routes/auth/oauth2/failure/+page.svelte b/src/routes/auth/oauth2/failure/+page.svelte index 359c6ffdc..928ad82c4 100644 --- a/src/routes/auth/oauth2/failure/+page.svelte +++ b/src/routes/auth/oauth2/failure/+page.svelte @@ -2,20 +2,56 @@ import { goto } from '$app/navigation'; import { page } from '$app/stores'; import { Heading } from '$lib/components'; - import { onMount } from 'svelte'; - onMount(async () => { - const project = $page.url.searchParams.get('project'); - if (project) { - await goto(`appwrite-callback-${project}://${$page.url.search}`); + const project = $page.url.searchParams.get('project'); + const link = `appwrite-callback-${project}://${$page.url.search}`; + + const redirect = new Promise((resolve, reject) => { + if (!project) { + reject('no-project'); } + // this timeout is needed because goto does not + // throw an exception if the redirect does not work + setTimeout(() => reject('timeout'), 500); + // goto will resolve on successful redirect + goto(link).then(resolve); }); -Missing Redirect URL -

    - Your OAuth login flow is missing a proper redirect URL. Please check the - OAuth docs - and send request for new session with a valid callback URL. -

    +{#await redirect then} +
    +
    + Login failed +

    You will be automatically redirected back to your app shortly.

    +

    + If you are not redirected, please click on the following + link. +

    +
    +
    +{:catch} +
    +
    + Missing Redirect URL +

    + Your OAuth login flow is missing a proper redirect URL. Please check the + OAuth docs + and send request for new session with a valid callback URL. +

    +
    +
    +{/await} + + diff --git a/src/routes/auth/oauth2/success/+page.svelte b/src/routes/auth/oauth2/success/+page.svelte index 359c6ffdc..b67ad0ce6 100644 --- a/src/routes/auth/oauth2/success/+page.svelte +++ b/src/routes/auth/oauth2/success/+page.svelte @@ -2,20 +2,56 @@ import { goto } from '$app/navigation'; import { page } from '$app/stores'; import { Heading } from '$lib/components'; - import { onMount } from 'svelte'; - onMount(async () => { - const project = $page.url.searchParams.get('project'); - if (project) { - await goto(`appwrite-callback-${project}://${$page.url.search}`); + const project = $page.url.searchParams.get('project'); + const link = `appwrite-callback-${project}://${$page.url.search}`; + + const redirect = new Promise((resolve, reject) => { + if (!project) { + reject('no-project'); } + // this timeout is needed because goto does not + // throw an exception if the redirect does not work + setTimeout(() => reject('timeout'), 500); + // goto will resolve on successful redirect + goto(link).then(resolve); }); -Missing Redirect URL -

    - Your OAuth login flow is missing a proper redirect URL. Please check the - OAuth docs - and send request for new session with a valid callback URL. -

    +{#await redirect then} +
    +
    + You're now logged in +

    You will be automatically redirected back to your app shortly.

    +

    + If you are not redirected, please click on the following + link. +

    +
    +
    +{:catch} +
    +
    + Missing Redirect URL +

    + Your OAuth login flow is missing a proper redirect URL. Please check the + OAuth docs + and send request for new session with a valid callback URL. +

    +
    +
    +{/await} + + diff --git a/src/routes/console/+layout.svelte b/src/routes/console/+layout.svelte index 870794b40..38e74b324 100644 --- a/src/routes/console/+layout.svelte +++ b/src/routes/console/+layout.svelte @@ -7,7 +7,7 @@ import Header from '$lib/layout/header.svelte'; import SideNavigation from '$lib/layout/navigation.svelte'; import Shell from '$lib/layout/shell.svelte'; - import { feedback } from '$lib/stores/app'; + import { feedback } from '$lib/stores/feedback'; import { log } from '$lib/stores/logs'; import { newOrgModal } from '$lib/stores/organization'; import { wizard } from '$lib/stores/wizard'; @@ -15,6 +15,8 @@ import { loading } from '../store'; import Create from './createOrganization.svelte'; + let isOpen = false; + onMount(() => { loading.set(false); @@ -47,12 +49,13 @@
    - +