Split into Svelte components
This commit is contained in:
parent
4c02dbb1d4
commit
0091614779
5 changed files with 147 additions and 84 deletions
|
@ -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>
|
||||
|
|
42
client/src/lib/ScheduleItem.svelte
Normal file
42
client/src/lib/ScheduleItem.svelte
Normal 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>
|
71
client/src/lib/ScheduleItemForm.svelte
Normal file
71
client/src/lib/ScheduleItemForm.svelte
Normal 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>
|
|
@ -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}`;
|
||||
}
|
||||
|
|
2
main.go
2
main.go
|
@ -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 {
|
||||
|
|
Loading…
Reference in a new issue