From 1fb71fe8994ec0443c7142538575ab9059d6aba3 Mon Sep 17 00:00:00 2001
From: bytedream <bytedream@protonmail.com>
Date: Tue, 29 Aug 2023 00:43:15 +0200
Subject: [PATCH] make admin table resizable

---
 package-lock.json                      | 25 +++++++++++
 package.json                           |  2 +
 src/lib/components/Input/Input.svelte  | 17 ++++++--
 src/lib/components/Input/Select.svelte |  7 ++-
 src/lib/components/utils.ts            | 49 +++++++++++++++++++++
 src/lib/permissions.ts                 |  4 +-
 src/routes/+layout.svelte              |  8 ++--
 src/routes/admin/+layout.svelte        |  4 +-
 src/routes/admin/admin/+page.svelte    | 60 +++++++++++++++++---------
 src/routes/admin/admin/+server.ts      |  2 +
 svelte.config.js                       |  4 +-
 11 files changed, 148 insertions(+), 34 deletions(-)

diff --git a/package-lock.json b/package-lock.json
index f8f8388..0d594cb 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -31,11 +31,13 @@
 				"postcss": "^8.4.27",
 				"prettier": "^2.8.0",
 				"prettier-plugin-svelte": "^2.10.1",
+				"sass": "^1.66.1",
 				"svelte": "^4.0.5",
 				"svelte-check": "^3.4.3",
 				"svelte-heros-v2": "^0.9.3",
 				"svelte-local-storage-store": "^0.6.0",
 				"svelte-multicssclass": "^2.1.1",
+				"svelte-preprocess": "^5.0.4",
 				"tailwindcss": "^3.3.3",
 				"tslib": "^2.4.1",
 				"typescript": "^5.0.0",
@@ -2854,6 +2856,12 @@
 				"node": ">= 4"
 			}
 		},
+		"node_modules/immutable": {
+			"version": "4.3.4",
+			"resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.4.tgz",
+			"integrity": "sha512-fsXeu4J4i6WNWSikpI88v/PcVflZz+6kMhUfIwc5SY+poQRPnaf5V7qds6SUyUN3cVxEzuCab7QIoLOQ+DQ1wA==",
+			"dev": true
+		},
 		"node_modules/import-fresh": {
 			"version": "3.3.0",
 			"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
@@ -4391,6 +4399,23 @@
 				"rimraf": "bin.js"
 			}
 		},
+		"node_modules/sass": {
+			"version": "1.66.1",
+			"resolved": "https://registry.npmjs.org/sass/-/sass-1.66.1.tgz",
+			"integrity": "sha512-50c+zTsZOJVgFfTgwwEzkjA3/QACgdNsKueWPyAR0mRINIvLAStVQBbPg14iuqEQ74NPDbXzJARJ/O4SI1zftA==",
+			"dev": true,
+			"dependencies": {
+				"chokidar": ">=3.0.0 <4.0.0",
+				"immutable": "^4.0.0",
+				"source-map-js": ">=0.6.2 <2.0.0"
+			},
+			"bin": {
+				"sass": "sass.js"
+			},
+			"engines": {
+				"node": ">=14.0.0"
+			}
+		},
 		"node_modules/semver": {
 			"version": "7.5.4",
 			"resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz",
diff --git a/package.json b/package.json
index e795aad..0bb4457 100644
--- a/package.json
+++ b/package.json
@@ -28,11 +28,13 @@
 		"postcss": "^8.4.27",
 		"prettier": "^2.8.0",
 		"prettier-plugin-svelte": "^2.10.1",
+		"sass": "^1.66.1",
 		"svelte": "^4.0.5",
 		"svelte-check": "^3.4.3",
 		"svelte-heros-v2": "^0.9.3",
 		"svelte-local-storage-store": "^0.6.0",
 		"svelte-multicssclass": "^2.1.1",
+		"svelte-preprocess": "^5.0.4",
 		"tailwindcss": "^3.3.3",
 		"tslib": "^2.4.1",
 		"typescript": "^5.0.0",
diff --git a/src/lib/components/Input/Input.svelte b/src/lib/components/Input/Input.svelte
index 099c5f9..c810459 100644
--- a/src/lib/components/Input/Input.svelte
+++ b/src/lib/components/Input/Input.svelte
@@ -28,7 +28,11 @@
 <div class={type === 'submit' && disabled ? 'cursor-not-allowed' : ''}>
 	{#if type === 'submit'}
 		<input
-			class={`btn btn-${size}`}
+			class="btn"
+			class:btn-xs={size === 'xs'}
+			class:btn-sm={size === 'sm'}
+			class:btn-md={size === 'md'}
+			class:btn-lg={size === 'lg'}
 			{id}
 			type="submit"
 			{disabled}
@@ -49,9 +53,16 @@
 			{/if}
 			<div class="relative flex items-center" class:sm:max-w-[16rem]={type !== 'checkbox'}>
 				<input
-					class={type === 'checkbox' ? `checkbox-${size}` : `input-${size}`}
 					class:checkbox={type === 'checkbox'}
-					class:input,input-bordered,w-[100%]={type !== 'checkbox'}
+					class:checkbox-xs={type === 'checkbox' && size === 'xs'}
+					class:checkbox-sm={type === 'checkbox' && size === 'sm'}
+					class:checkbox-md={type === 'checkbox' && size === 'md'}
+					class:checkbox-lg={type === 'checkbox' && size === 'lg'}
+					class:input,input-bordered,w-full={type !== 'checkbox'}
+					class:input-xs={type !== 'checkbox' && size === 'xs'}
+					class:input-sm={type !== 'checkbox' && size === 'sm'}
+					class:input-md={type !== 'checkbox' && size === 'md'}
+					class:input-lg={type !== 'checkbox' && size === 'lg'}
 					class:pr-11={initialType === 'password'}
 					{id}
 					{name}
diff --git a/src/lib/components/Input/Select.svelte b/src/lib/components/Input/Select.svelte
index 294dd7d..5cfff11 100644
--- a/src/lib/components/Input/Select.svelte
+++ b/src/lib/components/Input/Select.svelte
@@ -6,6 +6,7 @@
 	export let notice: string | null = null;
 	export let required = false;
 	export let disabled = false;
+	export let size: 'xs' | 'sm' | 'md' | 'lg' = 'md';
 </script>
 
 <div>
@@ -20,7 +21,11 @@
 		</label>
 	{/if}
 	<select
-		class="input input-bordered w-[100%] sm:max-w-[16rem]"
+		class="select select-bordered w-[100%] sm:max-w-[16rem]"
+		class:select-xs={size === 'xs'}
+		class:select-sm={size === 'sm'}
+		class:select-md={size === 'md'}
+		class:select-lg={size === 'lg'}
 		{id}
 		{name}
 		{required}
diff --git a/src/lib/components/utils.ts b/src/lib/components/utils.ts
index 9d1296e..9bb5bc0 100644
--- a/src/lib/components/utils.ts
+++ b/src/lib/components/utils.ts
@@ -3,3 +3,52 @@ export async function buttonTriggeredRequest<T>(e: MouseEvent, promise: Promise<
 	await promise;
 	(e.target as HTMLButtonElement).disabled = false;
 }
+
+export function resizeTableColumn(event: MouseEvent, dragOffset: number) {
+	const element = event.target as HTMLTableCellElement;
+	const rect = element.getBoundingClientRect();
+
+	const posX = event.clientX - rect.left;
+	const offset = rect.width - event.clientX;
+	if (posX <= dragOffset || posX >= rect.width - dragOffset) {
+		// do not resize if resize request is on the table left or right
+		if (
+			(posX <= dragOffset && !element.previousElementSibling) ||
+			(posX >= rect.width - dragOffset && !element.nextElementSibling)
+		) {
+			return;
+		}
+
+		const table = element.parentElement!.parentElement!.parentElement as HTMLTableElement;
+		let resizeRow: HTMLTableRowElement;
+		if (table.tBodies[0].rows[0].hidden) {
+			resizeRow = table.rows[0];
+		} else {
+			resizeRow = table.tBodies[0].insertRow(0);
+			resizeRow.hidden = true;
+			for (let i = 0; i < table.rows[0].cells.length; i++) {
+				resizeRow.insertCell();
+			}
+
+			// insert an additional to keep the zebra in place pattern which might be applied
+			const zebraGhostRow = table.tBodies[0].insertRow(0);
+			zebraGhostRow.hidden = true;
+		}
+
+		const resizeElement =
+			resizeRow.cells[element.cellIndex - ((posX <= dragOffset) as unknown as number)];
+
+		// eslint-disable-next-line svelte/no-inner-declarations,no-inner-declarations
+		function resize(e: MouseEvent) {
+			document.body.style.cursor = 'col-resize';
+			resizeElement.style.width = `${offset + e.clientX}px`;
+		}
+
+		document.addEventListener('mousemove', resize);
+
+		document.addEventListener('mouseup', () => {
+			document.removeEventListener('mousemove', resize);
+			document.body.style.cursor = 'initial';
+		});
+	}
+}
diff --git a/src/lib/permissions.ts b/src/lib/permissions.ts
index 4cb2866..6e96bb8 100644
--- a/src/lib/permissions.ts
+++ b/src/lib/permissions.ts
@@ -6,9 +6,11 @@ export class Permissions {
 
 	readonly value: number;
 
-	constructor(value: number | number[]) {
+	constructor(value: number | number[] | null) {
 		if (typeof value == 'number') {
 			this.value = value;
+		} else if (value == null) {
+			this.value = 0;
 		} else {
 			let finalValue = 0;
 			for (const v of Object.values(value)) {
diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte
index e310ac0..ce5957b 100644
--- a/src/routes/+layout.svelte
+++ b/src/routes/+layout.svelte
@@ -92,9 +92,8 @@
 </main>
 <nav>
 	<div
-		class={onAdminPage
-			? 'fixed bottom-4 right-4 group/menu-bar flex flex-col-reverse justify-center items-center'
-			: 'fixed bottom-4 right-4 group/menu-bar flex flex-col-reverse justify-center items-center sm:left-4 sm:right-[initial]'}
+		class="fixed bottom-4 right-4 sm:left-4 sm:right-[initial] group/menu-bar flex flex-col-reverse justify-center items-center"
+		class:hidden={onAdminPage}
 		bind:this={nav}
 	>
 		<button
@@ -145,8 +144,7 @@
 			<ul class="flex flex-col bg-base-200 rounded">
 				{#each navPaths as navPath, i}
 					<li
-						class="flex justify-center tooltip tooltip-left"
-						class:sm:tooltip-right={!onAdminPage}
+						class="flex justify-center tooltip tooltip-left sm:tooltip-right"
 						data-tip={navPath.name}
 					>
 						<a
diff --git a/src/routes/admin/+layout.svelte b/src/routes/admin/+layout.svelte
index 363e5b6..982f902 100644
--- a/src/routes/admin/+layout.svelte
+++ b/src/routes/admin/+layout.svelte
@@ -19,7 +19,7 @@
 
 {#if $page.url.pathname !== `${env.PUBLIC_BASE_PATH}/admin/login`}
 	<div class="flex h-screen">
-		<div class="h-full">
+		<div class="h-full w-max">
 			<ul class="menu p-4 w-fit h-full bg-base-200 text-base-content">
 				<li>
 					<a href="{env.PUBLIC_BASE_PATH}/admin/admin">
@@ -35,7 +35,7 @@
 				</li>
 			</ul>
 		</div>
-		<div class="h-full w-full">
+		<div class="h-full w-full overflow-scroll">
 			<slot />
 		</div>
 	</div>
diff --git a/src/routes/admin/admin/+page.svelte b/src/routes/admin/admin/+page.svelte
index a159a4e..7fcd397 100644
--- a/src/routes/admin/admin/+page.svelte
+++ b/src/routes/admin/admin/+page.svelte
@@ -6,7 +6,7 @@
 	import { Permissions } from '$lib/permissions';
 	import { env } from '$env/dynamic/public';
 	import ErrorToast from '$lib/components/Toast/ErrorToast.svelte';
-	import { buttonTriggeredRequest } from '$lib/components/utils';
+	import { buttonTriggeredRequest, resizeTableColumn } from '$lib/components/utils';
 	import { goto } from '$app/navigation';
 
 	let allPermissionBadges = {
@@ -30,7 +30,9 @@
 			})
 		});
 		if (response.ok) {
-			data.admins.push(await response.json());
+			let res = await response.json();
+			res.permissions = new Permissions(res.permissions).asArray();
+			data.admins.push(res);
 			data.admins = data.admins;
 		} else {
 			throw new Error();
@@ -89,28 +91,21 @@
 	let permissions = new Permissions(data.permissions);
 </script>
 
-<table class="table table-zebra">
-	<colgroup>
-		<col span="1" style="width: 5%" />
-		<col span="1" style="width: 25%" />
-		<col span="1" style="width: 25%" />
-		<col span="1" style="width: 30%" />
-		<col span="1" style="width: 15%" />
-	</colgroup>
+<table class="table table-zebra w-full">
 	<thead>
 		<tr>
-			<th />
-			<th>Benutzername</th>
-			<th>Passwort</th>
-			<th>Berechtigungen</th>
-			<th />
+			<th on:mousedown={(e) => resizeTableColumn(e, 5)} />
+			<th on:mousedown={(e) => resizeTableColumn(e, 5)}>Benutzername</th>
+			<th on:mousedown={(e) => resizeTableColumn(e, 5)}>Passwort</th>
+			<th on:mousedown={(e) => resizeTableColumn(e, 5)}>Berechtigungen</th>
+			<th on:mousedown={(e) => resizeTableColumn(e, 5)} />
 		</tr>
 	</thead>
 	<tbody>
 		{#each data.admins as admin, i}
 			<tr>
-				<td>{i}</td>
-				<td
+				<td on:mousedown={(e) => resizeTableColumn(e, 5)}>{i}</td>
+				<td on:mousedown={(e) => resizeTableColumn(e, 5)}
 					><Input
 						type="text"
 						bind:value={admin.username}
@@ -118,7 +113,7 @@
 						size="sm"
 					/></td
 				>
-				<td
+				<td on:mousedown={(e) => resizeTableColumn(e, 5)}
 					><Input
 						type="password"
 						bind:value={admin.password}
@@ -127,14 +122,14 @@
 						size="sm"
 					/></td
 				>
-				<td
+				<td on:mousedown={(e) => resizeTableColumn(e, 5)}
 					><Badges
 						bind:value={admin.permissions}
 						available={allPermissionBadges}
 						disabled={!permissions.adminWrite() || !admin.edit}
 					/></td
 				>
-				<td>
+				<td on:mousedown={(e) => resizeTableColumn(e, 5)}>
 					<div>
 						{#if admin.edit}
 							<span class="w-min" class:cursor-not-allowed={!permissions.adminWrite()}>
@@ -245,3 +240,28 @@
 <ErrorToast show={errorMessage !== ''}>
 	<span />
 </ErrorToast>
+
+<style lang="scss">
+	thead tr th,
+	tbody tr td {
+		@apply relative;
+
+		&:not(:first-child) {
+			@apply border-l-[1px] border-dashed;
+
+			&::before {
+				@apply absolute left-0 bottom-0 h-full w-[5px] cursor-col-resize;
+				content: '';
+			}
+		}
+
+		&:not(:last-child) {
+			@apply border-r-[1px] border-dashed border-base-300;
+
+			&::after {
+				@apply absolute right-0 bottom-0 h-full w-[5px] cursor-col-resize;
+				content: '';
+			}
+		}
+	}
+</style>
diff --git a/src/routes/admin/admin/+server.ts b/src/routes/admin/admin/+server.ts
index 7f0cb19..732c748 100644
--- a/src/routes/admin/admin/+server.ts
+++ b/src/routes/admin/admin/+server.ts
@@ -34,6 +34,8 @@ export const POST = (async ({ request, cookies }) => {
 		permissions: new Permissions(permissions)
 	});
 
+	delete admin.dataValues.password;
+
 	return new Response(JSON.stringify(admin), {
 		status: 201
 	});
diff --git a/svelte.config.js b/svelte.config.js
index 2214c60..940bed5 100644
--- a/svelte.config.js
+++ b/svelte.config.js
@@ -1,11 +1,11 @@
 import adapter from '@sveltejs/adapter-node';
-import { vitePreprocess } from '@sveltejs/kit/vite';
+import preprocess from 'svelte-preprocess';
 
 /** @type {import('@sveltejs/kit').Config} */
 const config = {
 	// Consult https://kit.svelte.dev/docs/integrations#preprocessors
 	// for more information about preprocessors
-	preprocess: vitePreprocess(),
+	preprocess: preprocess(),
 
 	kit: {
 		// adapter-auto only supports some environments, see https://kit.svelte.dev/docs/adapter-auto for a list.