Merge pull request #48 from appwrite/feedback

draft: feedback widget
This commit is contained in:
Torsten Dittmann
2022-11-14 22:33:35 +01:00
committed by GitHub
11 changed files with 352 additions and 15 deletions
+18 -9
View File
@@ -7,20 +7,29 @@
export let scrollable = false;
export let childStart = false;
export let noArrow = false;
export let width: string = null;
</script>
<Drop bind:show {placement} {childStart} {noArrow}>
<slot />
<svelte:fragment slot="list">
<div class="drop is-no-arrow" style="position: revert">
<section
class:u-overflow-y-auto={scrollable}
class:u-max-height-200={scrollable}
class="drop-section ">
<ul class="drop-list">
<slot name="list" />
</ul>
</section>
<div
class="drop is-no-arrow"
style={`${
width
? `--drop-width-size-desktop:${width}rem; position:relative`
: ' position:relative'
}`}>
{#if $$slots.list}
<section
class:u-overflow-y-auto={scrollable}
class:u-max-height-200={scrollable}
class="drop-section ">
<ul class="drop-list">
<slot name="list" />
</ul>
</section>
{/if}
<slot name="other" />
</div>
</svelte:fragment>
+27
View File
@@ -0,0 +1,27 @@
<script lang="ts">
import { clickOnEnter } from '$lib/helpers/a11y';
export let value: number = null;
</script>
<fieldset class="u-margin-block-start-8">
<legend class="label is-required u-margin-0 u-line-height-1-5 u-bold">
<slot />
</legend>
<ul class="u-flex u-main-space-between u-margin-block-start-16">
{#each Array(10) as _, i}
<li on:keyup={clickOnEnter} on:click={() => (value = i)}>
<input
type="radio"
name="recommend"
class="radio-button"
data-text={i}
checked={value === i} />
</li>
{/each}
</ul>
<div class="u-flex u-main-space-between u-margin-block-start-8">
<span>Not at all likely</span>
<span>Extremely likely</span>
</div>
</fieldset>
+82
View File
@@ -0,0 +1,82 @@
<script lang="ts">
import {
Form,
FormList,
InputTextarea,
Button,
InputText,
InputEmail
} from '$lib/elements/forms';
import { feedback } from '$lib/stores/app';
import { addNotification } from '$lib/stores/notifications';
export let show = false;
let message: string;
let name: string;
let email: string;
async function handleSubmit() {
try {
await feedback.submitFeedback(
undefined,
email,
message,
['general'],
[{ firstname: name }]
);
show = false;
} catch (error) {
show = false;
addNotification({
type: 'error',
message: error.message
});
}
}
</script>
<section class="drop-section">
<header class="u-flex u-main-space-between u-gap-16">
<h4 class="body-text-1">How can we improve?</h4>
<button
type="button"
class="x-button u-margin-inline-start-auto"
aria-label="Close Modal"
title="Close Modal"
on:click={() => (show = false)}>
<span class="icon-x" aria-hidden="true" />
</button>
</header>
<div class="u-margin-block-start-8 u-line-height-1-5">
Your feedback is important to us. Please be honest and tell us what you think.
</div>
<Form on:submit={handleSubmit}>
<FormList>
<InputText
label="name"
id="name"
bind:value={name}
placeholder="Enter name"
showLabel={false} />
<InputEmail
label="email"
id="email"
bind:value={email}
placeholder="Enter email"
showLabel={false} />
<InputTextarea
id="feedback"
placeholder="Your message here"
showLabel={false}
label="Feedback"
required
bind:value={message} />
</FormList>
<div class="u-flex u-main-end u-gap-16 u-margin-block-start-24">
<Button text on:click={() => (show = false)}>Cancel</Button>
<Button secondary submit>Submit</Button>
</div>
</Form>
</section>
+82
View File
@@ -0,0 +1,82 @@
<script lang="ts">
import {
Form,
FormList,
InputTextarea,
Button,
InputText,
InputEmail
} from '$lib/elements/forms';
import { feedback } from '$lib/stores/app';
import { addNotification } from '$lib/stores/notifications';
import Evaluation from './evaluation.svelte';
export let show = false;
let value: number = null;
let message: string;
let email: string;
let name: string;
async function handleSubmit() {
try {
await feedback.submitFeedback(undefined, email, message, ['npm'], [{ value, name }]);
console.log(value, message);
} catch (error) {
addNotification({
type: 'error',
message: error.message
});
}
}
</script>
<section class="drop-section">
<header class="u-flex u-main-space-between u-gap-16">
<h4 class="body-text-1">How are we doing?</h4>
<button
type="button"
class="x-button u-margin-inline-start-auto"
aria-label="Close Modal"
title="Close Modal"
on:click={() => (show = false)}>
<span class="icon-x" aria-hidden="true" />
</button>
</header>
<div class="u-margin-block-start-8 u-line-height-1-5">
At Appwrite, we value the feedback of our users. That means you! We'd love to hear what you
think.
</div>
<Form on:submit={handleSubmit}>
<Evaluation bind:value>
How likely are you to recommend Appwrite to a friend or colleague?
</Evaluation>
{#if value}
<FormList>
<InputTextarea
id="feedback"
placeholder="Your message here"
label="Feedback"
required
bind:value={message}
showLabel={false} />
<InputText
label="name"
id="name"
bind:value={name}
placeholder="Enter name"
showLabel={false} />
<InputEmail
label="email"
id="email"
bind:value={email}
placeholder="Enter email"
showLabel={false} />
</FormList>
{/if}
<div class="u-flex u-main-end u-gap-16 u-margin-block-start-24">
<Button text on:click={() => (show = false)}>No thanks</Button>
<Button disabled={!value} secondary submit>Submit</Button>
</div>
</Form>
</section>
+3
View File
@@ -31,6 +31,9 @@ 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';
+2
View File
@@ -1,5 +1,7 @@
export const PAGE_LIMIT = 12; // default page limit
export const CARD_LIMIT = 6; // default card limit
export const INTERVAL = 5 * 60000; // default interval to check for feedback
export enum Dependencies {
ORGANIZATION = 'dependency:organization',
PROJECT = 'dependency:project',
+38 -5
View File
@@ -1,27 +1,44 @@
<script lang="ts">
import { base } from '$app/paths';
import { AvatarInitials, DropListItem, DropListLink } from '$lib/components';
import { app } from '$lib/stores/app';
import {
AvatarInitials,
DropList,
DropListItem,
DropListLink,
FeedbackGeneral
} from '$lib/components';
import { app, feedback } from '$lib/stores/app';
import { user } from '$lib/stores/user';
import { organizationList, organization, newOrgModal } from '$lib/stores/organization';
import AppwriteLogo from '$lib/images/appwrite-gray-light.svg';
import LightMode from '$lib/images/mode/light-mode.svg';
import DarkMode from '$lib/images/mode/dark-mode.svg';
import SystemMode from '$lib/images/mode/system-mode.svg';
import { FeedbackNPS } from '$lib/components';
let showFeedback = false;
import { slide } from 'svelte/transition';
import { page } from '$app/stores';
let showDropdown = false;
let droplistElement: HTMLDivElement;
const onBlur = (event: MouseEvent) => {
function toggleFeedback() {
showFeedback = !showFeedback;
if ($feedback.notification) {
feedback.toggleNotification();
feedback.addVisualization();
}
}
function onBlur(event: MouseEvent) {
if (
showDropdown &&
!(event.target === droplistElement || droplistElement.contains(event.target as Node))
) {
showDropdown = false;
}
};
}
</script>
<svelte:window on:click={onBlur} />
@@ -38,7 +55,23 @@
<div class="main-header-end">
<nav class="u-flex is-only-desktop">
<button class="button is-small is-text"><span class="text">Feedback</span></button>
{#if $feedback.notification}
<div class="u-flex u-cross-center">
<div class="pulse-notification" />
</div>
{/if}
<DropList width="28" bind:show={showFeedback} scrollable={true}>
<button class="button is-small is-text" on:click={toggleFeedback}>
<span class="text">Feedback</span>
</button>
<svelte:fragment slot="other">
{#if $feedback.notification}
<FeedbackNPS bind:show={showFeedback} />
{:else}
<FeedbackGeneral bind:show={showFeedback} />
{/if}
</svelte:fragment>
</DropList>
<a
href="https://appwrite.io/support"
target="_blank"
+12
View File
@@ -0,0 +1,12 @@
<script lang="ts">
import { DropList } from '$lib/components';
export let show = false;
</script>
<DropList bind:show>
<button id="button" on:click={() => (show = !show)}>test</button>
<svelte:fragment slot="list"><li id="list">list</li></svelte:fragment>
<svelte:fragment slot="other"><span id="other">other</span></svelte:fragment>
</DropList>
+70
View File
@@ -5,11 +5,81 @@ export type AppStore = {
theme: 'light' | 'dark' | 'auto';
themeInUse: 'light' | 'dark';
};
export type Feedback = {
elapsed: number;
visualized: number;
notification: boolean;
type: 'nps' | 'general';
};
export const app = writable<AppStore>({
theme: 'auto',
themeInUse: 'light'
});
function createFeedbackStore() {
const { subscribe, update } = writable<Feedback>({
elapsed: browser ? parseInt(localStorage.getItem('feedbackElapsed')) : 0,
visualized: browser ? parseInt(localStorage.getItem('feedbackVisualized')) : 0,
notification: false,
type: 'nps'
});
return {
subscribe,
update,
toggleNotification: () =>
update((feedback) => {
feedback.notification = !feedback.notification;
return feedback;
}),
switchType: () =>
update((feedback) => {
feedback.type = feedback.type === 'nps' ? 'general' : 'nps';
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,
email?: string,
message?: string,
tags?: string[],
customFields?: Record<string, unknown>[]
) => {
const response = await fetch('https://growth.appwrite.io/v1/feedback', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
subject,
email,
message,
tags,
customFields
})
});
if (response.status !== 200) {
throw new Error('Failed to submit feedback');
}
}
};
}
export const feedback = createFeedbackStore();
if (browser) {
app.update((n) => ({
...n,
+17
View File
@@ -11,11 +11,28 @@
import { log } from '$lib/stores/logs';
import { onMount } from 'svelte';
import { loading } from '../store';
import { feedback } from '$lib/stores/app';
import { INTERVAL } from '$lib/constants';
onMount(() => {
loading.set(false);
setInterval(() => {
checkForFeedback(INTERVAL);
}, INTERVAL);
});
function checkForFeedback(interval: number) {
const minutes = interval / 60000;
feedback.increaseElapsed(minutes);
const hours = Math.floor($feedback.elapsed / 60);
if (hours >= 1 && hours < 10 && $feedback.visualized < 1) {
feedback.toggleNotification();
} else if (hours >= $feedback.visualized * 10) {
feedback.toggleNotification();
}
}
beforeNavigate(() => {
$log.show = false;
});
+1 -1
View File
@@ -1,6 +1,6 @@
import '@testing-library/jest-dom';
import { render, fireEvent } from '@testing-library/svelte';
import { DropList } from '../../../src/lib/components';
import DropList from '../../../src/lib/mock/drop.test.svelte';
const data = {
show: true