328 lines
6.3 KiB
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>
|