test-card/src/routes/(pages)/+page.svelte

328 lines
6.3 KiB
Svelte

<script lang="ts">
import { version } from '../../../package.json';
import { i18n } from '$lib/i18n';
import type { Snapshot } from '@sveltejs/kit';
let search = '';
type Entry = {
id: string;
label: string;
icon: string;
};
type Test = Entry & {
categories: Array<Category>;
disabled?: boolean;
};
type Category = (typeof categories)[number]['id'] | (typeof superCategories)[number]['id'];
let superCategories = [
{
id: 'inputs',
label: 'Inputs',
icon: 'ti-arrow-down-to-arc'
},
{
id: 'outputs',
label: 'Outputs',
icon: 'ti-arrow-down-from-arc'
}
] as const satisfies Entry[];
let categories = [
{
id: 'audio',
label: 'Audio',
icon: 'ti-volume'
},
{
id: 'video',
label: 'Video',
icon: 'ti-video'
},
{
id: 'control',
label: 'Control',
icon: 'ti-hand-finger'
},
{
id: 'misc',
label: 'Miscellaneous',
icon: 'ti-circle-plus'
}
] as const satisfies Entry[];
const tests: Test[] = [
{
id: 'card',
label: 'Test Card',
icon: 'ti-device-desktop',
categories: ['outputs', 'video']
},
{
id: 'audio',
label: 'Audio',
icon: 'ti-volume',
categories: ['outputs', 'audio']
},
{
id: 'av-sync',
label: 'AV Sync',
icon: 'ti-time-duration-off',
categories: ['outputs', 'video', 'audio']
},
{
id: 'keyboard',
label: 'Keyboard',
icon: 'ti-keyboard',
categories: ['inputs', 'control']
},
{
id: 'mouse',
label: 'Mouse',
icon: 'ti-mouse',
categories: ['inputs', 'control']
},
{
id: 'gamepad',
label: 'Gamepad',
icon: 'ti-device-gamepad',
categories: ['inputs', 'control']
},
{
id: 'camera',
label: 'Camera',
icon: 'ti-camera',
categories: ['inputs', 'video']
},
{
id: 'microphone',
label: 'Microphone',
icon: 'ti-microphone',
categories: ['inputs', 'audio'],
disabled: true
},
{
id: 'sensors',
label: 'Sensors',
icon: 'ti-cpu-2',
categories: ['inputs', 'misc'],
disabled: true
},
{
id: 'internet',
label: 'Internet speed',
icon: 'ti-world',
categories: ['inputs', 'outputs', 'misc']
},
{
id: 'timer',
label: 'High resolution timer',
icon: 'ti-alarm',
categories: ['video']
}
];
let filteredTests: Test[] = tests;
let filteredCategories: Category[] = [];
function doSearch(search: string) {
filteredTests = tests.filter((test) => {
if (!search) return true;
const searchValue = search.toLocaleLowerCase();
return (
test.label.includes(searchValue) ||
test.id.includes(searchValue) ||
$i18n.t(`tests.${test.id}.description`).includes(searchValue)
);
});
}
$: doSearch(search);
function setFilter(category: Category) {
if (filteredCategories.includes(category)) {
filteredCategories = filteredCategories.filter((c) => c !== category);
} else {
filteredCategories = [...filteredCategories, category];
}
}
$: nonEmptyCategories = categories.filter((category) => {
const categoryTests = filteredTests.filter((test) => test.categories.includes(category.id));
return categoryTests.some(
(test) =>
!filteredCategories.length || filteredCategories.every((f) => test.categories.includes(f))
);
});
export const snapshot: Snapshot<string> = {
capture: () => JSON.stringify({ filtered: filteredCategories, search }),
restore: (value) => {
const { filtered: restoredFiltered, search: restoredSearch } = JSON.parse(value);
filteredCategories = restoredFiltered;
search = restoredSearch;
}
};
</script>
<h1>Total Tech Test</h1>
<nav>
<!-- svelte-ignore a11y-autofocus -->
<input type="search" placeholder={$i18n.t('Search')} bind:value={search} autofocus />
<div class="options">
{#each superCategories as category}
<button
on:click={() => setFilter(category.id)}
class:active={!filteredCategories.length || filteredCategories.includes(category.id)}
class="super"
>
<i class="ti {category.icon}"></i>
{$i18n.t(category.label)}
</button>
{/each}
<div class="separator"></div>
{#each categories as category}
<button
on:click={() => setFilter(category.id)}
class:active={!filteredCategories.length || filteredCategories.includes(category.id)}
>
<i class="ti {category.icon}"></i>
{$i18n.t(category.label)}
</button>
{/each}
</div>
<div class="tests">
{#each nonEmptyCategories as category}
{#if tests.filter((test) => test.categories.includes(category.id)).length > 0}
<h2>{$i18n.t(category.label)}</h2>
{#each filteredTests.filter((test) => test.categories.includes(category.id) && filteredCategories.every( (f) => test.categories.includes(f) )) as test}
<div class="test" class:disabled={test.disabled}>
<a class="test" href={test.id}>
<span class="label">
<i class="ti {test.icon}"></i>
{$i18n.t(test.label)}
</span>
<span class="description">
{$i18n.t(`tests.${test.id}.description`)}
</span>
</a>
</div>
{/each}
{/if}
{:else}
<p>
{$i18n.t('No tests found.')}
</p>
{/each}
</div>
</nav>
<footer><a href="https://git.thm.place/thm/test-card">testcard v{version}</a></footer>
<style>
nav {
display: flex;
flex-direction: column;
gap: 1rem;
flex-grow: 1;
padding: 0 4rem;
overflow: auto;
& input[type='search'] {
padding: 0.5em;
width: 100%;
}
}
h1 {
text-align: center;
font-size: 3rem;
margin: 1rem;
text-transform: uppercase;
}
h2 {
margin: 0.25em 0;
}
.options {
display: flex;
justify-content: space-evenly;
align-items: center;
gap: 2em;
& button {
border: none;
background: none;
display: flex;
flex-direction: column;
opacity: 0.66;
&.active {
opacity: 1;
}
}
& .separator {
border-left: 1px solid currentColor;
height: 3rem;
opacity: 0.8;
}
& a {
text-align: center;
text-decoration: none;
white-space: nowrap;
&.disabled {
pointer-events: none;
opacity: 0.5;
}
}
& .ti {
display: block;
font-size: 3rem;
}
}
.test {
display: flex;
gap: 0.5em;
margin-bottom: 0.25em;
& .label {
white-space: nowrap;
}
& .description {
opacity: 0.85;
}
& a {
text-decoration: none;
color: inherit;
}
&.disabled {
opacity: 0.5;
pointer-events: none;
}
}
footer {
text-align: center;
opacity: 0.6;
margin-top: 1rem;
& a {
text-decoration: none;
}
}
</style>