Skip to content

Commit

Permalink
feat: new card layout
Browse files Browse the repository at this point in the history
  • Loading branch information
typicalninja committed Sep 10, 2023
1 parent 79589f0 commit 2e8307d
Show file tree
Hide file tree
Showing 11 changed files with 198 additions and 163 deletions.
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@
"@tanstack/svelte-query": "^4.35.0",
"@vincjo/datatables": "^1.12.4",
"axios": "^1.5.0",
"lucide-svelte": "^0.274.0",
"svelte-forms-lib": "^2.0.1",
"svelte-local-storage-store": "^0.6.0",
"svelte-sonner": "^0.1.4"
}
Expand Down
20 changes: 20 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

270 changes: 114 additions & 156 deletions src/components/commandList.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -5,208 +5,166 @@
import { createQuery, useQueryClient } from '@tanstack/svelte-query';
import { toast } from 'svelte-sonner';
import { goto } from '$app/navigation';
import { DataHandler, Th, Datatable, ThFilter } from '@vincjo/datatables'
// headers for the table
import './list.css'
// custom icons
import RefreshCw from './icons/RefreshCW.svelte';
import Check from './icons/Check.svelte';
import Trash from './icons/Trash.svelte';
import Pen from './icons/Pen.svelte';
import { browser } from '$app/environment';
import { fly, fade, slide } from 'svelte/transition';
// headers for the table
let headers: string[] = ['Name', 'Type', 'Description'];
// get basePath since same component can be used to get guild commands
export let basePath = '';
export let id = 'global';
const queryClient = useQueryClient()
const queryClient = useQueryClient();
$: queryKey = ['app.commands', id];
$: refreshing = false;
$: commandList = createQuery({
queryKey,
queryFn: async () => (await fetchAPI(basePath)).data
queryFn: async () => (await fetchAPI(basePath)).data,
// disable automatic fetching in when not in browser (messes up dev and static builds)
enabled: browser
});
$: loading = $commandList.isLoading;
$: error = $commandList.isError;
$: fetching = $commandList.isFetching;
$: data = $commandList.data || [];
$: data = ($commandList.data || []) as DiscordInteraction[];
$: deletionConfirmPending = [] as string[]
$: handler = new DataHandler(data, { rowsPerPage: 10 })
$: rows = handler.getRows()
/** Functions */
const addLink = () => goto(`${base}/add${id === 'global' ? '' : `?guildId=${id}`}`)
// delete a interaction
async function deleteInteraction(interaction: DiscordInteraction) {
if(deletionConfirmPending.includes(interaction.id)) return toast.error(`Previous deletion confirmation is active.`)
deletionConfirmPending = deletionConfirmPending.concat([interaction.id])
const removeFromPending = () => { deletionConfirmPending = deletionConfirmPending.filter((id) => id !== interaction.id) }
// uses toast for confirmation
toast.warning(
`Are you sure you want to delete "${interaction.name}" ${typeToName(interaction.type)} Interaction?`,
`Are you sure you want to delete "${interaction.name}" ${typeToName(
interaction.type
)} Interaction?`,
{
action: {
label: 'Delete',
onClick: () => {
toast.promise(
fetchAPI(`${basePath}/${interaction.id}`, { method: 'DELETE' }).then(refreshList),
fetchAPI(`${basePath}/${interaction.id}`, { method: 'DELETE' }).then(() => { refreshList(); removeFromPending();}),
{
success: `${typeToName(interaction.type)} (${interaction.name}) successfully deleted`,
success: `${typeToName(interaction.type)} (${
interaction.name
}) successfully deleted`,
error: 'Error occurred, try again',
loading: 'Deletion pending...'
} as any
);
}
}
},
onAutoClose: removeFromPending,
onDismiss: removeFromPending,
duration: 20000
}
);
}
async function refreshList() {
if(refreshing) return;
if (refreshing) return;
refreshing = true;
queryClient.invalidateQueries({ queryKey })
queryClient.invalidateQueries({ queryKey });
setTimeout(() => {
// cooldown
refreshing = false
refreshing = false;
}, 2000);
}
</script>

<Datatable {handler} class="bg-primary-800 text-white rounded-t-lg rounded-b-lg min-w-full divide-y divide-primary-600">
<table>
<thead>
<tr class="shadow-md shadow-primary-700">
{#each headers as header (header)}
<Th
orderBy={header.toLowerCase()}
{handler}
scope="col"
class="px-6 py-3 text-left text-xs font-bold text-stone-400 uppercase tracking-wider"
>{header}</Th
<div class="flex flex-col gap-2">
<!-- control panel -->
<div class="bg-primary-800 w-full rounded-md flex items-center justify-between h-12 gap-1">
<!-- current counts -->
<span class="flex items-center flex-shrink-0 ml-2">{data.length} Interaction</span>
<div>
<!-- Status -->
{#if $commandList.isLoading}
<img src="{base}/slash.png" alt="loading..." class="animate-spin w-5 h-5 mx-auto" />
{:else if $commandList.isFetching}
<RefreshCw class="animate-spin text-yellow-300" />
{:else if $commandList.isError}
<p class="font-bold text-red-300">(╯°□°)╯︵ ┻━┻ | Error!!</p>
{:else}
<Check class="text-green-300" />
{/if}
</div>
<div class="flex">
<!-- control Buttons -->
<button class="bg-primary-800 h-10 hover:bg-primary-700 px-2" on:click={addLink}>
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="text-green-500"><path d="M5 12h14" /><path d="M12 5v14" /></svg
>
{/each}
<Th {handler} orderBy="" class="px-6 py-3 flex gap-2 text-xs font-bold text-stone-400 tracking-wider">
<button
on:click={() => goto(`${base}/add${id === 'global' ? '' : `?guildId=${id}`}`)}
class="bg-primary-600 hover:bg-primary-500 rounded-full p-2"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="text-green-500"><path d="M5 12h14" /><path d="M12 5v14" /></svg
>
</button>
<button
on:click={refreshList}
disabled={refreshing}
class="bg-primary-600 hover:bg-primary-500 rounded-full p-2"
>
<svg class="{refreshing && 'animate-spin cursor-not-allowed'} text-yellow-300" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12a9 9 0 0 0-9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"/><path d="M3 3v5h5"/><path d="M3 12a9 9 0 0 0 9 9 9.75 9.75 0 0 0 6.74-2.74L21 16"/><path d="M16 16h5v5"/></svg>
</button>
</Th>
</tr>
<tr>
{#each headers as header (header)}
<ThFilter
filterBy={header.toLowerCase()}
{handler}
scope="col"
class="px-6 py-3 text-left text-xs font-bold text-stone-400 uppercase tracking-wider"
>{header}</ThFilter
>
{/each}
<Th {handler} orderBy="" class="px-6 py-3 text-xs font-bold text-stone-400 tracking-wider">
<!-- EMPTY -->
</Th>
</tr>
</thead>
<tbody>
<!-- Status stuff -->
{#if loading}
<tr>
<td colspan={headers.length + 1} class="p-4">
<img src="{base}/slash.png" alt="loading..." class="animate-spin w-11 h-11 mx-auto" />
</td>
</tr>
{:else if error}
<tr>
<td colspan={headers.length + 1} class="p-4">
<div class="w-max h-11 mx-auto flex flex-col justify-center items-center">
<p class="font-bold text-lg">(╯°□°)╯︵ ┻━┻</p>
<span class="text-red-400 font-bold">Oops! Something went wrong.</span>
</button>
<button
on:click={refreshList}
disabled={refreshing || $commandList.isLoading}
class="bg-primary-800 h-10 hover:bg-primary-700 px-2"
>
<RefreshCw class="{refreshing && 'animate-spin cursor-not-allowed'} text-yellow-300" />
</button>
</div>
</div>
<div class="grid grid-cols-1 lg:grid-cols-4 my-3 gap-2">
<div role="button" tabindex="-1" on:keydown={addLink} on:click={addLink} class="bg-blurple-500 hover:scale-105 p-5 m-1 rounded-lg">
<h1 class="text-lg font-bold">Create a new interaction</h1>
<p class="text-sm font-semibold text-primary-100">Click here to create a new interaction!</p>
</div>
{#if $commandList.isLoading}
{#each Array.from({ length: 3 }, (_, index) => index) as cardId (cardId)}
<div class="relative bg-primary-700 m-1 p-5 rounded-lg">
<div class="flex flex-col gap-2 animate-pulse">
<div class="h-2 w-20 mt-4 bg-slate-700 rounded"></div>
<div class="h-2 w-24 bg-slate-700 rounded"></div>
<div class="h-2 w-36 bg-slate-700 rounded"></div>
</div>
<div class="absolute top-1 right-1 p-1 border-l border-b border-primary-600 hover:bg-primary-800">
<Trash class="animate-pulse opacity-80" />
</div>
</td>
</tr>
{:else if $rows.length == 0}
<tr>
<td colspan={headers.length + 1} class="p-4">
<div class="w-max h-11 mx-auto flex flex-col justify-center items-center">
<p class="font-bold text-lg">¯\_(ツ)_/¯</p>
<span class="text-yellow-200 font-bold">Nothing to display here.</span>
<div class="absolute bottom-1 right-1 p-1 border-l border-t border-primary-600 hover:bg-primary-800">
<Pen class="animate-pulse opacity-80" />
</div>
</td>
</tr>
{:else}
<!-- Table content -->
{#each $rows as row (row.id)}
<tr>
<td class="border-b-2 border-primary-400 px-4 py-4 whitespace-nowrap font-bold font-mono">{row.name}</td>
<td class="border-b-2 border-primary-400 px-4 py-4 whitespace-nowrap font-semibold">{typeToName(row.type)}</td>
<td class="border-b-2 border-primary-400 px-4 py-4 whitespace-nowrap">{row.description}</td>
<td class="border-b-2 border-primary-400 px-4 py-4 whitespace-nowrap flex justify-center items-center gap-2">
<button class="bg-primary-600 hover:bg-primary-500 rounded-full p-2">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="text-blurple-500 w-6 h-6"
>
<path d="M12 20h9" /><path d="M16.5 3.5a2.12 2.12 0 0 1 3 3L7 19l-4 1 1-4Z" /><path
d="m15 5 3 3"
/>
</svg>
</button>
<button
on:click={() => deleteInteraction(row)}
class="bg-primary-600 hover:bg-primary-500 rounded-full p-2"
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="text-red-600 w-6 h-6"
>
<path d="M3 6h18" /><path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6" /><path
d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2"
/><line x1="10" x2="10" y1="11" y2="17" /><line x1="14" x2="14" y1="11" y2="17" />
</svg>
</button>
</td>
</tr>
</div>
{/each}
{/if}

<!-- Table bottom, status bar -->
<tr class="opacity-75">
<td colspan={headers.length + 1} class="p-4 bg-primary-700 border-primary-400">
{#if fetching && !loading}
<svg class="animate-spin text-yellow-300 h-7 w-7 mx-auto" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12a9 9 0 0 0-9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"/><path d="M3 3v5h5"/><path d="M3 12a9 9 0 0 0 9 9 9.75 9.75 0 0 0 6.74-2.74L21 16"/><path d="M16 16h5v5"/></svg>
{:else if !loading && !error}
<svg class="animate-bounce text-green-300 h-7 w-7 mx-auto" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>
{#each data as interaction (interaction.id)}
<div in:fly={{ duration: 2000 }} out:fade class="relative {deletionConfirmPending.includes(interaction.id) && 'bg-red-800 animate-shake'} bg-primary-700 m-1 p-5 rounded-lg hover:scale-105">
<h1 class="text-lg font-bold">{interaction.name}</h1>
<span class="text-sm font-semibold text-primary-200">{typeToName(interaction.type)}</span>
<p class="text-sm text-primary-200">{interaction.description}</p>
{#if !deletionConfirmPending.includes(interaction.id)}
<button on:click={() => deleteInteraction(interaction)} class="absolute top-1 right-1 p-1 border-l border-b border-primary-600 hover:bg-primary-800">
<Trash class="text-red-300" />
</button>
<a href="/edit?commandId={interaction.id}&to={id}" class="absolute bottom-1 right-1 p-1 border-l border-t border-primary-600 hover:bg-primary-800">
<Pen class="text-blurple-300" />
</a>
{/if}
</td>
</tr>
</tbody>
</table>
</Datatable>
</div>
{/each}
</div>
</div>
10 changes: 10 additions & 0 deletions src/components/icons/Check.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<script>
/**
Original icon by lucide.dev
*/
let className = '';
export { className as class }
</script>

<svg class={className} xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/><polyline points="22 4 12 14.01 9 11.01"/></svg>
10 changes: 10 additions & 0 deletions src/components/icons/Pen.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<script>
/**
Original icon by lucide.dev
*/
let className = '';
export { className as class }
</script>

<svg class={className} xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17 3a2.85 2.83 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5Z"/></svg>
Loading

0 comments on commit 2e8307d

Please sign in to comment.