From 4e8f0c96904f9915c6bc6e50f12e5094f924759c Mon Sep 17 00:00:00 2001
From: ivarlovlie <git@ivarlovlie.no>
Date: Tue, 11 Oct 2022 12:57:36 +0800
Subject: feat: !WIP projects table

- Sorting
- Filtering
- Project status badge
---
 .../src/routes/(main)/(app)/projects/+page.svelte  | 75 ++++++++++++++++++----
 1 file changed, 62 insertions(+), 13 deletions(-)

(limited to 'code/app/src/routes/(main)/(app)/projects/+page.svelte')

diff --git a/code/app/src/routes/(main)/(app)/projects/+page.svelte b/code/app/src/routes/(main)/(app)/projects/+page.svelte
index ad273ab..ceb08b4 100644
--- a/code/app/src/routes/(main)/(app)/projects/+page.svelte
+++ b/code/app/src/routes/(main)/(app)/projects/+page.svelte
@@ -1,13 +1,16 @@
 <script lang="ts">
-  import { Button } from "$lib/components";
+  import { Button, ProjectStatusBadge, Input } from "$lib/components";
   import type { Project } from "$lib/models/projects/Project";
   import { onMount } from "svelte";
   import { faker } from "@faker-js/faker";
   import { Temporal } from "temporal-polyfill";
   import { createTable, Subscribe, Render } from "svelte-headless-table";
+  import { addSortBy, addTableFilter } from "svelte-headless-table/plugins";
   import { ProjectStatus } from "$lib/models/projects/ProjectStatus";
   import { writable, type Writable } from "svelte/store";
+  import { ChevronDownIcon, ChevronUpIcon, ChevronUpDownIcon, MagnifyingGlassIcon, FunnelIcon } from "$lib/components/icons";
   import LL from "$lib/i18n/i18n-svelte";
+  import { goto } from "$app/navigation";
 
   let projects: Writable<Array<Project>> = writable([]);
 
@@ -28,37 +31,75 @@
     projects.set(tempProjects);
   });
 
-  const table = createTable(projects);
+  function goto_project(name: string) {
+    const projectId = $projects.find((p) => p.name === name).id;
+    goto("/projects/" + projectId);
+  }
+
+  const table = createTable(projects, {
+    sort: addSortBy(),
+    filter: addTableFilter(),
+  });
+
   const columns = table.createColumns([
     table.column({ header: $LL.name(), accessor: "name" }),
     table.column({ header: "Status", accessor: "status" }),
     table.column({ header: "Start", accessor: "start" }),
-    table.column({ header: "Description", accessor: "description" }),
+    table.column({ header: "Description", accessor: "description", plugins: { sort: { disable: true } } }),
   ]);
 
-  const { headerRows, rows, tableAttrs, tableBodyAttrs } = table.createViewModel(columns);
+  const { headerRows, rows, tableAttrs, tableBodyAttrs, pluginStates } = table.createViewModel(columns);
+  const { filterValue } = pluginStates.filter;
 </script>
 
-<div class="px-4 sm:px-6 lg:px-8">
+<div class="py-2 px-5">
   <div class="sm:flex sm:items-center">
     <div class="sm:flex-auto">
       <h1 class="text-xl font-semibold text-gray-900">Projects</h1>
       <p class="mt-2 text-sm text-gray-700">A list of all the projects in your organsation.</p>
     </div>
-    <div class="mt-4 sm:mt-0 sm:ml-16 sm:flex-none">
+    <div class="mt-4 sm:mt-0 sm:ml-16 inline-flex gap-1 sm:flex-none">
+      <Input icon={MagnifyingGlassIcon} placeholder="Search" bind:value={$filterValue} />
       <Button text="Create project" />
     </div>
   </div>
-  <div class="-mx-4 mt-8 overflow-hidden sm:-mx-6 md:mx-0 MuonW PowerTable">
+  <div class="-mx-2 mt-6 rounded-md shadow overflow-auto max-h-[80vh] sm:-mx-6 md:mx-0">
     <table {...$tableAttrs} class="min-w-full divide-y divide-gray-300">
       <thead class="bg-gray-50">
         {#each $headerRows as headerRow (headerRow.id)}
           <Subscribe rowAttrs={headerRow.attrs()} let:rowAttrs>
-            <tr {...rowAttrs}>
+            <tr {...rowAttrs} class="shadow-sm">
               {#each headerRow.cells as cell (cell.id)}
-                <Subscribe attrs={cell.attrs()} let:attrs>
-                  <th {...attrs} class="py-3.5 pl-4 pr-3 text-left text-sm font-semibold text-gray-900 sm:pl-6">
-                    <Render of={cell.render()} />
+                <Subscribe attrs={cell.attrs()} let:attrs props={cell.props()} let:props>
+                  <th
+                    {...attrs}
+                    scope="col"
+                    class="sticky top-0 bg-gray-50 bg-opacity-100 whitespace-nowrap px-2 py-3.5 text-left text-sm font-semibold text-gray-900"
+                  >
+                    <div class="group inline-flex">
+                      <Render of={cell.render()} />
+                      <span
+                        on:click={props.sort.toggle}
+                        class="{props.sort.disabled
+                          ? 'bg-gray-200 text-gray-900 group-hover:bg-gray-300'
+                          : 'invisible text-gray-400 group-hover:visible group-focus:visible'}
+                          {props.sort.disabled ? '' : 'cursor-pointer'}
+                          ml-2 flex-none rounded"
+                      >
+                        {#if props.sort.order === "asc"}
+                          <ChevronUpIcon />
+                        {:else if props.sort.order === "desc"}
+                          <ChevronDownIcon />
+                        {:else if !props.sort.disabled}
+                          <ChevronUpDownIcon />
+                        {/if}
+                      </span>
+                      {#if cell.id === "status"}
+                        <span class="invisible text-gray-400 cursor-pointer group-hover:visible group-focus:visible ml-2 flex-none rounded">
+                          <FunnelIcon />
+                        </span>
+                      {/if}
+                    </div>
                   </th>
                 </Subscribe>
               {/each}
@@ -73,8 +114,16 @@
               {#each row.cells as cell (cell.id)}
                 {@const materialisedCell = cell.render()}
                 <Subscribe attrs={cell.attrs()} let:attrs>
-                  <td class="w-full max-w-0 py-4 pl-4 pr-3 text-sm font-medium text-gray-900 sm:w-auto sm:max-w-none sm:pl-6" {...attrs}>
-                    <Render of={materialisedCell} />
+                  <td {...attrs} class="whitespace-nowrap px-2 py-2 text-sm">
+                    {#if cell.id === "name"}
+                      <span class="link" title="Open project" on:click={() => goto_project(materialisedCell.toString())}>
+                        <Render of={materialisedCell} />
+                      </span>
+                    {:else if cell.id === "status"}
+                      <ProjectStatusBadge status={materialisedCell.toString()} />
+                    {:else}
+                      <Render of={materialisedCell} />
+                    {/if}
                   </td>
                 </Subscribe>
               {/each}
-- 
cgit v1.3

