wip: filter handling

This commit is contained in:
Wlad Meixner
2023-07-03 18:44:07 +02:00
parent 80e07450da
commit 9efb9faa01
14 changed files with 458 additions and 59 deletions
+2
View File
@@ -34,8 +34,10 @@
"type": "module",
"dependencies": {
"@sveltejs/adapter-static": "^2.0.2",
"@types/papaparse": "^5.3.7",
"autoprefixer": "^10.4.14",
"feather-icons": "^4.29.0",
"papaparse": "^5.4.1",
"postcss": "^8.4.24",
"sass": "^1.63.6",
"svelte-feather-icons": "^4.0.1",
+20
View File
@@ -1,15 +1,25 @@
lockfileVersion: '6.0'
settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
dependencies:
'@sveltejs/adapter-static':
specifier: ^2.0.2
version: 2.0.2(@sveltejs/kit@1.20.5)
'@types/papaparse':
specifier: ^5.3.7
version: 5.3.7
autoprefixer:
specifier: ^10.4.14
version: 10.4.14(postcss@8.4.24)
feather-icons:
specifier: ^4.29.0
version: 4.29.0
papaparse:
specifier: ^5.4.1
version: 5.4.1
postcss:
specifier: ^8.4.24
version: 8.4.24
@@ -472,6 +482,12 @@ packages:
/@types/node@20.3.1:
resolution: {integrity: sha512-EhcH/wvidPy1WeML3TtYFGR83UzjxeWRen9V402T8aUGYsCHOmfoisV3ZSg03gAFIbLq8TnWOJ0f4cALtnSEUg==}
/@types/papaparse@5.3.7:
resolution: {integrity: sha512-f2HKmlnPdCvS0WI33WtCs5GD7X1cxzzS/aduaxSu3I7TbhWlENjSPs6z5TaB9K0J+BH1jbmqTaM+ja5puis4wg==}
dependencies:
'@types/node': 20.3.1
dev: false
/@types/pug@2.0.6:
resolution: {integrity: sha512-SnHmG9wN1UVmagJOnyo/qkk0Z7gejYxOYYmaAwr5u2yFYfsupN3sg10kyzN8Hep/2zbHxCnsumxOoRIRMBwKCg==}
dev: true
@@ -1507,6 +1523,10 @@ packages:
p-limit: 3.1.0
dev: true
/papaparse@5.4.1:
resolution: {integrity: sha512-HipMsgJkZu8br23pW15uvo6sib6wne/4woLZPlFf3rpDyMe9ywEXUsuD7+6K9PRkJlVT51j/sCOYDKGGS3ZJrw==}
dev: false
/parent-module@1.0.1:
resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==}
engines: {node: '>=6'}
+13
View File
@@ -0,0 +1,13 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
/* Smooth scroll on href */
html {
scroll-behavior: smooth;
}
body,
html {
min-height: 100vh;
}
+1 -1
View File
@@ -6,7 +6,7 @@
<meta name="viewport" content="width=device-width" />
%sveltekit.head%
</head>
<body data-sveltekit-preload-data="hover">
<body data-sveltekit-preload-data="hover" class="bg-secondary-100">
<div style="display: contents">%sveltekit.body%</div>
</body>
</html>
+14 -27
View File
@@ -1,50 +1,37 @@
<script lang="ts">
import { onMount } from 'svelte';
import { writable } from 'svelte/store';
import { Theme, appSettingsStore, toggleThemeMode, updateTheme } from '../store/SettingsStore';
let className: string | undefined = '';
export { className as class };
const theme = writable('light'); // Initial theme
// Switch theme function
function toggleTheme() {
theme.update((currentTheme) => (currentTheme === 'light' ? 'dark' : 'light'));
}
let theme: Theme;
onMount(() => {
// Check for previously saved theme in local storage
// const savedTheme = localStorage.getItem('theme');
// if (savedTheme) {
// theme.set(savedTheme);
// }
appSettingsStore.subscribe((value) => {
theme = value.theme;
});
});
$: {
// Save the current theme to local storage
// localStorage.setItem('theme', $theme);
}
</script>
<div class="sidebar {className} {theme}">
<div class="p-4">
<!-- Sidebar content goes here -->
<slot />
<div>
<!-- Sidebar content goes here -->
<slot />
</div>
<div class="flex items-center mt-4">
<label for="themeToggle" class="flex items-center cursor-pointer">
<span class="mr-2">Dark Mode</span>
<input
type="checkbox"
id="themeToggle"
class="hidden"
on:change={toggleTheme}
checked={$theme === 'dark'}
/>
<input type="checkbox" id="themeToggle" class="hidden" on:change={toggleThemeMode} />
<span class="relative inline-block w-10 h-6 transition bg-gray-300 rounded-full">
<span
class="absolute top-0 left-0 w-6 h-6 transition transform bg-white rounded-full shadow-md"
style="transform: translateX({$theme === 'dark' ? 'calc(100% - 0.75rem)' : '0'})"
style="transform: translateX({$appSettingsStore.theme === Theme.Dark
? 'calc(100% - 0.75rem)'
: '0'})"
/>
</span>
<span class="ml-2">Light Mode</span>
@@ -55,8 +42,8 @@
<style>
.sidebar {
width: 200px;
height: 100vh;
overflow-y: scroll;
transition: background-color 0.3s, color 0.3s;
}
+49
View File
@@ -0,0 +1,49 @@
<script lang="ts">
import type { Action } from '@sveltejs/kit';
export let size: 'sm' | 'md' | 'lg' = 'md';
export let disabled: boolean = false;
export let color: 'primary' | 'secondary' = 'secondary';
let className: string | undefined = undefined;
export { className as class };
function classForSize() {
switch (size) {
case 'sm':
return 'px-2.5 py-1.5 text-xs';
case 'md':
return 'px-4 py-2 text-sm';
case 'lg':
return 'px-4 py-2 text-base';
}
}
function classForColors(): string {
switch (color) {
case 'primary':
return 'text-white bg-primary-600 hover:bg-primary-700 ';
case 'secondary':
return 'text-secondary-600 bg-white hover:bg-secondary-50 hover:bg-gray-50';
}
}
function classForDisabledState(): string {
if (disabled) {
return 'cursor-not-allowed opacity-50 pointer-events-none';
}
return '';
}
</script>
<button
{disabled}
on:click
class="inline-flex justify-between items-center {classForDisabledState()} {classForColors()} {classForSize()} border rounded-xl shadow-sm font-medium focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500 {className ??
''}"
>
<slot name="leading" />
<slot />
<slot name="trailing" />
</button>
+78
View File
@@ -0,0 +1,78 @@
<script lang="ts">
import { onMount, onDestroy } from 'svelte';
import { browser } from '$app/environment';
import Button from './Button.svelte';
import { ChevronDownIcon, ChevronUpIcon } from 'svelte-feather-icons';
export let isOpen: boolean = false;
export let disabled: boolean = false;
export let buttonClass: string | undefined = undefined;
let className: string | undefined = undefined;
export { className as class };
let popoverElement: HTMLDivElement;
function toggleDropdown() {
isOpen = !isOpen;
}
interface $$Slots {
button: {};
content: {};
}
function fadeSlide(node: HTMLElement, options?: { duration?: number }) {
return {
duration: options?.duration || 100,
css: (t: number) => `
transform: translateY(${(1 - t) * -20}px) scale(${0.9 + t * 0.1});
opacity: ${t};
`
};
}
function handleClickOutside(event: MouseEvent) {
if (popoverElement && !popoverElement.contains(event.target as Node)) {
isOpen = false;
}
}
// Close the dropdown when clicking outside
onMount(() => {
if (browser) {
window.addEventListener('click', handleClickOutside);
}
});
onDestroy(() => {
if (browser) {
window.removeEventListener('click', handleClickOutside);
}
});
</script>
<div class="relative {className}" bind:this={popoverElement}>
<Button
class="flex items-center justify-between gap-2 {buttonClass}"
on:click={toggleDropdown}
{disabled}
>
<slot name="button" />
{#if isOpen}
<ChevronDownIcon />
{:else}
<ChevronUpIcon />
{/if}
</Button>
{#if isOpen}
<div
transition:fadeSlide={{ duration: 100 }}
class="z-10 overflow-hidden origin-top-left absolute left-0 mt-2 w-56 rounded-xl shadow-xl bg-white ring-1 ring-black ring-opacity-5 focus:outline-none"
>
<div role="none">
<slot name="content" />
</div>
</div>
{/if}
</div>
+119
View File
@@ -0,0 +1,119 @@
<script lang="ts">
import { CheckCircleIcon, CheckIcon, MailIcon, PhoneIcon } from 'svelte-feather-icons';
import Dropdown from './Dropdown.svelte';
import Button from './Button.svelte';
export let isOpen = false;
export let singular = false;
export let disabled = false;
export let label: string | undefined = undefined;
// Data type
type T = $$Generic;
export let options: { label: string; value: T }[];
export let selected: T[] = [];
// add on select action
export let onSelect: (selected: T[]) => void | undefined;
let buttonText = labelForSelection();
function internalOnSelect(value: T) {
if (singular) {
selected = [value];
onSelect?.(selected);
buttonText = labelForSelection();
return;
}
if (selected.includes(value)) {
selected = selected.filter((v) => v !== value);
} else {
selected = [...selected, value];
}
onSelect?.(selected);
buttonText = labelForSelection();
}
function clearAll() {
selected = [];
onSelect?.(selected);
buttonText = labelForSelection();
}
function selectAll() {
selected = options.map((o) => o.value);
onSelect?.(selected);
buttonText = labelForSelection();
}
// Computes the button text based on current selection
function labelForSelection() {
if (selected.length === 0) {
return 'Select';
}
if (singular) {
return options.find((o) => o.value === selected[0])?.label || 'Select';
}
if (options.length === selected.length) {
return 'All selected';
}
return `(${selected.length}) selected`;
}
</script>
<div class="flex flex-col">
{#if label !== undefined}<div class="font-bold text-sm px-4 pb-1 text-secondary-500">
{label}
</div>{/if}
<Dropdown buttonClass="w-full" {isOpen} {disabled}>
<span slot="button">
{#if $$slots.default}
<slot />
{:else}
<span class="text-sm">{buttonText}</span>
{/if}
</span>
<div slot="content">
<ul>
{#each options as { label, value }, i}
{@const isSelected = selected.includes(value)}
<li class="border-spacing-1 border-b last:border-b-0">
<button
on:click={() => internalOnSelect(value)}
class="p-4 hover:bg-secondary-200 w-full text-left flex gap-4"
>
<div class="w-6">
{#if singular}
<div
class="rounded-full op w-6 h-6 border-2 flex items-center justify-center border-secondary-900 {isSelected
? 'opacity-100'
: 'opacity-20'}"
>
<div hidden={!isSelected} class="rounded-full w-4 h-4 bg-secondary-900" />
</div>
{:else}
<i hidden={!isSelected}><CheckIcon /></i>
{/if}
</div>
<span class={isSelected ? 'font-bold' : ''}>{label}</span></button
>
</li>
{/each}
</ul>
{#if !singular}
<div class="border-t p-4 flex justify-end gap-2">
<Button size="sm" color="primary" on:click={selectAll}>Select all</Button>
<Button size="sm" on:click={clearAll}>Clear</Button>
</div>
{/if}
</div>
</Dropdown>
</div>
+24 -2
View File
@@ -1,3 +1,25 @@
<div class="min-h-screen relative isolate bg-slate-50 pb-96">
<slot />
<script lang="ts">
import { onMount } from 'svelte';
import '../app.css';
onMount(() => {
// On page load or when changing themes, best to add inline in `head` to avoid FOUC
if (
localStorage.theme === 'dark' ||
(!('theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)
) {
document.documentElement.classList.add('dark');
} else {
document.documentElement.classList.remove('dark');
}
// Whenever the user explicitly chooses light mode
localStorage.theme = 'light';
});
</script>
<div class="min-h-screen relative isolate max-h-screen max-w-full">
<main class="p-4">
<slot />
</main>
</div>
+1
View File
@@ -0,0 +1 @@
export const prerender = true;
+31 -5
View File
@@ -4,6 +4,8 @@ import type { Load } from '@sveltejs/kit';
export type DataEntry = {
name: string;
fixture: string;
filterName: string;
infoUrl: string;
dataUrl: string;
};
@@ -17,32 +19,56 @@ export type EntryDefinition = {
// FIXME: add missing fields maybe generate even
};
export type FilterEntry = {
name: string;
entries: DataEntry[];
};
export const load: Load = async ({ params }) => {
// Use glob to index all available data entries
const definitions = await import.meta.glob('/static/datasets/*/*.json');
const data = await import.meta.glob('/static/datasets/*/*.csv');
// const data = await import.meta.glob('/static/datasets/*/*.csv');
const dataEntries: DataEntry[] = [];
const filters: { [key: string]: FilterEntry } = {};
// Iterate over found modules
for (const path in definitions) {
// Simplit path into [filter]/[name] and remove extension
const [filter, name] = path
let [filter, name] = path
.split('/')
.slice(-2)
.map((p) => p.replace('.json', ''));
filter = filter.replace('_part', '');
// load defintiion info
const info: EntryDefinition = (await definitions[path]()) as EntryDefinition;
dataEntries.push({
const entry = {
name: info.name,
// FIXME: just a temporary hack to get the filter name -> add this value to test output
filterName: info.name.split(' ')[0],
fixture: info.fixutre,
infoUrl: `/datasets/${filter}/${name}.json`,
dataUrl: `/datasets/${filter}/${name}.csv`
});
};
dataEntries.push(entry);
// Construct build time filters
if (!filters[filter]) {
filters[filter] = {
name: filter,
entries: [entry]
};
} else {
filters[filter].entries.push(entry);
}
}
return {
data: dataEntries
data: dataEntries,
filters: filters
};
};
+64 -23
View File
@@ -1,9 +1,18 @@
<script lang="ts">
import Papa from 'papaparse';
import type { ParseResult } from 'papaparse';
import Sidebar from '../lib/Sidebar.svelte';
import type { PageServerData } from './$types';
import type { DataEntry, EntryDefinition } from './proxy+page.server';
import { contenteditable_truthy_values } from 'svelte/internal';
import Button from '$lib/components/Button.svelte';
import Dropdown from '$lib/components/Dropdown.svelte';
import DropdownSelect from '$lib/components/DropdownSelect.svelte';
export let data: PageServerData;
// TODO: either generate or manually write types for csv data
type CsvData = object;
let selectedItem: {
name: string;
info: Promise<EntryDefinition>;
@@ -16,41 +25,73 @@
let entry: typeof selectedItem = {
name: item.name,
info: fetch(item.infoUrl).then((r) => r.json()),
content: fetch(item.dataUrl).then((r) => r.text())
content: fetchAndParseCSV(item.dataUrl)
};
selectedItem = entry;
}
async function fetchAndParseCSV(csvUrl: string) {
const url = new URL(csvUrl, location.href);
console.log(url);
const promise = new Promise<Papa.ParseResult<CsvData>>((resolve, reject) => {
Papa.parse<CsvData>(url.href, {
download: true,
header: true,
worker: true,
dynamicTyping: true,
skipEmptyLines: true,
complete: function (results) {
console.log('Finished:', results);
results.errors.length > 0 ? reject(results.errors) : resolve(results);
}
});
});
return promise;
}
</script>
<div class="grid">
<Sidebar class="grid-cols-1">
<ul>
{#each data.data as item}
<li>
<button
class:selected={item.name === selectedItem?.name}
on:click={() => selectItem(item)}>{item.name}</button
>
</li>
{/each}
</ul>
</Sidebar>
<div class="grid-cols-5">
<div class="">
<div class="flex gap-4">
<DropdownSelect
label="Filters"
onSelect={(selected) => {
console.log(selected);
//selectItem(selected);
}}
options={Object.entries(data.filters).map(([key, value]) => ({
label: key,
value: value
}))}
/>
<DropdownSelect
label="Number of elements"
singular
disabled={true}
onSelect={(selected) => {
console.log(selected);
//selectItem(selected);
}}
options={Object.entries(data.filters).map(([key, value]) => ({
label: key,
value: value
}))}
/>
</div>
<div class="border-t-2 mt-6">
{#if selectedItem}
<h2>{selectedItem.name}</h2>
{#await selectedItem.info}
<div>loading...</div>
{:then info}
<pre>{JSON.stringify(info)}</pre>
{/await}
{#await selectedItem.content}
<div>loading...</div>
{:then content}
<pre>{content}</pre>
<pre>{JSON.stringify(content.data)}</pre>
{/await}
{/if}
</div>
</div>
<style>
.selected {
color: hotpink;
font-weight: bold;
}
</style>
+31
View File
@@ -0,0 +1,31 @@
import { writable } from 'svelte/store';
import type { Writable } from 'svelte/store';
export enum Theme {
Light = 'light',
Dark = 'dark'
}
/**
* Store for all application settings
*/
interface AppSettings {
theme: Theme;
}
const initialAppSettings: AppSettings = {
theme: Theme.Light
};
export const appSettingsStore: Writable<AppSettings> = writable(initialAppSettings);
export const updateTheme = (theme: Theme) => {
appSettingsStore.update((settings) => {
return { ...settings, theme };
});
};
export const toggleThemeMode = () => {
console.log('toggleThemeMode', appSettingsStore);
updateTheme(appSettingsStore.theme === Theme.Dark ? Theme.Light : Theme.Dark);
};
+11 -1
View File
@@ -1,12 +1,22 @@
const colors = require('tailwindcss/colors');
/** @type {import('tailwindcss').Config} */
export default {
content: ['./src/**/*.{html,js,svelte,ts}'],
darkMode: 'class',
theme: {
fontFamily: {
display: ['Source Serif Pro', 'Georgia', 'serif'],
body: ['Manrope', 'system-ui', 'sans-serif']
},
extend: {}
extend: {
colors: {
primary: colors.blue,
secondary: colors.gray,
accent: colors.orange
}
}
},
plugins: []
};