Split into Svelte components

This commit is contained in:
Jake Walker 2023-07-14 17:05:44 +01:00
parent 4c02dbb1d4
commit 0091614779
5 changed files with 147 additions and 84 deletions

View file

@ -1,40 +1,13 @@
<script type="ts">
import { useQuery, useMutation, useQueryClient } from '@sveltestack/svelte-query';
import { getSchedule, createScheduleItem, deleteScheduleItem } from './api';
import { TrashIcon, PlusIcon } from 'svelte-feather-icons';
const queryClient = useQueryClient();
<script lang="ts">
import { useQuery } from '@sveltestack/svelte-query';
import { getSchedule } from './api';
import { PlusIcon } from 'svelte-feather-icons';
import ScheduleItem from './ScheduleItem.svelte';
import ScheduleItemForm from './ScheduleItemForm.svelte';
const queryResult = useQuery("schedule", getSchedule);
const deleteMutation = useMutation(deleteScheduleItem, {
onSettled: () => {
queryClient.invalidateQueries("schedule")
}
});
const createMutation = useMutation(createScheduleItem, {
onSettled: () => {
queryClient.invalidateQueries("schedule")
}
});
let newItemName = "";
let newItemDate = (new Date()).toISOString().split("T")[0];
let newItemDuration = "1h";
let newItemRawTags = "";
$: newItemTags = newItemRawTags.split(";").filter(x => x);
function createItem() {
$createMutation.mutate({
name: newItemName,
date: newItemDate,
duration: newItemDuration,
tags: newItemTags
});
newItemName = "";
newItemDuration = "1h";
document.getElementById("name-input").focus();
}
</script>
<div class="container py-5">
@ -49,14 +22,18 @@
</div>
{:else if $queryResult.error}
<div class="alert alert-danger" role="alert">
<strong>Something has gone wrong!</strong> {$queryResult.error.message}
<strong>Something has gone wrong!</strong> {$queryResult.error.message || ""}
</div>
{:else}
{#each Object.entries($queryResult.data) as [date, items]}
<div class="card mb-3">
<div class="row g-0">
<div class="col-md-4 text-center p-3 d-flex align-items-center flex-row flex-md-column gap-2 justify-content-between justify-content-md-center">
<h5>{new Date(date).toDateString()}</h5>
<h5>{new Date(date).toLocaleDateString(undefined, {
weekday: "short",
month: "short",
day: "numeric"
})}</h5>
<button type="button" class="btn btn-primary btn-sm" on:click={() => {
newItemDate = date;
document.getElementById("name-input").focus();
@ -68,21 +45,7 @@
<div class="col-md-8 p-3">
<div class="vstack gap-3 justify-content-center h-100">
{#each items as item}
<div class="hstack gap-3">
<span class="me-auto">
{item.name}
{#each item.tags as tag}
<span class="badge bg-info me-1">{tag.name}</span>
{/each}
</span>
<button type="button" class="btn btn-danger btn-sm" on:click={() => {
if (!confirm(`Are you sure you want to delete "${item.name}"?`)) return;
$deleteMutation.mutate(item.id)
}}>
<TrashIcon size="16" />
Delete
</button>
</div>
<ScheduleItem item={item} />
{/each}
</div>
</div>
@ -94,37 +57,17 @@
<div class="col-md-6">
<h2>New</h2>
<form on:submit|preventDefault={createItem}>
<div class="mb-3">
<label for="name-input" class="form-label">Name</label>
<input type="text" class="form-control" id="name-input" bind:value={newItemName} required />
</div>
<div class="mb-3">
<label for="date-input" class="form-label">Date</label>
<input type="date" class="form-control" id="date-input" bind:value={newItemDate} required />
</div>
<div class="mb-3">
<label for="duration-input" class="form-label">Duration</label>
<input type="text" class="form-control" id="duration-input" bind:value={newItemDuration} required />
</div>
<div class="mb-3">
<label for="tags-input" class="form-label">Tags</label>
<input type="text" class="form-control" id="tags-input" bind:value={newItemRawTags} />
<div class="form-text">
{#each newItemTags as tag}
<span class="badge bg-info me-1">{tag}</span>
{/each}
</div>
</div>
<button type="submit" class="btn btn-primary">Submit</button>
</form>
<ScheduleItemForm bind:date={newItemDate} />
</div>
</div>
<h2>Raw Data</h2>
{#if $queryResult.data}
<div class="small mt-2">
<pre><code>{JSON.stringify($queryResult.data, null, 2)}</code></pre>
</div>
{#if import.meta.env.ENV != "prod"}
<h2>Development</h2>
<h3>Raw Data</h3>
{#if $queryResult.data}
<div class="small mt-2 bg-light p-3 br-3 rounded">
<pre><code>{JSON.stringify($queryResult.data, null, 2)}</code></pre>
</div>
{/if}
{/if}
</div>

View file

@ -0,0 +1,42 @@
<script lang="ts">
import { useMutation, useQueryClient } from '@sveltestack/svelte-query';
import { deleteScheduleItem } from './api';
import type { ScheduleItem } from './api';
import { TrashIcon } from 'svelte-feather-icons';
export let item: ScheduleItem;
const queryClient = useQueryClient();
const deleteMutation = useMutation(deleteScheduleItem, {
onSettled: () => {
queryClient.invalidateQueries("schedule")
}
});
function deleteAction() {
if (!confirm(`Are you sure you want to delete "${item.name}"?`)) {
return;
}
$deleteMutation.mutate(item.id);
}
</script>
<div class="hstack gap-3">
<span class="me-auto">
{item.name}
{#each item.tags as tag}
<span class="badge bg-info me-1">{tag.name}</span>
{/each}
</span>
<button type="button" class="btn btn-danger btn-sm" on:click={deleteAction} disabled="{$deleteMutation.isLoading}">
{#if $deleteMutation.isLoading}
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
{:else}
<TrashIcon size="16" />
{/if}
Delete
</button>
</div>

View file

@ -0,0 +1,71 @@
<script lang="ts">
import { useMutation, useQueryClient } from '@sveltestack/svelte-query';
import { createScheduleItem, prettyJoin } from './api';
import { PlusIcon, XIcon } from 'svelte-feather-icons';
export let name = "";
export let date = (new Date()).toISOString().split("T")[0];
export let tags = "";
const queryClient = useQueryClient();
const createMutation = useMutation(createScheduleItem, {
onSettled: () => {
queryClient.invalidateQueries("schedule")
}
});
$: parsedTags = tags.split(";").filter(x => x);
function createAction() {
$createMutation.mutate({
name,
date,
tags: parsedTags
});
resetAction();
}
function resetAction() {
name = "";
tags = "";
document.getElementById("name-input").focus();
}
</script>
<form on:submit|preventDefault={createAction}>
<div class="mb-3">
<label for="name-input" class="form-label">Name</label>
<input type="text" class="form-control" id="name-input" bind:value={name} required disabled="{$createMutation.isLoading}" />
</div>
<div class="mb-3">
<label for="date-input" class="form-label">Date</label>
<input type="date" class="form-control" id="date-input" bind:value={date} required disabled="{$createMutation.isLoading}" />
</div>
<div class="mb-3">
<label for="tags-input" class="form-label">Tags</label>
<input type="text" class="form-control" id="tags-input" bind:value={tags} disabled="{$createMutation.isLoading}" />
<div class="form-text">
Separate tags with semi-colons.
{#if parsedTags.length > 1}
There are {parsedTags.length} tags: {prettyJoin(parsedTags)}.
{:else if parsedTags.length > 0}
There is 1 tag: {parsedTags[0]}.
{/if}
</div>
</div>
<button type="submit" class="btn btn-primary" disabled="{$createMutation.isLoading}">
{#if $createMutation.isLoading}
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
{:else}
<PlusIcon size="18" />
{/if}
Submit
</button>
<button type="button" class="btn btn-outline-secondary" on:click={resetAction} disabled="{$createMutation.isLoading}">
<XIcon size="18" />
Clear
</button>
</form>

View file

@ -1,14 +1,14 @@
const SCHEDULE_DAYS = 14;
const API_URL = import.meta.env.PROD ? "/api" : import.meta.env.VITE_API_URL;
type ScheduleTag = {
export type ScheduleTag = {
id: number,
createdAt: string,
updatedAt: string,
name: string
}
type ScheduleItem = {
export type ScheduleItem = {
id: number,
createdAt: string,
updatedAt: string,
@ -19,7 +19,7 @@ type ScheduleItem = {
tags: ScheduleTag[]
};
type Schedule = { [date: string]: ScheduleItem[] };
export type Schedule = { [date: string]: ScheduleItem[] };
export async function getSchedule(): Promise<Schedule> {
const res = await fetch(`${API_URL}/schedule?days=${SCHEDULE_DAYS}`);
@ -31,7 +31,7 @@ export async function getSchedule(): Promise<Schedule> {
}
}
export async function createScheduleItem(data: { name: string, duration: string, date: string, tags: string[] }) {
export async function createScheduleItem(data: { name: string, date: string, tags: string[] }) {
const res = await fetch(`${API_URL}/items`, {
method: "POST",
headers: {
@ -61,3 +61,10 @@ export async function deleteScheduleItem(id: number) {
throw new Error(`Invalid response code ${res?.status}`);
}
}
export function prettyJoin(list: string[]) {
if (list.length === 1) return list[0];
const firsts = list.slice(0, list.length - 1);
const last = list[list.length - 1];
return `${firsts.join(", ")} and ${last}`;
}

View file

@ -126,7 +126,7 @@ func main() {
}
if input.Duration == nil {
return c.Status(fiber.StatusBadRequest).SendString("Duration is required")
input.Duration = &Duration{Duration: time.Hour}
}
if input.Date == nil {