feat: items page
This commit is contained in:
@ -18,5 +18,7 @@
|
||||
--color-subtext-1: #bac2de;
|
||||
|
||||
--color-text: #cdd6f4;
|
||||
|
||||
--color-sky: #89dceb;
|
||||
--color-red: #f38ba8;
|
||||
}
|
296
client/src/lib/services/item/v1/item_pb.ts
Normal file
296
client/src/lib/services/item/v1/item_pb.ts
Normal file
@ -0,0 +1,296 @@
|
||||
// @generated by protoc-gen-es v2.2.3 with parameter "target=ts"
|
||||
// @generated from file item/v1/item.proto (package item.v1, syntax proto3)
|
||||
/* eslint-disable */
|
||||
|
||||
import type { GenFile, GenMessage, GenService } from "@bufbuild/protobuf/codegenv1";
|
||||
import { fileDesc, messageDesc, serviceDesc } from "@bufbuild/protobuf/codegenv1";
|
||||
import type { Timestamp } from "@bufbuild/protobuf/wkt";
|
||||
import { file_google_protobuf_timestamp } from "@bufbuild/protobuf/wkt";
|
||||
import type { Message } from "@bufbuild/protobuf";
|
||||
|
||||
/**
|
||||
* Describes the file item/v1/item.proto.
|
||||
*/
|
||||
export const file_item_v1_item: GenFile = /*@__PURE__*/
|
||||
fileDesc("ChJpdGVtL3YxL2l0ZW0ucHJvdG8SB2l0ZW0udjEinAEKBEl0ZW0SDwoCaWQYASABKA1IAIgBARIMCgRuYW1lGAIgASgJEhMKC2Rlc2NyaXB0aW9uGAMgASgJEg0KBXByaWNlGAQgASgCEhAKCHF1YW50aXR5GAUgASgNEi4KBWFkZGVkGAYgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcEgBiAEBQgUKA19pZEIICgZfYWRkZWQiHAoOR2V0SXRlbVJlcXVlc3QSCgoCaWQYASABKA0iLgoPR2V0SXRlbVJlc3BvbnNlEhsKBGl0ZW0YASABKAsyDS5pdGVtLnYxLkl0ZW0i3wEKD0dldEl0ZW1zUmVxdWVzdBIuCgVzdGFydBgBIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXBIAIgBARIsCgNlbmQYAiABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wSAGIAQESEwoGZmlsdGVyGAMgASgJSAKIAQESEgoFbGltaXQYBCABKA1IA4gBARITCgZvZmZzZXQYBSABKA1IBIgBAUIICgZfc3RhcnRCBgoEX2VuZEIJCgdfZmlsdGVyQggKBl9saW1pdEIJCgdfb2Zmc2V0Ij8KEEdldEl0ZW1zUmVzcG9uc2USHAoFaXRlbXMYASADKAsyDS5pdGVtLnYxLkl0ZW0SDQoFY291bnQYAiABKAQiMAoRQ3JlYXRlSXRlbVJlcXVlc3QSGwoEaXRlbRgBIAEoCzINLml0ZW0udjEuSXRlbSIxChJDcmVhdGVJdGVtUmVzcG9uc2USGwoEaXRlbRgBIAEoCzINLml0ZW0udjEuSXRlbSIwChFVcGRhdGVJdGVtUmVxdWVzdBIbCgRpdGVtGAEgASgLMg0uaXRlbS52MS5JdGVtIjEKElVwZGF0ZUl0ZW1SZXNwb25zZRIbCgRpdGVtGAEgASgLMg0uaXRlbS52MS5JdGVtIh8KEURlbGV0ZUl0ZW1SZXF1ZXN0EgoKAmlkGAEgASgNIhQKEkRlbGV0ZUl0ZW1SZXNwb25zZTLrAgoLSXRlbVNlcnZpY2USPgoHR2V0SXRlbRIXLml0ZW0udjEuR2V0SXRlbVJlcXVlc3QaGC5pdGVtLnYxLkdldEl0ZW1SZXNwb25zZSIAEkEKCEdldEl0ZW1zEhguaXRlbS52MS5HZXRJdGVtc1JlcXVlc3QaGS5pdGVtLnYxLkdldEl0ZW1zUmVzcG9uc2UiABJHCgpDcmVhdGVJdGVtEhouaXRlbS52MS5DcmVhdGVJdGVtUmVxdWVzdBobLml0ZW0udjEuQ3JlYXRlSXRlbVJlc3BvbnNlIgASRwoKVXBkYXRlSXRlbRIaLml0ZW0udjEuVXBkYXRlSXRlbVJlcXVlc3QaGy5pdGVtLnYxLlVwZGF0ZUl0ZW1SZXNwb25zZSIAEkcKCkRlbGV0ZUl0ZW0SGi5pdGVtLnYxLkRlbGV0ZUl0ZW1SZXF1ZXN0GhsuaXRlbS52MS5EZWxldGVJdGVtUmVzcG9uc2UiAEKdAQoLY29tLml0ZW0udjFCCUl0ZW1Qcm90b1ABWkZnaXRodWIuY29tL3Nwb3RkZW1vNC90cmV2c3RhY2svc2VydmVyL2ludGVybmFsL3NlcnZpY2VzL2l0ZW0vdjE7aXRlbXYxogIDSVhYqgIHSXRlbS5WMcoCB0l0ZW1cVjHiAhNJdGVtXFYxXEdQQk1ldGFkYXRh6gIISXRlbTo6VjFiBnByb3RvMw", [file_google_protobuf_timestamp]);
|
||||
|
||||
/**
|
||||
* @generated from message item.v1.Item
|
||||
*/
|
||||
export type Item = Message<"item.v1.Item"> & {
|
||||
/**
|
||||
* @generated from field: optional uint32 id = 1;
|
||||
*/
|
||||
id?: number;
|
||||
|
||||
/**
|
||||
* @generated from field: string name = 2;
|
||||
*/
|
||||
name: string;
|
||||
|
||||
/**
|
||||
* @generated from field: string description = 3;
|
||||
*/
|
||||
description: string;
|
||||
|
||||
/**
|
||||
* @generated from field: float price = 4;
|
||||
*/
|
||||
price: number;
|
||||
|
||||
/**
|
||||
* @generated from field: uint32 quantity = 5;
|
||||
*/
|
||||
quantity: number;
|
||||
|
||||
/**
|
||||
* @generated from field: optional google.protobuf.Timestamp added = 6;
|
||||
*/
|
||||
added?: Timestamp;
|
||||
};
|
||||
|
||||
/**
|
||||
* Describes the message item.v1.Item.
|
||||
* Use `create(ItemSchema)` to create a new message.
|
||||
*/
|
||||
export const ItemSchema: GenMessage<Item> = /*@__PURE__*/
|
||||
messageDesc(file_item_v1_item, 0);
|
||||
|
||||
/**
|
||||
* @generated from message item.v1.GetItemRequest
|
||||
*/
|
||||
export type GetItemRequest = Message<"item.v1.GetItemRequest"> & {
|
||||
/**
|
||||
* @generated from field: uint32 id = 1;
|
||||
*/
|
||||
id: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* Describes the message item.v1.GetItemRequest.
|
||||
* Use `create(GetItemRequestSchema)` to create a new message.
|
||||
*/
|
||||
export const GetItemRequestSchema: GenMessage<GetItemRequest> = /*@__PURE__*/
|
||||
messageDesc(file_item_v1_item, 1);
|
||||
|
||||
/**
|
||||
* @generated from message item.v1.GetItemResponse
|
||||
*/
|
||||
export type GetItemResponse = Message<"item.v1.GetItemResponse"> & {
|
||||
/**
|
||||
* @generated from field: item.v1.Item item = 1;
|
||||
*/
|
||||
item?: Item;
|
||||
};
|
||||
|
||||
/**
|
||||
* Describes the message item.v1.GetItemResponse.
|
||||
* Use `create(GetItemResponseSchema)` to create a new message.
|
||||
*/
|
||||
export const GetItemResponseSchema: GenMessage<GetItemResponse> = /*@__PURE__*/
|
||||
messageDesc(file_item_v1_item, 2);
|
||||
|
||||
/**
|
||||
* @generated from message item.v1.GetItemsRequest
|
||||
*/
|
||||
export type GetItemsRequest = Message<"item.v1.GetItemsRequest"> & {
|
||||
/**
|
||||
* @generated from field: optional google.protobuf.Timestamp start = 1;
|
||||
*/
|
||||
start?: Timestamp;
|
||||
|
||||
/**
|
||||
* @generated from field: optional google.protobuf.Timestamp end = 2;
|
||||
*/
|
||||
end?: Timestamp;
|
||||
|
||||
/**
|
||||
* @generated from field: optional string filter = 3;
|
||||
*/
|
||||
filter?: string;
|
||||
|
||||
/**
|
||||
* @generated from field: optional uint32 limit = 4;
|
||||
*/
|
||||
limit?: number;
|
||||
|
||||
/**
|
||||
* @generated from field: optional uint32 offset = 5;
|
||||
*/
|
||||
offset?: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* Describes the message item.v1.GetItemsRequest.
|
||||
* Use `create(GetItemsRequestSchema)` to create a new message.
|
||||
*/
|
||||
export const GetItemsRequestSchema: GenMessage<GetItemsRequest> = /*@__PURE__*/
|
||||
messageDesc(file_item_v1_item, 3);
|
||||
|
||||
/**
|
||||
* @generated from message item.v1.GetItemsResponse
|
||||
*/
|
||||
export type GetItemsResponse = Message<"item.v1.GetItemsResponse"> & {
|
||||
/**
|
||||
* @generated from field: repeated item.v1.Item items = 1;
|
||||
*/
|
||||
items: Item[];
|
||||
|
||||
/**
|
||||
* @generated from field: uint64 count = 2;
|
||||
*/
|
||||
count: bigint;
|
||||
};
|
||||
|
||||
/**
|
||||
* Describes the message item.v1.GetItemsResponse.
|
||||
* Use `create(GetItemsResponseSchema)` to create a new message.
|
||||
*/
|
||||
export const GetItemsResponseSchema: GenMessage<GetItemsResponse> = /*@__PURE__*/
|
||||
messageDesc(file_item_v1_item, 4);
|
||||
|
||||
/**
|
||||
* @generated from message item.v1.CreateItemRequest
|
||||
*/
|
||||
export type CreateItemRequest = Message<"item.v1.CreateItemRequest"> & {
|
||||
/**
|
||||
* @generated from field: item.v1.Item item = 1;
|
||||
*/
|
||||
item?: Item;
|
||||
};
|
||||
|
||||
/**
|
||||
* Describes the message item.v1.CreateItemRequest.
|
||||
* Use `create(CreateItemRequestSchema)` to create a new message.
|
||||
*/
|
||||
export const CreateItemRequestSchema: GenMessage<CreateItemRequest> = /*@__PURE__*/
|
||||
messageDesc(file_item_v1_item, 5);
|
||||
|
||||
/**
|
||||
* @generated from message item.v1.CreateItemResponse
|
||||
*/
|
||||
export type CreateItemResponse = Message<"item.v1.CreateItemResponse"> & {
|
||||
/**
|
||||
* @generated from field: item.v1.Item item = 1;
|
||||
*/
|
||||
item?: Item;
|
||||
};
|
||||
|
||||
/**
|
||||
* Describes the message item.v1.CreateItemResponse.
|
||||
* Use `create(CreateItemResponseSchema)` to create a new message.
|
||||
*/
|
||||
export const CreateItemResponseSchema: GenMessage<CreateItemResponse> = /*@__PURE__*/
|
||||
messageDesc(file_item_v1_item, 6);
|
||||
|
||||
/**
|
||||
* @generated from message item.v1.UpdateItemRequest
|
||||
*/
|
||||
export type UpdateItemRequest = Message<"item.v1.UpdateItemRequest"> & {
|
||||
/**
|
||||
* @generated from field: item.v1.Item item = 1;
|
||||
*/
|
||||
item?: Item;
|
||||
};
|
||||
|
||||
/**
|
||||
* Describes the message item.v1.UpdateItemRequest.
|
||||
* Use `create(UpdateItemRequestSchema)` to create a new message.
|
||||
*/
|
||||
export const UpdateItemRequestSchema: GenMessage<UpdateItemRequest> = /*@__PURE__*/
|
||||
messageDesc(file_item_v1_item, 7);
|
||||
|
||||
/**
|
||||
* @generated from message item.v1.UpdateItemResponse
|
||||
*/
|
||||
export type UpdateItemResponse = Message<"item.v1.UpdateItemResponse"> & {
|
||||
/**
|
||||
* @generated from field: item.v1.Item item = 1;
|
||||
*/
|
||||
item?: Item;
|
||||
};
|
||||
|
||||
/**
|
||||
* Describes the message item.v1.UpdateItemResponse.
|
||||
* Use `create(UpdateItemResponseSchema)` to create a new message.
|
||||
*/
|
||||
export const UpdateItemResponseSchema: GenMessage<UpdateItemResponse> = /*@__PURE__*/
|
||||
messageDesc(file_item_v1_item, 8);
|
||||
|
||||
/**
|
||||
* @generated from message item.v1.DeleteItemRequest
|
||||
*/
|
||||
export type DeleteItemRequest = Message<"item.v1.DeleteItemRequest"> & {
|
||||
/**
|
||||
* @generated from field: uint32 id = 1;
|
||||
*/
|
||||
id: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* Describes the message item.v1.DeleteItemRequest.
|
||||
* Use `create(DeleteItemRequestSchema)` to create a new message.
|
||||
*/
|
||||
export const DeleteItemRequestSchema: GenMessage<DeleteItemRequest> = /*@__PURE__*/
|
||||
messageDesc(file_item_v1_item, 9);
|
||||
|
||||
/**
|
||||
* @generated from message item.v1.DeleteItemResponse
|
||||
*/
|
||||
export type DeleteItemResponse = Message<"item.v1.DeleteItemResponse"> & {
|
||||
};
|
||||
|
||||
/**
|
||||
* Describes the message item.v1.DeleteItemResponse.
|
||||
* Use `create(DeleteItemResponseSchema)` to create a new message.
|
||||
*/
|
||||
export const DeleteItemResponseSchema: GenMessage<DeleteItemResponse> = /*@__PURE__*/
|
||||
messageDesc(file_item_v1_item, 10);
|
||||
|
||||
/**
|
||||
* @generated from service item.v1.ItemService
|
||||
*/
|
||||
export const ItemService: GenService<{
|
||||
/**
|
||||
* @generated from rpc item.v1.ItemService.GetItem
|
||||
*/
|
||||
getItem: {
|
||||
methodKind: "unary";
|
||||
input: typeof GetItemRequestSchema;
|
||||
output: typeof GetItemResponseSchema;
|
||||
},
|
||||
/**
|
||||
* @generated from rpc item.v1.ItemService.GetItems
|
||||
*/
|
||||
getItems: {
|
||||
methodKind: "unary";
|
||||
input: typeof GetItemsRequestSchema;
|
||||
output: typeof GetItemsResponseSchema;
|
||||
},
|
||||
/**
|
||||
* @generated from rpc item.v1.ItemService.CreateItem
|
||||
*/
|
||||
createItem: {
|
||||
methodKind: "unary";
|
||||
input: typeof CreateItemRequestSchema;
|
||||
output: typeof CreateItemResponseSchema;
|
||||
},
|
||||
/**
|
||||
* @generated from rpc item.v1.ItemService.UpdateItem
|
||||
*/
|
||||
updateItem: {
|
||||
methodKind: "unary";
|
||||
input: typeof UpdateItemRequestSchema;
|
||||
output: typeof UpdateItemResponseSchema;
|
||||
},
|
||||
/**
|
||||
* @generated from rpc item.v1.ItemService.DeleteItem
|
||||
*/
|
||||
deleteItem: {
|
||||
methodKind: "unary";
|
||||
input: typeof DeleteItemRequestSchema;
|
||||
output: typeof DeleteItemResponseSchema;
|
||||
},
|
||||
}> = /*@__PURE__*/
|
||||
serviceDesc(file_item_v1_item, 0);
|
||||
|
@ -1,8 +1,8 @@
|
||||
import { createConnectTransport } from "@connectrpc/connect-web"
|
||||
import { Code, ConnectError, createClient } from "@connectrpc/connect"
|
||||
import { Code, ConnectError, createClient, type Interceptor } from "@connectrpc/connect"
|
||||
import { AuthService } from "$lib/services/user/v1/auth_pb";
|
||||
import { UserService } from "$lib/services/user/v1/user_pb";
|
||||
import type { Interceptor } from "@connectrpc/connect";
|
||||
import { ItemService } from "$lib/services/item/v1/item_pb";
|
||||
import { goto } from "$app/navigation";
|
||||
|
||||
const redirector: Interceptor = (next) => async (req) => {
|
||||
@ -23,4 +23,5 @@ const transport = createConnectTransport({
|
||||
});
|
||||
|
||||
export const AuthClient = createClient(AuthService, transport);
|
||||
export const UserClient = createClient(UserService, transport);
|
||||
export const UserClient = createClient(UserService, transport);
|
||||
export const ItemClient = createClient(ItemService, transport);
|
51
client/src/lib/ui/Modal.svelte
Normal file
51
client/src/lib/ui/Modal.svelte
Normal file
@ -0,0 +1,51 @@
|
||||
<script lang="ts">
|
||||
import { Dialog } from 'bits-ui';
|
||||
import { fade } from 'svelte/transition';
|
||||
import type { Snippet } from 'svelte';
|
||||
|
||||
let {
|
||||
trigger,
|
||||
content,
|
||||
open = $bindable(false)
|
||||
}: { trigger: Snippet; content: Snippet; open: boolean } = $props();
|
||||
</script>
|
||||
|
||||
<Dialog.Root bind:open>
|
||||
<Dialog.Trigger>
|
||||
{@render trigger()}
|
||||
</Dialog.Trigger>
|
||||
<Dialog.Portal>
|
||||
<Dialog.Overlay forceMount>
|
||||
{#snippet child({ props, open })}
|
||||
{#if open}
|
||||
<div
|
||||
{...props}
|
||||
transition:fade={{
|
||||
duration: 100
|
||||
}}
|
||||
>
|
||||
<div class="fixed inset-0 z-50 mt-[50px] bg-black/50 transition-all"></div>
|
||||
</div>
|
||||
{/if}
|
||||
{/snippet}
|
||||
</Dialog.Overlay>
|
||||
<Dialog.Content forceMount>
|
||||
{#snippet child({ props, open })}
|
||||
{#if open}
|
||||
<div
|
||||
{...props}
|
||||
transition:fade={{
|
||||
duration: 100
|
||||
}}
|
||||
>
|
||||
<div
|
||||
class="bg-mantle border-surface-0 fixed inset-0 left-[50%] top-[50%] z-50 size-fit w-96 -translate-x-1/2 -translate-y-1/2 transform overflow-y-auto rounded-xl border pb-1 drop-shadow-md"
|
||||
>
|
||||
{@render content()}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{/snippet}
|
||||
</Dialog.Content>
|
||||
</Dialog.Portal>
|
||||
</Dialog.Root>
|
@ -34,7 +34,7 @@
|
||||
},
|
||||
{
|
||||
name: 'Items',
|
||||
href: '/items',
|
||||
href: '/items/',
|
||||
icon: LayoutList
|
||||
},
|
||||
{
|
||||
|
332
client/src/routes/(app)/items/+page.svelte
Normal file
332
client/src/routes/(app)/items/+page.svelte
Normal file
@ -0,0 +1,332 @@
|
||||
<script lang="ts">
|
||||
import { ItemClient } from '$lib/transport';
|
||||
import { Plus, Trash, Pencil } from '@lucide/svelte';
|
||||
import { timestampFromDate, timestampDate } from '@bufbuild/protobuf/wkt';
|
||||
import { Dialog, Button } from 'bits-ui';
|
||||
import { fade } from 'svelte/transition';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import { ConnectError } from '@connectrpc/connect';
|
||||
import Modal from '$lib/ui/Modal.svelte';
|
||||
import { SvelteMap } from 'svelte/reactivity';
|
||||
|
||||
// Config
|
||||
let limit: number = $state(10);
|
||||
let offset: number = $state(0);
|
||||
let start = $state(new Date(new Date().setDate(new Date().getDate() - 1)));
|
||||
let end = $state(new Date());
|
||||
let filter = $state('');
|
||||
|
||||
// Items
|
||||
let items = $state(getItems());
|
||||
let count: number = $state(0);
|
||||
|
||||
// Open
|
||||
let addedOpen = $state(false);
|
||||
let deletesOpen: SvelteMap<number, boolean> = new SvelteMap();
|
||||
let editsOpen: SvelteMap<number, boolean> = new SvelteMap();
|
||||
|
||||
async function getItems() {
|
||||
return await ItemClient.getItems({
|
||||
limit: limit,
|
||||
offset: offset,
|
||||
start: timestampFromDate(start),
|
||||
end: timestampFromDate(end),
|
||||
filter: filter
|
||||
}).then((resp) => {
|
||||
count = Number(resp.count);
|
||||
return resp.items;
|
||||
});
|
||||
}
|
||||
async function updateItems() {
|
||||
let i = getItems();
|
||||
i.then(() => {
|
||||
items = i;
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="border-surface-0 bg-mantle mx-4 mt-2 overflow-x-auto rounded border-x border-t drop-shadow-md"
|
||||
>
|
||||
<table class="w-full table-auto border-collapse text-left rtl:text-right">
|
||||
<thead>
|
||||
<tr class="border-surface-0 border-b">
|
||||
<th scope="col" class="text-subtext-0 px-6 py-3 font-normal">Added</th>
|
||||
<th scope="col" class="text-subtext-0 px-6 py-3 font-normal">Name</th>
|
||||
<th scope="col" class="text-subtext-0 px-6 py-3 font-normal">Description</th>
|
||||
<th scope="col" class="text-subtext-0 px-6 py-3 font-normal">Price</th>
|
||||
<th scope="col" class="text-subtext-0 px-6 py-3 font-normal">Quantity</th>
|
||||
<th class="w-0"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#await items}
|
||||
<tr class="border-surface-0 border-b">
|
||||
<td class="px-6 py-3"><div class="bg-surface-2 m-2 h-3 animate-pulse rounded"></div></td>
|
||||
<td class="px-6 py-3"><div class="bg-surface-2 m-2 h-3 animate-pulse rounded"></div></td>
|
||||
<td class="px-6 py-3"><div class="bg-surface-2 m-2 h-3 animate-pulse rounded"></div></td>
|
||||
<td class="px-6 py-3"><div class="bg-surface-2 m-2 h-3 animate-pulse rounded"></div></td>
|
||||
<td class="px-6 py-3"><div class="bg-surface-2 m-2 h-3 animate-pulse rounded"></div></td>
|
||||
</tr>
|
||||
{:then items}
|
||||
{#each items as item}
|
||||
<tr class="border-surface-0 border-b">
|
||||
<td class="px-6 py-3">
|
||||
{item.added ? timestampDate(item.added).toLocaleString() : ''}
|
||||
</td>
|
||||
<td class="px-6 py-3">{item.name}</td>
|
||||
<td class="px-6 py-3">{item.description}</td>
|
||||
<td class="px-6 py-3">{item.price}</td>
|
||||
<td class="px-6 py-3">{item.quantity}</td>
|
||||
<td class="pr-2">
|
||||
<div class="flex gap-2">
|
||||
<Modal bind:open={
|
||||
() => editsOpen.has(item.id!) ? editsOpen.get(item.id!)! : editsOpen.set(item.id!, false) && editsOpen.get(item.id!)!,
|
||||
(value) => editsOpen.set(item.id!, value)
|
||||
}>
|
||||
{#snippet trigger()}
|
||||
<button
|
||||
class="bg-text text-crust hover:brightness-120 block cursor-pointer rounded p-2 drop-shadow-md"
|
||||
>
|
||||
<Pencil />
|
||||
</button>
|
||||
{/snippet}
|
||||
|
||||
{#snippet content()}
|
||||
<h1 class="border-surface-0 border-b py-3 text-center text-xl font-bold">
|
||||
Edit {item.name}
|
||||
</h1>
|
||||
<form
|
||||
onsubmit={async (e) => {
|
||||
e.preventDefault();
|
||||
const form = e.target as HTMLFormElement;
|
||||
const formData = new FormData(form);
|
||||
const name = formData.get('name')?.toString();
|
||||
const description = formData.get('description')?.toString();
|
||||
const price = formData.get('price')?.toString();
|
||||
const quantity = formData.get('quantity')?.toString();
|
||||
|
||||
try {
|
||||
const response = await ItemClient.updateItem({
|
||||
item: {
|
||||
id: item.id,
|
||||
name: name,
|
||||
description: description,
|
||||
price: parseFloat(price ?? '0'),
|
||||
quantity: parseInt(quantity ?? '0')
|
||||
}
|
||||
});
|
||||
|
||||
if (response.item && item.id) {
|
||||
toast.success(`item "${name}" saved`);
|
||||
editsOpen.set(item.id, false)
|
||||
await updateItems();
|
||||
}
|
||||
} catch (err) {
|
||||
const error = ConnectError.from(err);
|
||||
toast.error(error.rawMessage);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div class="flex flex-col gap-4 p-3">
|
||||
<div class="flex flex-col gap-1">
|
||||
<label for="name" class="text-sm">Name</label>
|
||||
<input
|
||||
id="name"
|
||||
name="name"
|
||||
type="text"
|
||||
class="border-surface-0 rounded border p-2 text-sm"
|
||||
value={item.name}
|
||||
/>
|
||||
</div>
|
||||
<div class="flex flex-col gap-1">
|
||||
<label for="description" class="text-sm">Description</label>
|
||||
<input
|
||||
id="description"
|
||||
name="description"
|
||||
type="text"
|
||||
class="border-surface-0 rounded border p-2 text-sm"
|
||||
value={item.description}
|
||||
/>
|
||||
</div>
|
||||
<div class="flex flex-col gap-1">
|
||||
<label for="price" class="text-sm">Price</label>
|
||||
<input
|
||||
id="price"
|
||||
name="price"
|
||||
type="number"
|
||||
class="border-surface-0 rounded border p-2 text-sm"
|
||||
value={item.price}
|
||||
/>
|
||||
</div>
|
||||
<div class="flex flex-col gap-1">
|
||||
<label for="quantity" class="text-sm">Quantity</label>
|
||||
<input
|
||||
id="quantity"
|
||||
name="quantity"
|
||||
type="number"
|
||||
class="border-surface-0 rounded border p-2 text-sm"
|
||||
value={item.quantity}
|
||||
/>
|
||||
</div>
|
||||
<Button.Root
|
||||
type="submit"
|
||||
class="bg-sky text-crust hover:brightness-120 w-20 cursor-pointer rounded p-2 px-4 text-sm transition-all"
|
||||
>
|
||||
Submit
|
||||
</Button.Root>
|
||||
</div>
|
||||
</form>
|
||||
{/snippet}
|
||||
</Modal>
|
||||
|
||||
<Modal bind:open={
|
||||
() => deletesOpen.has(item.id!) ? deletesOpen.get(item.id!)! : deletesOpen.set(item.id!, false) && deletesOpen.get(item.id!)!,
|
||||
(value) => deletesOpen.set(item.id!, value)
|
||||
}>
|
||||
{#snippet trigger()}
|
||||
<button
|
||||
class="bg-red text-crust hover:brightness-120 block cursor-pointer rounded p-2 drop-shadow-md"
|
||||
>
|
||||
<Trash />
|
||||
</button>
|
||||
{/snippet}
|
||||
|
||||
{#snippet content()}
|
||||
<h1 class="border-surface-0 border-b py-3 text-center text-xl font-bold">
|
||||
Delete {item.name}
|
||||
</h1>
|
||||
<form
|
||||
onsubmit={async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
try {
|
||||
await ItemClient.deleteItem({
|
||||
id: item.id
|
||||
});
|
||||
|
||||
toast.success(`item "${item.name}" deleted`);
|
||||
deletesOpen.set(item.id!, false);
|
||||
await updateItems();
|
||||
} catch (err) {
|
||||
const error = ConnectError.from(err);
|
||||
toast.error(error.rawMessage);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div class="flex flex-col gap-4 p-3">
|
||||
<span class="text-center">Are you sure you want to delete "{item.name}"?</span
|
||||
>
|
||||
<div class="flex justify-center gap-4">
|
||||
<Button.Root
|
||||
type="submit"
|
||||
class="bg-sky text-crust hover:brightness-120 cursor-pointer rounded p-2 px-4 text-sm transition-all"
|
||||
>
|
||||
Confirm
|
||||
</Button.Root>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
{/snippet}
|
||||
</Modal>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
{/await}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="mx-4 mt-1 flex justify-end">
|
||||
<Modal bind:open={addedOpen}>
|
||||
{#snippet trigger()}
|
||||
<button
|
||||
class="bg-sky text-crust hover:brightness-120 cursor-pointer rounded p-2 px-4 drop-shadow-md"
|
||||
>
|
||||
<Plus />
|
||||
</button>
|
||||
{/snippet}
|
||||
|
||||
{#snippet content()}
|
||||
<h1 class="border-surface-0 border-b py-3 text-center text-xl font-bold">Add Item</h1>
|
||||
<form
|
||||
onsubmit={async (e) => {
|
||||
e.preventDefault();
|
||||
const form = e.target as HTMLFormElement;
|
||||
const formData = new FormData(form);
|
||||
const name = formData.get('name')?.toString();
|
||||
const description = formData.get('description')?.toString();
|
||||
const price = formData.get('price')?.toString();
|
||||
const quantity = formData.get('quantity')?.toString();
|
||||
|
||||
try {
|
||||
const response = await ItemClient.createItem({
|
||||
item: {
|
||||
name: name,
|
||||
description: description,
|
||||
price: parseFloat(price ?? '0'),
|
||||
quantity: parseInt(quantity ?? '0')
|
||||
}
|
||||
});
|
||||
|
||||
if (response.item) {
|
||||
form.reset();
|
||||
toast.success(`item "${name}" added`);
|
||||
addedOpen = false;
|
||||
await updateItems();
|
||||
}
|
||||
} catch (err) {
|
||||
const error = ConnectError.from(err);
|
||||
toast.error(error.rawMessage);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div class="flex flex-col gap-4 p-3">
|
||||
<div class="flex flex-col gap-1">
|
||||
<label for="name" class="text-sm">Name</label>
|
||||
<input
|
||||
id="name"
|
||||
name="name"
|
||||
type="text"
|
||||
class="border-surface-0 rounded border p-2 text-sm"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex flex-col gap-1">
|
||||
<label for="description" class="text-sm">Description</label>
|
||||
<input
|
||||
id="description"
|
||||
name="description"
|
||||
type="text"
|
||||
class="border-surface-0 rounded border p-2 text-sm"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex flex-col gap-1">
|
||||
<label for="price" class="text-sm">Price</label>
|
||||
<input
|
||||
id="price"
|
||||
name="price"
|
||||
type="number"
|
||||
class="border-surface-0 rounded border p-2 text-sm"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex flex-col gap-1">
|
||||
<label for="quantity" class="text-sm">Quantity</label>
|
||||
<input
|
||||
id="quantity"
|
||||
name="quantity"
|
||||
type="number"
|
||||
class="border-surface-0 rounded border p-2 text-sm"
|
||||
/>
|
||||
</div>
|
||||
<Button.Root
|
||||
type="submit"
|
||||
class="bg-sky text-crust hover:brightness-120 w-fit cursor-pointer rounded p-2 px-4 text-sm transition-all"
|
||||
>
|
||||
Submit
|
||||
</Button.Root>
|
||||
</div>
|
||||
</form>
|
||||
{/snippet}
|
||||
</Modal>
|
||||
</div>
|
Reference in New Issue
Block a user