This commit is contained in:
2023-06-21 20:18:19 +02:00
parent c1c58118a2
commit c1faa879a3
7 changed files with 99 additions and 47 deletions
+13
View File
@@ -12,6 +12,7 @@
"@vavite/node-loader": "^1.8.1",
"font-color-contrast": "^11.1.0",
"lodash": "^4.17.21",
"plotly.js-dist": "^2.24.2",
"qs": "^6.11.2",
"rollup-plugin-copy": "^3.4.0",
"svelte-awesome-color-picker": "^2.4.4",
@@ -21,6 +22,7 @@
"@sveltejs/adapter-auto": "^2.0.0",
"@sveltejs/kit": "^1.5.0",
"@types/lodash": "^4.14.195",
"@types/plotly.js": "^2.12.18",
"@types/qs": "^6.9.7",
"@typescript-eslint/eslint-plugin": "^5.45.0",
"@typescript-eslint/parser": "^5.45.0",
@@ -716,6 +718,12 @@
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.2.6.tgz",
"integrity": "sha512-GQBWUtGoefMEOx/vu+emHEHU5aw6JdDoEtZhoBrHFPZbA/YNRFfN996XbBASEWdvmLSLyv9FKYppYGyZjCaq/g=="
},
"node_modules/@types/plotly.js": {
"version": "2.12.18",
"resolved": "https://registry.npmjs.org/@types/plotly.js/-/plotly.js-2.12.18.tgz",
"integrity": "sha512-ff+CIEWnqZNjZqHtQZvkEAVuLs9fkm1f54QnPVmgoET7wMHdSqUka2hasVN4e5yfHD05YwGjsAtCseewJh/BMw==",
"dev": true
},
"node_modules/@types/pug": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/@types/pug/-/pug-2.0.6.tgz",
@@ -2761,6 +2769,11 @@
"node": ">= 6"
}
},
"node_modules/plotly.js-dist": {
"version": "2.24.2",
"resolved": "https://registry.npmjs.org/plotly.js-dist/-/plotly.js-dist-2.24.2.tgz",
"integrity": "sha512-J5Jg6mAkMwzjHsfLlZaZiS11ntKvdkyP5C1Re2SFL5dXL7Nb66BGGpu3UopbIUaopLVJBiYu9kvOfUTjy0rHFQ=="
},
"node_modules/postcss": {
"version": "8.4.24",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.24.tgz",
+2
View File
@@ -16,6 +16,7 @@
"@sveltejs/adapter-auto": "^2.0.0",
"@sveltejs/kit": "^1.5.0",
"@types/lodash": "^4.14.195",
"@types/plotly.js": "^2.12.18",
"@types/qs": "^6.9.7",
"@typescript-eslint/eslint-plugin": "^5.45.0",
"@typescript-eslint/parser": "^5.45.0",
@@ -42,6 +43,7 @@
"@vavite/node-loader": "^1.8.1",
"font-color-contrast": "^11.1.0",
"lodash": "^4.17.21",
"plotly.js-dist": "^2.24.2",
"qs": "^6.11.2",
"rollup-plugin-copy": "^3.4.0",
"svelte-awesome-color-picker": "^2.4.4",
-1
View File
@@ -2,7 +2,6 @@ import { metricsDb } from '$lib/server/metrics';
import { tests as testsStore, type Test, userTests } from '$lib/server/tests'
import { get } from 'svelte/store'
export async function load({ cookies, params }) {
// Set a random session ID cookie if there is none, so we can uniquely
// identify the user and not randomly change their experience on every page load.
-25
View File
@@ -81,28 +81,3 @@ export const actions = {
return { success: true }
}
}
type StructuredFormData =
| string
| boolean
| number
| File
| StructuredFormData[];
function formBody(body: FormData) {
return [...body.entries()].reduce((data, [k, v]) => {
let value: StructuredFormData = v;
if (v === "true") value = true;
if (v === "false") value = false;
if (!isNaN(Number(v))) value = Number(v);
// For grouped fields like multi-selects and checkboxes, we need to
// store the values in an array.
if (k in data) {
const val = data[k];
value = Array.isArray(val) ? [...val, value] : [val, value];
}
data[k] = value;
return data;
}, {} as Record<string, StructuredFormData>);
}
+22 -2
View File
@@ -2,7 +2,6 @@
import { enhance } from '$app/forms'
import { page } from '$app/stores'
import type { Test } from '$lib/server/tests'
import { tick } from 'svelte'
import AB from './AB.svelte'
export let data
@@ -37,6 +36,27 @@
otherTests.forEach((t) => (t.chance = Math.round(Math.max(0, Math.min(t.chance + diffPerTest, 1) * 100)) / 100))
}
// Calculate max views and duration across all tests so we can scale the charts accordingly
let globalAnalytics: {
maxViews: number
maxDuration: number
}
$: globalAnalytics = data.tests.reduce(
(acc, test) => {
if (test.analytics) {
if (test.analytics?.views > acc.maxViews) {
acc.maxViews = test.analytics.views
}
if (test.analytics?.duration > acc.maxDuration) {
acc.maxDuration = test.analytics.duration
}
}
return acc
},
{ maxViews: 0, maxDuration: 0 }
)
// Prevent accidental form submission on enter
function preventSubmit(e: KeyboardEvent) {
if (e.key === 'Enter') {
e.preventDefault()
@@ -52,7 +72,7 @@
<div class="mt-6">
<div class="flex flex-col gap-6">
{#each data.tests as test, index (test.id)}
<AB i={index} bind:data={test} on:delete={() => deleteTest(index)} on:chanceChanged={() => onChanceChanged(test)} />
<AB i={index} bind:data={test} bind:globalAnalytics on:delete={() => deleteTest(index)} on:chanceChanged={() => onChanceChanged(test)} />
{/each}
</div>
</div>
+60 -16
View File
@@ -3,12 +3,13 @@
import Editor from '@tinymce/tinymce-svelte'
import { createEventDispatcher } from 'svelte'
import type { Test } from '$lib/server/tests'
import { tick } from 'svelte'
import { onMount } from 'svelte'
const dispatch = createEventDispatcher()
export let i: number
export let data: Test & { analytics?: { views: number; duration: number } }
export let globalAnalytics: { maxViews: number; maxDuration: number }
function deleteSelf(this: HTMLButtonElement) {
if (this.innerHTML !== 'Delete?') {
@@ -33,19 +34,56 @@
}
const enabled = data.enabled
let chartViews: HTMLDivElement
let chartDuration: HTMLDivElement
onMount(async () => {
const Plotly = await import('plotly.js-dist')
Plotly.newPlot(chartViews, [
{
domain: { x: [0, 1], y: [0, 1] },
value: data.analytics?.views,
title: { text: 'Page Views' },
type: 'indicator',
mode: 'gauge+number',
gauge: {
axis: { range: [0, globalAnalytics.maxViews] },
bgcolor: 'white',
}
},
], {
width: 400, height: 230, margin: { t: 0, b: 0 }, paper_bgcolor: 'transparent'
})
Plotly.newPlot(chartDuration, [
{
domain: { x: [0, 1], y: [0, 1] },
value: data.analytics?.duration,
title: { text: 'Average Duration (s)' },
type: 'indicator',
mode: 'gauge+number',
gauge: {
axis: { range: [0, globalAnalytics.maxDuration] },
bgcolor: 'white',
}
},
], {
width: 400, height: 230, margin: { t: 0, b: 0 }, paper_bgcolor: 'transparent'
})
})
</script>
<details class="collapse collapse-arrow rounded-lg" open={enabled} >
<summary class="collapse-title text-xl font-medium leading-tight bg-base-300 min-h-min">
<div class="flex items-center gap-2">
<input type="checkbox" name="test[{i}][enabled]" class="toggle toggle-accent" bind:checked={data.enabled} />
<input type="hidden" name="test[{i}][enabled]" value='off' />
<span style="color: {data.color}">{data.title}</span>
<!-- <span>{data.id}</span> -->
<input type="range" name="test[{i}][chance]" class="range max-w-xs ml-20" min="0" max="1" step="0.01" bind:value={data.chance} on:input={() => chancePercent = Math.round(data.chance * 100)} on:input={chanceChanged} />
<input type="text" class="input input-ghost w-16" bind:value={chancePercent} on:input={() => data.chance = chancePercent / 100} on:input={chanceChanged} />
<!-- <span>{Math.round(data.chance * 100)}%</span> -->
<!-- <span>{data.chance}</span> -->
<details class="collapse-arrow collapse rounded-lg" open={enabled}>
<summary class="collapse-title min-h-min bg-base-300 text-xl font-medium leading-tight">
<div class="flex gap-2">
<div class="flex items-center basis-1/4 flex-none overflow-hidden">
<input type="checkbox" name="test[{i}][enabled]" class="toggle-accent toggle" bind:checked={data.enabled} />
<input type="hidden" name="test[{i}][enabled]" value="off" />
<span class="ml-2 overflow-hidden text-ellipsis" style="color: {data.color}">{data.title}</span>
</div>
<div class="flex items-center">
<input type="range" name="test[{i}][chance]" class="range max-w-xs" min="0" max="1" step="0.01" bind:value={data.chance} on:input={() => (chancePercent = Math.round(data.chance * 100))} on:input={chanceChanged} />
<input type="text" class="input-ghost input w-16" bind:value={chancePercent} on:input={() => (data.chance = chancePercent / 100)} on:input={chanceChanged} />
</div>
</div>
</summary>
<div class="collapse-content flex w-full flex-col flex-wrap gap-4 bg-base-200">
@@ -62,7 +100,8 @@
<div class="grow">
<div class="form-control w-full">
<label class="label">
<span class="label-text">Title
<span class="label-text"
>Title
{#if data.title.length == 0}
<span class="text-red-500"> (required)</span>
{/if}
@@ -73,7 +112,8 @@
<div class="form-control w-full">
<label class="label">
<span class="label-text">Body
<span class="label-text"
>Body
{#if data.body.length == 0}
<span class="text-red-500"> (required)</span>
{/if}
@@ -97,8 +137,12 @@
{#if data.analytics}
<div>
<h1 class="text-2xl font-bold">Analytics</h1>
<p>Page views: {data.analytics.views}</p>
<p>Average duration: {isNaN(data.analytics.duration) ? '???' : Math.round(data.analytics.duration)} sec</p>
<div class="flex flex-row justify-center">
<div bind:this={chartViews} />
<div bind:this={chartDuration} />
</div>
<!-- <p>Page views: {data.analytics.views}</p>
<p>Average duration: {isNaN(data.analytics.duration) ? '???' : Math.round(data.analytics.duration)} sec</p> -->
</div>
{/if}
</div>
@@ -1,5 +1,4 @@
import { metricsDb } from "$lib/server/metrics";
import { json } from "@sveltejs/kit";
import { metricsDb } from "$lib/server/metrics"
export async function POST({ params, cookies, request }) {
// Update metrics page view end time