feat(frontend): make data-table fully responsive
This commit is contained in:
parent
2882db284d
commit
b3d291c3ee
9 changed files with 325 additions and 192 deletions
3
frontend/docs/theme/colorscheme.gpl
vendored
3
frontend/docs/theme/colorscheme.gpl
vendored
|
@ -42,3 +42,6 @@ Columns: 1
|
||||||
163 163 163 background-700
|
163 163 163 background-700
|
||||||
112 112 112 background-800
|
112 112 112 background-800
|
||||||
51 51 51 background-900
|
51 51 51 background-900
|
||||||
|
0 204 0 success
|
||||||
|
237 192 0 warning
|
||||||
|
228 0 0 failure
|
||||||
|
|
|
@ -70,7 +70,36 @@
|
||||||
}
|
}
|
||||||
button:disabled {
|
button:disabled {
|
||||||
@apply opacity-25;
|
@apply opacity-25;
|
||||||
@apply bg-neutral-100;
|
@apply cursor-not-allowed;
|
||||||
|
}
|
||||||
|
button:disabled:hover {
|
||||||
|
@apply bg-accent;
|
||||||
|
@apply text-primary-400 font-semibold;
|
||||||
|
@apply cursor-not-allowed;
|
||||||
|
}
|
||||||
|
button.outlineButton {
|
||||||
|
@apply bg-secondary/20;
|
||||||
|
@apply border-2 border-secondary;
|
||||||
|
@apply text-secondary font-medium;
|
||||||
|
}
|
||||||
|
button.outlineButton:hover {
|
||||||
|
@apply bg-secondary;
|
||||||
|
@apply text-secondary-900 font-bold;
|
||||||
|
}
|
||||||
|
button.outlineButton:active {
|
||||||
|
@apply bg-secondary-600;
|
||||||
|
@apply text-secondary-900;
|
||||||
|
}
|
||||||
|
button.outlineButton:focus {
|
||||||
|
@apply ring-secondary-400;
|
||||||
|
}
|
||||||
|
button.outlineButton:disabled {
|
||||||
|
@apply opacity-25;
|
||||||
|
}
|
||||||
|
button.outlineButton:disabled:hover {
|
||||||
|
@apply bg-secondary/20;
|
||||||
|
@apply text-secondary font-medium;
|
||||||
|
@apply cursor-not-allowed;
|
||||||
}
|
}
|
||||||
a.button {
|
a.button {
|
||||||
@apply p-2;
|
@apply p-2;
|
||||||
|
@ -114,24 +143,27 @@
|
||||||
tr:last-of-type td:last-of-type {
|
tr:last-of-type td:last-of-type {
|
||||||
@apply rounded-br-md;
|
@apply rounded-br-md;
|
||||||
}
|
}
|
||||||
tr {
|
|
||||||
@apply hover:bg-secondary-300/50;
|
|
||||||
@apply border-y border-primary/50;
|
|
||||||
@apply bg-secondary-100/10;
|
|
||||||
}
|
|
||||||
tr:first-of-type, tr:last-of-type {
|
tr:first-of-type, tr:last-of-type {
|
||||||
@apply border-y-0;
|
@apply border-y-0;
|
||||||
}
|
}
|
||||||
|
tr:nth-child(odd) {
|
||||||
|
@apply bg-secondary/5;
|
||||||
|
}
|
||||||
|
tr:nth-child(even) {
|
||||||
|
@apply bg-secondary/20;
|
||||||
|
}
|
||||||
|
tr {
|
||||||
|
@apply hover:bg-accent/25;
|
||||||
|
}
|
||||||
th {
|
th {
|
||||||
@apply bg-primary font-semibold text-secondary-100;
|
@apply bg-primary font-semibold text-secondary-100;
|
||||||
}
|
|
||||||
caption, th, td {
|
|
||||||
@apply p-1;
|
@apply p-1;
|
||||||
}
|
}
|
||||||
@media screen(sm) {
|
td {
|
||||||
caption, th, td {
|
@apply p-1;
|
||||||
@apply p-2 text-start;
|
}
|
||||||
}
|
caption {
|
||||||
|
@apply p-1 text-start font-semibold text-xl;
|
||||||
}
|
}
|
||||||
|
|
||||||
fieldset {
|
fieldset {
|
||||||
|
@ -158,4 +190,11 @@
|
||||||
label {
|
label {
|
||||||
@apply text-sm text-secondary font-light;
|
@apply text-sm text-secondary font-light;
|
||||||
}
|
}
|
||||||
|
select {
|
||||||
|
@apply text-primary font-semibold;
|
||||||
|
@apply border border-secondary;
|
||||||
|
@apply bg-secondary/20;
|
||||||
|
@apply rounded-md;
|
||||||
|
@apply p-1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -31,17 +31,34 @@
|
||||||
args: [],
|
args: [],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export let defaultSortField: string = columns[0].field;
|
||||||
|
export let defaultSortOrder: 'asc' | 'desc' = 'asc';
|
||||||
|
|
||||||
|
let viewportWidth: number = 0;
|
||||||
|
let table: Element;
|
||||||
|
let tableMaxWidth: number = 0;
|
||||||
|
let compactView = false;
|
||||||
|
function checkForTableResize() {
|
||||||
|
if (!table) return;
|
||||||
|
|
||||||
|
if (table.scrollWidth > tableMaxWidth) {
|
||||||
|
tableMaxWidth = table.scrollWidth;
|
||||||
|
}
|
||||||
|
|
||||||
|
compactView = tableMaxWidth >= viewportWidth;
|
||||||
|
}
|
||||||
|
|
||||||
type SortEvent = {
|
type SortEvent = {
|
||||||
detail: {
|
detail: {
|
||||||
order: null | 'asc' | 'desc',
|
order: null | 'asc' | 'desc',
|
||||||
field: string,
|
field: string,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let currentSortField: string = columns[0].field;
|
let currentSortField: string = defaultSortField;
|
||||||
let currentSortOrder: 'asc' | 'desc' = 'asc';
|
let currentSortOrder: 'asc' | 'desc' = defaultSortOrder;
|
||||||
|
|
||||||
let currentState: 'finished' | 'loading' | 'error' = 'finished';
|
let currentState: 'finished' | 'loading' | 'error' = 'finished';
|
||||||
let errorMessages: string[] = [];
|
let errorMessage: string;
|
||||||
|
|
||||||
let currentItems: any[] = []; // TODO apply generics here
|
let currentItems: any[] = []; // TODO apply generics here
|
||||||
let totalItemCount: number | null = null;
|
let totalItemCount: number | null = null;
|
||||||
|
@ -134,15 +151,12 @@
|
||||||
);
|
);
|
||||||
currentState = 'finished';
|
currentState = 'finished';
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
if (!errorMessages.length) {
|
if (typeof error.body.message === 'string') {
|
||||||
// Make sure we have an error message to show to the user
|
errorMessage = error.body.message;
|
||||||
if (typeof error.message === 'string') {
|
} else {
|
||||||
errorMessages.push(error.message);
|
errorMessage = "An unknown error occurred.";
|
||||||
} else {
|
|
||||||
errorMessages.push("An unknown error occurred.");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
console.log(error)
|
||||||
currentState = 'error';
|
currentState = 'error';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -155,73 +169,140 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
|
checkForTableResize();
|
||||||
await updateTable();
|
await updateTable();
|
||||||
|
checkForTableResize();
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<table>
|
<svelte:window bind:outerWidth={viewportWidth} on:resize={checkForTableResize} />
|
||||||
{#if caption !== null}
|
|
||||||
<caption>{caption}</caption>
|
<div id="table-container"
|
||||||
{/if}
|
class="flex flex-col items-center gap-2"
|
||||||
<thead>
|
class:compact-view={compactView}
|
||||||
<tr>
|
class:extended-view={!compactView}
|
||||||
|
>
|
||||||
|
<table
|
||||||
|
bind:this={table}
|
||||||
|
>
|
||||||
|
{#if caption !== null}
|
||||||
|
<caption>{caption}</caption>
|
||||||
|
{/if}
|
||||||
|
{#if compactView}
|
||||||
|
<div class="table-controls m-2">
|
||||||
|
<span>Sort by:</span>
|
||||||
|
<select bind:value={currentSortField} on:change={updateTable} class="mr-2">
|
||||||
{#each columns as column}
|
{#each columns as column}
|
||||||
<Th
|
<option selected={column.field === currentSortField} value={column.field}>{column.heading}</option>
|
||||||
on:sortEvent={handleSortEvent}
|
|
||||||
field={column.field}
|
|
||||||
heading={column.heading}
|
|
||||||
order={currentSortField === column.field ? currentSortOrder : null}
|
|
||||||
sortable={column.sortable ?? true}
|
|
||||||
/>
|
|
||||||
{/each}
|
{/each}
|
||||||
</tr>
|
</select>
|
||||||
</thead>
|
<span>Order:</span>
|
||||||
<tbody>
|
<select bind:value={currentSortOrder} on:change={updateTable}>
|
||||||
{#if currentState === 'finished'}
|
<option selected={currentSortOrder === 'asc'} value={'asc'}>Ascending</option>
|
||||||
{#each currentItems as item}
|
<option selected={currentSortOrder === 'desc'} value={'desc'}>Descending</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{#if !compactView}
|
||||||
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
{#each columns as column}
|
{#each columns as column}
|
||||||
<Td
|
<Th
|
||||||
data={item[column.field]}
|
on:sortEvent={handleSortEvent}
|
||||||
type={column.type ?? 'string'}
|
field={column.field}
|
||||||
isLink={column.isLink ?? false}
|
heading={column.heading}
|
||||||
linkTarget={i(column.linkTarget, item) ?? ''}
|
order={currentSortField === column.field ? currentSortOrder : null}
|
||||||
|
sortable={column.sortable ?? true}
|
||||||
/>
|
/>
|
||||||
{/each}
|
{/each}
|
||||||
</tr>
|
</tr>
|
||||||
{/each}
|
</thead>
|
||||||
{:else if currentState === 'loading'}
|
|
||||||
<tr>
|
|
||||||
<td colspan={columns.length}>Loading...</td>
|
|
||||||
</tr>
|
|
||||||
{:else if currentState === 'error'}
|
|
||||||
{#each errorMessages as message}
|
|
||||||
<tr>
|
|
||||||
<td colspan={columns.length} class="text-center text-red-500">{message}</td>
|
|
||||||
</tr>
|
|
||||||
{/each}
|
|
||||||
{/if}
|
{/if}
|
||||||
</tbody>
|
<tbody
|
||||||
</table>
|
class:compact-view={compactView}
|
||||||
<button on:click={handleGotoFirstPage} disabled={gotoFirstPageDisabled}>
|
class:extended-view={!compactView}
|
||||||
<Icon src={ChevronDoubleLeft} mini class="w-6 h-6" />
|
>
|
||||||
</button>
|
{#if currentState === 'finished'}
|
||||||
<button on:click={handleGotoPrevPage} disabled={gotoPrevPageDisabled}>
|
{#each currentItems as item}
|
||||||
<Icon src={ChevronLeft} mini class="w-6 h-6" />
|
<tr>
|
||||||
</button>
|
{#each columns as column}
|
||||||
<input
|
<Td
|
||||||
type=number bind:value={selectedCurrentPage} min=0 max={lastPage} disabled={lastPage === 0}
|
fieldName={column.heading}
|
||||||
on:change={handleCurrentPageInputChanged} on:focus={handleCurrentPageInputFocussed}
|
data={item[column.field]}
|
||||||
>
|
type={column.type ?? 'string'}
|
||||||
<span>of {lastPage + 1}</span>
|
isLink={column.isLink ?? false}
|
||||||
<button on:click={handleGotoNextPage} disabled={gotoNextPageDisabled}>
|
linkTarget={i(column.linkTarget, item) ?? ''}
|
||||||
<Icon src={ChevronRight} mini class="w-6 h-6" />
|
compactView={compactView}
|
||||||
</button>
|
/>
|
||||||
<button on:click={handleGotoLastPage} disabled={gotoLastPageDisabled}>
|
{/each}
|
||||||
<Icon src={ChevronDoubleRight} mini class="w-6 h-6" />
|
</tr>
|
||||||
</button>
|
{/each}
|
||||||
<select bind:value={selectedItemsPerPage} on:change={handleItemsPerPageChange}>
|
{:else if currentState === 'loading'}
|
||||||
{#each itemsPerPageOptions as option}
|
<tr>
|
||||||
<option selected={option === currentItemsPerPage} value={option}>{option}</option>
|
<td colspan={columns.length}>Loading...</td>
|
||||||
{/each}
|
</tr>
|
||||||
</select>
|
{:else if currentState === 'error'}
|
||||||
|
<tr>
|
||||||
|
<td colspan={columns.length} class="text-center text-failure font-bold">{errorMessage}</td>
|
||||||
|
</tr>
|
||||||
|
{/if}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<div class="table-controls">
|
||||||
|
<label for="itemsPerPageSelector" class="text-lg">Items per page</label>
|
||||||
|
<select id="itemsPerPageSelector" bind:value={selectedItemsPerPage} on:change={handleItemsPerPageChange} class="mr-2">
|
||||||
|
{#each itemsPerPageOptions as option}
|
||||||
|
<option selected={option === currentItemsPerPage} value={option}>{option}</option>
|
||||||
|
{/each}
|
||||||
|
</select>
|
||||||
|
<span>Total: <b>{totalItemCount}</b></span>
|
||||||
|
</div>
|
||||||
|
<div class="table-controls">
|
||||||
|
<button on:click={handleGotoFirstPage} disabled={gotoFirstPageDisabled} class="outlineButton">
|
||||||
|
<Icon src={ChevronDoubleLeft} mini class="w-6 h-6" />
|
||||||
|
</button>
|
||||||
|
<button on:click={handleGotoPrevPage} disabled={gotoPrevPageDisabled} class="outlineButton">
|
||||||
|
<Icon src={ChevronLeft} mini class="w-6 h-6" />
|
||||||
|
</button>
|
||||||
|
<label for="currentPageNumberInput" class="text-lg">Page</label>
|
||||||
|
<input id="currentPageNumberInput"
|
||||||
|
type=number bind:value={selectedCurrentPage} min=0 max={lastPage} disabled={lastPage === 0}
|
||||||
|
on:change={handleCurrentPageInputChanged} on:focus={handleCurrentPageInputFocussed}
|
||||||
|
class="text-center"
|
||||||
|
>
|
||||||
|
<span>of {lastPage + 1}</span>
|
||||||
|
<button on:click={handleGotoNextPage} disabled={gotoNextPageDisabled} class="outlineButton">
|
||||||
|
<Icon src={ChevronRight} mini class="w-6 h-6" />
|
||||||
|
</button>
|
||||||
|
<button on:click={handleGotoLastPage} disabled={gotoLastPageDisabled} class="outlineButton">
|
||||||
|
<Icon src={ChevronDoubleRight} mini class="w-6 h-6" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
option {
|
||||||
|
@apply text-center;
|
||||||
|
}
|
||||||
|
|
||||||
|
#table-container.compact-view {
|
||||||
|
@apply max-w-fit;
|
||||||
|
@apply p-4;
|
||||||
|
}
|
||||||
|
#table-container.extended-view {
|
||||||
|
@apply max-w-max;
|
||||||
|
}
|
||||||
|
.compact-view caption {
|
||||||
|
@apply text-center;
|
||||||
|
}
|
||||||
|
.compact-view tr {
|
||||||
|
@apply border border-primary;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.table-controls {
|
||||||
|
@apply flex gap-1 items-center justify-center;
|
||||||
|
}
|
||||||
|
.table-controls span {
|
||||||
|
@apply text-lg text-secondary font-light;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
|
@ -1,30 +1,76 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Icon, Check, XMark } from 'svelte-hero-icons';
|
import { Icon, Check, Minus, XMark } from 'svelte-hero-icons';
|
||||||
|
|
||||||
|
export let fieldName: string;
|
||||||
export let data: any;
|
export let data: any;
|
||||||
export let type: 'string' | 'date' | 'boolean' = 'string';
|
export let type: 'string' | 'date' | 'number' | 'boolean' = 'string';
|
||||||
export let isLink: boolean = false;
|
export let isLink: boolean = false;
|
||||||
export let linkTarget: string = "";
|
export let linkTarget: string = "";
|
||||||
|
|
||||||
|
export let compactView = false;
|
||||||
|
$: extendedView = !compactView;
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<td>
|
<td
|
||||||
{#if type === 'date'}
|
|
||||||
{#if data !== null && data !== undefined}
|
class:compact-view={compactView}
|
||||||
{new Date(data).toLocaleDateString()}
|
class:extended-view={extendedView}
|
||||||
{:else}
|
|
||||||
<!-- date undefined -->
|
class:boolean-data={type === 'boolean'}
|
||||||
{/if}
|
class:string-data={type === 'string'}
|
||||||
{:else if type === 'boolean'}
|
class:number-data={type === 'number'}
|
||||||
{#if data}
|
class:date-data={type === 'date'}
|
||||||
<Icon src={Check} mini class="w-6 h-6" />
|
class:empty-data={data === null || data === undefined}
|
||||||
{:else}
|
>
|
||||||
<Icon src={XMark} mini class="w-6 h-6" />
|
{#if compactView}
|
||||||
{/if}
|
<p class="field-name">{fieldName}:</p>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if data === null || data === undefined}
|
||||||
|
<Icon src={Minus} mini class="field-data w-6 h-6 text-primary/40" />
|
||||||
{:else}
|
{:else}
|
||||||
{#if isLink}
|
{#if type === 'date'}
|
||||||
<a href={linkTarget}>{data}</a>
|
<p class="field-data">{new Date(data).toLocaleDateString()}</p>
|
||||||
|
{:else if type === 'boolean'}
|
||||||
|
{#if data}
|
||||||
|
<Icon src={Check} mini class="field-data w-6 h-6 text-success/50" />
|
||||||
|
{:else}
|
||||||
|
<Icon src={XMark} mini class="field-data w-6 h-6 text-failure/50" />
|
||||||
|
{/if}
|
||||||
{:else}
|
{:else}
|
||||||
{data}
|
{#if isLink}
|
||||||
|
<a href={linkTarget} class="field-data">{data}</a>
|
||||||
|
{:else}
|
||||||
|
<p class="field-data">{data}</p>
|
||||||
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
td.compact-view {
|
||||||
|
@apply grid grid-cols-2 items-center justify-start gap-x-2;
|
||||||
|
}
|
||||||
|
.compact-view .field-name {
|
||||||
|
@apply text-primary/50 font-light;
|
||||||
|
@apply text-end;
|
||||||
|
}
|
||||||
|
.compact-view .field-data {
|
||||||
|
@apply truncate;
|
||||||
|
@apply font-medium;
|
||||||
|
}
|
||||||
|
td.compact-view.date-data .field-data, td.compact-view.number-data .field-data {
|
||||||
|
@apply text-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
td.extended-view {
|
||||||
|
@apply table-cell;
|
||||||
|
}
|
||||||
|
td.extended-view.boolean-data, td.extended-view.empty-data {
|
||||||
|
@apply flex justify-center;
|
||||||
|
}
|
||||||
|
td.extended-view.date-data .field-data, td.extended-view.number-data .field-data {
|
||||||
|
@apply text-end;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
|
@ -29,7 +29,7 @@
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:window bind:outerWidth={viewportWidth}/>
|
<svelte:window bind:outerWidth={viewportWidth} />
|
||||||
|
|
||||||
<nav
|
<nav
|
||||||
class="sticky top-0 transition-transform ease-linear z-40"
|
class="sticky top-0 transition-transform ease-linear z-40"
|
||||||
|
@ -37,15 +37,8 @@
|
||||||
bind:clientHeight={currentHeight}
|
bind:clientHeight={currentHeight}
|
||||||
>
|
>
|
||||||
{#if !largeScreen}
|
{#if !largeScreen}
|
||||||
<div id="navHead"
|
<div id="navHead" class="flex" class:smallScreen={!largeScreen}>
|
||||||
class="flex"
|
<a href="/" on:click={closeMenu} class="group">
|
||||||
class:smallScreen={!largeScreen}
|
|
||||||
>
|
|
||||||
<a
|
|
||||||
href="/"
|
|
||||||
on:click={closeMenu}
|
|
||||||
class="group"
|
|
||||||
>
|
|
||||||
<img
|
<img
|
||||||
src="/images/common/logo.svg"
|
src="/images/common/logo.svg"
|
||||||
alt="Dewit Logo"
|
alt="Dewit Logo"
|
||||||
|
@ -71,41 +64,16 @@
|
||||||
</a>
|
</a>
|
||||||
{/if}
|
{/if}
|
||||||
{#if user}
|
{#if user}
|
||||||
<a
|
<a href={`/users/${user.id}`} on:click={closeMenu}>Profile</a>
|
||||||
href={`/users/${user.id}/todos`}
|
<a href={`/users/${user.id}/todos`} on:click={closeMenu}>Todos</a>
|
||||||
on:click={closeMenu}
|
|
||||||
>
|
|
||||||
Todos
|
|
||||||
</a>
|
|
||||||
<a
|
|
||||||
href={`/users/${user.id}`}
|
|
||||||
on:click={closeMenu}
|
|
||||||
>
|
|
||||||
My Profile
|
|
||||||
</a>
|
|
||||||
{#if user.isAdmin}
|
{#if user.isAdmin}
|
||||||
<a
|
<a href={`/users/all`} on:click={closeMenu}>All Users</a>
|
||||||
href={`/users/all`}
|
|
||||||
on:click={closeMenu}
|
|
||||||
>
|
|
||||||
All Users
|
|
||||||
</a>
|
|
||||||
{/if}
|
{/if}
|
||||||
{#if largeScreen}<div class="grow" aria-hidden />{/if}
|
{#if largeScreen}<div class="grow" aria-hidden />{/if}
|
||||||
<button
|
<button class="outlineButton" on:click={handleLogout}>Log Out</button>
|
||||||
class="outlineButton"
|
|
||||||
on:click={handleLogout}
|
|
||||||
>
|
|
||||||
Log Out
|
|
||||||
</button>
|
|
||||||
{:else}
|
{:else}
|
||||||
{#if largeScreen}<div class="grow" aria-hidden />{/if}
|
{#if largeScreen}<div class="grow" aria-hidden />{/if}
|
||||||
<a
|
<a href="/login" on:click={closeMenu}>Log In</a>
|
||||||
href="/login"
|
|
||||||
on:click={closeMenu}
|
|
||||||
>
|
|
||||||
Log In
|
|
||||||
</a>
|
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
@ -132,20 +100,4 @@
|
||||||
.navbarHidden {
|
.navbarHidden {
|
||||||
@apply -translate-y-full;
|
@apply -translate-y-full;
|
||||||
}
|
}
|
||||||
.outlineButton {
|
|
||||||
@apply bg-primary-400/50;
|
|
||||||
@apply border-2 border-secondary;
|
|
||||||
@apply text-secondary font-medium;
|
|
||||||
}
|
|
||||||
.outlineButton:hover {
|
|
||||||
@apply bg-secondary;
|
|
||||||
@apply text-secondary-900 font-bold;
|
|
||||||
}
|
|
||||||
.outlineButton:active {
|
|
||||||
@apply bg-secondary-600;
|
|
||||||
@apply text-secondary-900;
|
|
||||||
}
|
|
||||||
.outlineButton:focus {
|
|
||||||
@apply ring-secondary-400;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -28,7 +28,6 @@
|
||||||
import { loadUserIntoStore } from '$lib/auth/session';
|
import { loadUserIntoStore } from '$lib/auth/session';
|
||||||
import Navbar from '$lib/components/navbar/Navbar.svelte';
|
import Navbar from '$lib/components/navbar/Navbar.svelte';
|
||||||
import Footer from '$lib/components/footer/Footer.svelte';
|
import Footer from '$lib/components/footer/Footer.svelte';
|
||||||
import { slide } from 'svelte/transition';
|
|
||||||
|
|
||||||
export let title = import.meta.env.PUBLIC_TITLE;
|
export let title = import.meta.env.PUBLIC_TITLE;
|
||||||
export let description = import.meta.env.PUBLIC_DESCRIPTION;
|
export let description = import.meta.env.PUBLIC_DESCRIPTION;
|
||||||
|
@ -40,9 +39,10 @@
|
||||||
let lastKnownScrollPosY = 0;
|
let lastKnownScrollPosY = 0;
|
||||||
const navbarToggleThreshold = 32;
|
const navbarToggleThreshold = 32;
|
||||||
let navbarHeight: number;
|
let navbarHeight: number;
|
||||||
|
let viewportWidth: number;
|
||||||
|
|
||||||
function handleScroll(event: Event): void {
|
function handleScroll(event: Event): void {
|
||||||
if (window.scrollY <= navbarHeight) {
|
if (viewportWidth >= 640 || window.scrollY <= navbarHeight) {
|
||||||
navbarHidden = false;
|
navbarHidden = false;
|
||||||
} else {
|
} else {
|
||||||
let scrollDistance = Math.abs(window.scrollY - lastKnownScrollPosY);
|
let scrollDistance = Math.abs(window.scrollY - lastKnownScrollPosY);
|
||||||
|
@ -65,10 +65,10 @@
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:window on:scroll={handleScroll} />
|
<svelte:window bind:outerWidth={viewportWidth} on:scroll={handleScroll} />
|
||||||
|
|
||||||
<Navbar bind:isHidden={navbarHidden} bind:currentHeight={navbarHeight} />
|
<Navbar bind:isHidden={navbarHidden} bind:currentHeight={navbarHeight} />
|
||||||
<div class="flex flex-col grow justify-center">
|
<div class="flex flex-col grow justify-center items-center">
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
<Footer />
|
<Footer />
|
||||||
|
|
|
@ -3,44 +3,47 @@
|
||||||
import type { Column } from '$lib/components/data-table/types';
|
import type { Column } from '$lib/components/data-table/types';
|
||||||
import type { UserTodosPage } from "./+page";
|
import type { UserTodosPage } from "./+page";
|
||||||
import { readTodos, readTodoCount } from '$lib/api/endpoints';
|
import { readTodos, readTodoCount } from '$lib/api/endpoints';
|
||||||
|
import { getUserFromLocalstorage as getUser } from '$lib/auth/session';
|
||||||
|
|
||||||
export let data: UserTodosPage;
|
export let data: UserTodosPage;
|
||||||
|
const user = getUser();
|
||||||
|
|
||||||
const columns: Column[] = [
|
const columns: Column[] = [
|
||||||
{
|
{
|
||||||
field: "id",
|
field: 'id',
|
||||||
heading: "ID",
|
heading: 'ID',
|
||||||
|
type: 'number,'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: "title",
|
field: 'title',
|
||||||
heading: "Title",
|
heading: 'Title',
|
||||||
isLink: true,
|
isLink: true,
|
||||||
linkTarget: '/todos/%id%',
|
linkTarget: '/todos/%id%',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: "done",
|
field: 'done',
|
||||||
heading: "Done",
|
heading: 'Done',
|
||||||
type: "boolean",
|
type: 'boolean',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: "created",
|
field: 'created',
|
||||||
heading: "Created",
|
heading: 'Created',
|
||||||
type: "date",
|
type: 'date',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: "updated",
|
field: 'updated',
|
||||||
heading: "Updated",
|
heading: 'Updated',
|
||||||
type: "date",
|
type: 'date',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: "finished",
|
field: 'finished',
|
||||||
heading: "Finished",
|
heading: 'Finished',
|
||||||
type: "date",
|
type: 'date',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const table = {
|
const table = {
|
||||||
caption: `List of TODOs for user ${data.user.first_name} ${data.user.last_name}`,
|
caption: user.id === data.user.id ? 'My TODOs' : `${data.user.first_name} ${data.user.last_name}'s TODOs`,
|
||||||
columns: columns,
|
columns: columns,
|
||||||
getItemsEndpoint: {
|
getItemsEndpoint: {
|
||||||
callable: readTodos,
|
callable: readTodos,
|
||||||
|
@ -53,7 +56,9 @@
|
||||||
args: [
|
args: [
|
||||||
data.user.id
|
data.user.id
|
||||||
]
|
]
|
||||||
}
|
},
|
||||||
|
defaultSortField: 'updated',
|
||||||
|
defaultSortOrder: 'desc',
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -2,39 +2,41 @@
|
||||||
import Table from '$lib/components/data-table/Table.svelte';
|
import Table from '$lib/components/data-table/Table.svelte';
|
||||||
import type { Column } from '$lib/components/data-table/types';
|
import type { Column } from '$lib/components/data-table/types';
|
||||||
import { readUsers, readUserCount } from '$lib/api/endpoints';
|
import { readUsers, readUserCount } from '$lib/api/endpoints';
|
||||||
|
import { onMount } from 'svelte';
|
||||||
|
|
||||||
const columns: Column[] = [
|
const columns: Column[] = [
|
||||||
{
|
{
|
||||||
field: "id",
|
field: 'id',
|
||||||
heading: "ID",
|
heading: 'ID',
|
||||||
|
type: 'number',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: "email",
|
field: 'email',
|
||||||
heading: "Email",
|
heading: 'Email',
|
||||||
isLink: true,
|
isLink: true,
|
||||||
linkTarget: '/users/%id%',
|
linkTarget: '/users/%id%',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: "first_name",
|
field: 'first_name',
|
||||||
heading: "First Name",
|
heading: 'First Name',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: "last_name",
|
field: 'last_name',
|
||||||
heading: "Last Name",
|
heading: 'Last Name',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: "created",
|
field: 'created',
|
||||||
heading: "Created",
|
heading: 'Created',
|
||||||
type: 'date',
|
type: 'date',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: "updated",
|
field: 'updated',
|
||||||
heading: "Updated",
|
heading: 'Updated',
|
||||||
type: 'date',
|
type: 'date',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: "is_admin",
|
field: 'is_admin',
|
||||||
heading: "Admin",
|
heading: 'Admin',
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
@ -49,7 +51,9 @@
|
||||||
getItemCountEndpoint: {
|
getItemCountEndpoint: {
|
||||||
callable: readUserCount,
|
callable: readUserCount,
|
||||||
args: []
|
args: []
|
||||||
}
|
},
|
||||||
|
defaultSortField: 'id',
|
||||||
|
defaultSortOrder: 'asc',
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -65,6 +65,9 @@ export default {
|
||||||
800: "#707070",
|
800: "#707070",
|
||||||
900: "#333333",
|
900: "#333333",
|
||||||
},
|
},
|
||||||
|
success: "#00CC00",
|
||||||
|
warning: "#EDC000",
|
||||||
|
failure: "#E40000",
|
||||||
},
|
},
|
||||||
screens: {
|
screens: {
|
||||||
'3xl': '1920px',
|
'3xl': '1920px',
|
||||||
|
|
Loading…
Add table
Reference in a new issue