diff --git a/.tool-versions b/.tool-versions new file mode 100644 index 0000000..557ea46 --- /dev/null +++ b/.tool-versions @@ -0,0 +1 @@ +nodejs 20.11.0 diff --git a/makefile b/makefile index 57c93d3..360cf20 100644 --- a/makefile +++ b/makefile @@ -1,3 +1,5 @@ +-include .env + default: dev dev: pb_types @@ -11,4 +13,4 @@ db: pb_types: @echo Reading schema from $(PUBLIC_PB_HOST) - @npx --yes pocketbase-typegen@1.2.1 --url $(PUBLIC_PB_HOST) --email $(PB_ADMIN_EMAIL) --password $(PB_ADMIN_PASSWORD) --out ./src/lib/pocketbase/index.d.ts + @npx --yes pocketbase-typegen@1.3.0 --url $(PUBLIC_PB_HOST) --email $(PB_ADMIN_EMAIL) --password $(PB_ADMIN_PASSWORD) --out ./src/lib/pocketbase/index.d.ts diff --git a/pocketbase/Dockerfile b/pocketbase/Dockerfile index db76def..8ce76c9 100644 --- a/pocketbase/Dockerfile +++ b/pocketbase/Dockerfile @@ -1,6 +1,6 @@ FROM alpine:latest -ARG PB_VERSION=0.22.21 +ARG PB_VERSION=0.24.1 RUN apk add --no-cache \ unzip \ diff --git a/src/lib/components/AuthButton.svelte b/src/lib/components/AuthButton.svelte index d819296..9c4b5dc 100644 --- a/src/lib/components/AuthButton.svelte +++ b/src/lib/components/AuthButton.svelte @@ -18,7 +18,7 @@ } async function login() { - const authData = await pb.collection("users").authWithOAuth2({ provider: "google" }); + const authData = await pb.collection("user").authWithOAuth2({ provider: "google" }); const meta = authData.meta; @@ -32,7 +32,7 @@ formData.append("avatar", avatar); } - await pb.collection("users").update(authData.record.id, formData); + await pb.collection("user").update(authData.record.id, formData); } document.cookie = pb.authStore.exportToCookie({ httpOnly: false }); diff --git a/src/lib/pocketbase/index.d.ts b/src/lib/pocketbase/index.d.ts index caee69c..f0aa2b6 100644 --- a/src/lib/pocketbase/index.d.ts +++ b/src/lib/pocketbase/index.d.ts @@ -6,13 +6,18 @@ import type PocketBase from 'pocketbase' import type { RecordService } from 'pocketbase' export enum Collections { - ActiveMessage = "activeMessage", - Categories = "categories", - DisplayMessages = "displayMessages", - Drinks = "drinks", - OrderDrink = "order_drink", - Orders = "orders", - Users = "users", + Authorigins = "_authOrigins", + Externalauths = "_externalAuths", + Mfas = "_mfas", + Otps = "_otps", + Superusers = "_superusers", + Category = "category", + Item = "item", + Message = "message", + Order = "order", + OrderItem = "order_item", + Status = "status", + User = "user", } // Alias types for improved usability @@ -23,8 +28,6 @@ export type HTMLString = string // System fields export type BaseSystemFields = { id: RecordIdString - created: IsoDateString - updated: IsoDateString collectionId: string collectionName: Collections expand?: T @@ -39,124 +42,186 @@ export type AuthSystemFields = { // Record types for each collection -export type ActiveMessageRecord = { - isVisible?: boolean - message?: RecordIdString +export type AuthoriginsRecord = { + collectionRef: string + created?: IsoDateString + fingerprint: string + id: string + recordRef: string + updated?: IsoDateString +} + +export type ExternalauthsRecord = { + collectionRef: string + created?: IsoDateString + id: string + provider: string + providerId: string + recordRef: string + updated?: IsoDateString +} + +export type MfasRecord = { + collectionRef: string + created?: IsoDateString + id: string + method: string + recordRef: string + updated?: IsoDateString +} + +export type OtpsRecord = { + collectionRef: string + created?: IsoDateString + id: string + password: string + recordRef: string + sentTo?: string + updated?: IsoDateString +} + +export type SuperusersRecord = { + created?: IsoDateString + email: string + emailVisibility?: boolean + id: string + password: string + tokenKey: string + updated?: IsoDateString + verified?: boolean } -export type CategoriesRecord = { +export type CategoryRecord = { + created?: IsoDateString + id: string name: string sort_order: number + updated?: IsoDateString } -export type DisplayMessagesRecord = { - subtext?: string - title?: string -} - -export type DrinksRecord = { +export type ItemRecord = { category: RecordIdString + created?: IsoDateString + id: string image: string name: string - price: number + price_nok: number + updated?: IsoDateString } -export enum OrderDrinkServingSizeOptions { - "small" = "small", - "big" = "big", - "custom" = "custom", -} - -export enum OrderDrinkMilkOptions { - "oat" = "oat", - "soy" = "soy", - "whole" = "whole", - "low-fat" = "low-fat", - "lactose-free" = "lactose-free", -} - -export enum OrderDrinkExtrasOptions { - "sirup" = "sirup", - "espresso" = "espresso", - "cream" = "cream", -} - -export enum OrderDrinkFlavorOptions { - "vanilla" = "vanilla", - "salt-caramel" = "salt-caramel", - "pumpkin-spice" = "pumpkin-spice", - "irish" = "irish", - "spicy" = "spicy", -} -export type OrderDrinkRecord = { - drink: RecordIdString - extras?: OrderDrinkExtrasOptions[] - flavor?: OrderDrinkFlavorOptions[] - milk?: OrderDrinkMilkOptions - serving_size?: OrderDrinkServingSizeOptions +export type MessageRecord = { + created?: IsoDateString + id: string + subtitle?: string + title?: string + updated?: IsoDateString } -export enum OrdersStateOptions { +export enum OrderStateOptions { "received" = "received", "production" = "production", "completed" = "completed", "dispatched" = "dispatched", } -export type OrdersRecord = { +export type OrderRecord = { + created?: IsoDateString customer?: RecordIdString - drinks: RecordIdString[] - payment_fulfilled?: boolean - state?: OrdersStateOptions + id: string + items: RecordIdString[] + state?: OrderStateOptions + updated?: IsoDateString +} + +export type OrderItemRecord = { + created?: IsoDateString + id: string + item: RecordIdString + updated?: IsoDateString } -export type UsersRecord = { +export type StatusRecord = { + created?: IsoDateString + id: string + message?: RecordIdString + online?: boolean + updated?: IsoDateString +} + +export type UserRecord = { avatar?: string - favorites?: RecordIdString[] + created?: IsoDateString + email?: string + emailVisibility?: boolean + id: string is_admin?: boolean name?: string - purchased_cup?: boolean + password: string + tokenKey: string + updated?: IsoDateString + username: string + verified?: boolean } // Response types include system fields and match responses from the PocketBase API -export type ActiveMessageResponse = Required & BaseSystemFields -export type CategoriesResponse = Required & BaseSystemFields -export type DisplayMessagesResponse = Required & BaseSystemFields -export type DrinksResponse = Required & BaseSystemFields -export type OrderDrinkResponse = Required & BaseSystemFields -export type OrdersResponse = Required & BaseSystemFields -export type UsersResponse = Required & AuthSystemFields +export type AuthoriginsResponse = Required & BaseSystemFields +export type ExternalauthsResponse = Required & BaseSystemFields +export type MfasResponse = Required & BaseSystemFields +export type OtpsResponse = Required & BaseSystemFields +export type SuperusersResponse = Required & AuthSystemFields +export type CategoryResponse = Required & BaseSystemFields +export type ItemResponse = Required & BaseSystemFields +export type MessageResponse = Required & BaseSystemFields +export type OrderResponse = Required & BaseSystemFields +export type OrderItemResponse = Required & BaseSystemFields +export type StatusResponse = Required & BaseSystemFields +export type UserResponse = Required & AuthSystemFields // Types containing all Records and Responses, useful for creating typing helper functions export type CollectionRecords = { - activeMessage: ActiveMessageRecord - categories: CategoriesRecord - displayMessages: DisplayMessagesRecord - drinks: DrinksRecord - order_drink: OrderDrinkRecord - orders: OrdersRecord - users: UsersRecord + _authOrigins: AuthoriginsRecord + _externalAuths: ExternalauthsRecord + _mfas: MfasRecord + _otps: OtpsRecord + _superusers: SuperusersRecord + category: CategoryRecord + item: ItemRecord + message: MessageRecord + order: OrderRecord + order_item: OrderItemRecord + status: StatusRecord + user: UserRecord } export type CollectionResponses = { - activeMessage: ActiveMessageResponse - categories: CategoriesResponse - displayMessages: DisplayMessagesResponse - drinks: DrinksResponse - order_drink: OrderDrinkResponse - orders: OrdersResponse - users: UsersResponse + _authOrigins: AuthoriginsResponse + _externalAuths: ExternalauthsResponse + _mfas: MfasResponse + _otps: OtpsResponse + _superusers: SuperusersResponse + category: CategoryResponse + item: ItemResponse + message: MessageResponse + order: OrderResponse + order_item: OrderItemResponse + status: StatusResponse + user: UserResponse } // Type for usage with type asserted PocketBase instance // https://github.com/pocketbase/js-sdk#specify-typescript-definitions export type TypedPocketBase = PocketBase & { - collection(idOrName: 'activeMessage'): RecordService - collection(idOrName: 'categories'): RecordService - collection(idOrName: 'displayMessages'): RecordService - collection(idOrName: 'drinks'): RecordService - collection(idOrName: 'order_drink'): RecordService - collection(idOrName: 'orders'): RecordService - collection(idOrName: 'users'): RecordService + collection(idOrName: '_authOrigins'): RecordService + collection(idOrName: '_externalAuths'): RecordService + collection(idOrName: '_mfas'): RecordService + collection(idOrName: '_otps'): RecordService + collection(idOrName: '_superusers'): RecordService + collection(idOrName: 'category'): RecordService + collection(idOrName: 'item'): RecordService + collection(idOrName: 'message'): RecordService + collection(idOrName: 'order'): RecordService + collection(idOrName: 'order_item'): RecordService + collection(idOrName: 'status'): RecordService + collection(idOrName: 'user'): RecordService } diff --git a/src/lib/pocketbase/index.ts b/src/lib/pocketbase/index.ts index 096493c..171e3e9 100644 --- a/src/lib/pocketbase/index.ts +++ b/src/lib/pocketbase/index.ts @@ -1,10 +1,10 @@ import PocketBase from "pocketbase"; import { PUBLIC_PB_HOST } from "$env/static/public"; -import { type TypedPocketBase, Collections, OrdersStateOptions } from "$lib/pocketbase/index.d"; +import { type TypedPocketBase, Collections, OrderStateOptions } from "$lib/pocketbase/index.d"; const pb = new PocketBase(PUBLIC_PB_HOST) as TypedPocketBase; pb.autoCancellation(false); export default pb; -export { Collections, OrdersStateOptions }; +export { Collections, OrderStateOptions }; export type * from "$lib/pocketbase/index.d"; diff --git a/src/lib/stores/menuStore.ts b/src/lib/stores/menuStore.ts index abc50ae..d62a3ec 100644 --- a/src/lib/stores/menuStore.ts +++ b/src/lib/stores/menuStore.ts @@ -2,9 +2,9 @@ import { createGenericPbStore } from "$stores/pbStore"; import { Collections } from "$lib/pocketbase"; import { Item, Category } from "$lib/types"; -export const categories = createGenericPbStore(Collections.Categories, Category, { +export const categories = createGenericPbStore(Collections.Category, Category, { sort: "sort_order", - expand: "drinks_via_category" + expand: "item_via_category" }); -export const items = createGenericPbStore(Collections.Drinks, Item); +export const items = createGenericPbStore(Collections.Item, Item); diff --git a/src/lib/stores/messageStore.ts b/src/lib/stores/messageStore.ts index fba5833..0e185fa 100644 --- a/src/lib/stores/messageStore.ts +++ b/src/lib/stores/messageStore.ts @@ -1,5 +1,5 @@ import { createGenericPbStore } from "$stores/pbStore"; -import pb, { Collections, type DisplayMessagesResponse } from "$lib/pocketbase"; +import pb, { Collections, type MessageResponse } from "$lib/pocketbase"; import { Message, ActiveMessage } from "$lib/types"; import { writable } from "svelte/store"; @@ -7,7 +7,7 @@ import eventsource from "eventsource"; // eslint-disable-next-line @typescript-eslint/no-explicit-any (global as any).EventSource = eventsource; -export const messages = createGenericPbStore(Collections.DisplayMessages, Message); +export const messages = createGenericPbStore(Collections.Message, Message); function createActiveMessageStore() { // Initialize with dummy non-visible message @@ -18,34 +18,32 @@ function createActiveMessageStore() { message: new Message({ id: "", title: "", - subtext: "" + subtitle: "" } as Message) } as ActiveMessage) ); (async () => { // Only use the first record. Assumes that PB already has this and only this record. - const initialActiveMessage = await pb - .collection(Collections.ActiveMessage) - .getFirstListItem(""); - - const initialMessages: DisplayMessagesResponse[] = await pb - .collection(Collections.DisplayMessages) + const initialActiveMessage = await pb.collection(Collections.Status).getFirstListItem(""); + const initialMessages: MessageResponse[] = await pb + .collection(Collections.Message) .getFullList(); - const initialMessage: DisplayMessagesResponse = + + const initialMessage: MessageResponse = initialMessages.filter((message) => message.id == initialActiveMessage.message)[0] || - ({ id: "", title: "", subtext: "" } as DisplayMessagesResponse); + ({ id: "", title: "", subtitle: "" } as MessageResponse); const initialData = ActiveMessage.fromPb(initialActiveMessage, Message.fromPb(initialMessage)); set(initialData); - pb.collection(Collections.ActiveMessage).subscribe("*", (event) => { + pb.collection(Collections.Status).subscribe("*", (event) => { update((state) => { return ActiveMessage.fromPb(event.record, state.message); }); }); - pb.collection(Collections.DisplayMessages).subscribe("*", (event) => { + pb.collection(Collections.Message).subscribe("*", (event) => { update((state) => { if (event.record.id == state.message.id) { state.message = Message.fromPb(event.record); @@ -61,6 +59,6 @@ function createActiveMessageStore() { export const activeMessage = { subscribe: createActiveMessageStore(), update: async (activeMessage: ActiveMessage) => { - await pb.collection(Collections.ActiveMessage).update(activeMessage.id, activeMessage.toPb()); + await pb.collection(Collections.Status).update(activeMessage.id, activeMessage.toPb()); } }; diff --git a/src/lib/stores/orderStore.ts b/src/lib/stores/orderStore.ts index ef2d7a3..73aabf7 100644 --- a/src/lib/stores/orderStore.ts +++ b/src/lib/stores/orderStore.ts @@ -7,37 +7,39 @@ import { get } from "svelte/store"; const today = new Date().toISOString().split("T")[0]; const baseOptions = { - expand: "drinks,drinks.drink", + // Table `item` + // + col `item` which references `order_item` + expand: "items,items.item", filter: `created >= "${today}"` }; export default { - subscribe: createPbStore(Collections.Orders, Order, baseOptions), + subscribe: createPbStore(Collections.Order, Order, baseOptions), create: async (userId: RecordIdString, itemIds: RecordIdString[]) => { const getOrderItemIds = async (): Promise => { return await Promise.all( itemIds.map(async (itemId) => { - const response = await pb.collection(Collections.OrderDrink).create({ drink: itemId }); + const response = await pb.collection(Collections.OrderItem).create({ item: itemId }); return response.id; }) ); }; - await pb.collection(Collections.Orders).create({ + await pb.collection(Collections.Order).create({ customer: userId, - drinks: await getOrderItemIds(), + items: await getOrderItemIds(), state: State.received, payment_fulfilled: false }); }, updateState: (orderId: RecordIdString, state: State) => { - pb.collection(Collections.Orders).update(orderId, { state }); + pb.collection(Collections.Order).update(orderId, { state }); } }; -export const userOrders = createGenericPbStore(Collections.Orders, Order, { +export const userOrders = createGenericPbStore(Collections.Order, Order, { ...baseOptions, filter: `customer = '${get(auth).user.id}'` }); diff --git a/src/lib/types.ts b/src/lib/types.ts index 68b0df3..fd613dd 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -1,12 +1,12 @@ import pb, { type RecordIdString, - OrdersStateOptions, - type DrinksResponse, - type ActiveMessageResponse, - type OrdersResponse, - type OrderDrinkResponse, - type CategoriesResponse, - type DisplayMessagesResponse + OrderStateOptions, + type ItemResponse, + type MessageResponse, + type OrderResponse, + type OrderItemResponse, + type CategoryResponse, + type StatusResponse } from "$lib/pocketbase"; import { restrictedRoutes, adminRoutes } from "$lib/constants"; import type { AuthModel } from "pocketbase"; @@ -25,8 +25,8 @@ export class NavItem { } } -type State = OrdersStateOptions; -export { OrdersStateOptions as State }; +type State = OrderStateOptions; +export { OrderStateOptions as State }; export interface RecordBase { id: RecordIdString; @@ -63,8 +63,8 @@ export class User extends Record { } // orders -export type ExpandedOrderRecord = OrdersResponse & { - expand: { drinks: ExpandedOrderDrinkRecord[] }; +export type ExpandedOrderRecord = OrderResponse & { + expand: { items: ExpandedOrderItemRecord[] }; }; export class Order extends Record implements RecordBase { @@ -86,13 +86,13 @@ export class Order extends Record implements RecordBase { return new Order({ id: data.id, state: data.state, - items: data.expand.drinks.map(OrderItem.fromPb) + items: data.expand.items.map(OrderItem.fromPb) } as Order); } } -export type ExpandedOrderDrinkRecord = OrderDrinkResponse & { - expand: { drink: DrinksResponse }; +export type ExpandedOrderItemRecord = OrderItemResponse & { + expand: { item: ItemResponse }; }; export class OrderItem extends Record implements RecordBase { @@ -109,11 +109,11 @@ export class OrderItem extends Record implements RecordBase { return this; } - static fromPb(data: ExpandedOrderDrinkRecord): OrderItem { + static fromPb(data: ExpandedOrderItemRecord): OrderItem { return new OrderItem({ id: data.id, - name: data.expand.drink.name, - item: Item.fromPb(data.expand.drink) + name: data.expand.item.name, + item: Item.fromPb(data.expand.item) } as OrderItem); } } @@ -136,19 +136,19 @@ export class Item extends Record implements RecordBase { return this; } - static fromPb(data: DrinksResponse): Item { + static fromPb(data: ItemResponse): Item { return new Item({ id: data.id, name: data.name, - price: data.price, + price: data.price_nok, category: data.category, image: pb.files.getUrl(data, data.image) } as Item); } } -export type ExpandedCategoryRecord = CategoriesResponse & { - expand: { drinks_via_category: DrinksResponse[] }; +export type ExpandedCategoryRecord = CategoryResponse & { + expand: { item_via_category: ItemResponse[] }; }; export class Category extends Record implements RecordBase { @@ -172,7 +172,7 @@ export class Category extends Record implements RecordBase { id: data.id, name: data.name, sortOrder: data.sort_order, - items: data.expand.drinks_via_category.map(Item.fromPb) + items: data.expand.item_via_category.map(Item.fromPb) } as Category); } } @@ -180,27 +180,31 @@ export class Category extends Record implements RecordBase { // messages export class Message extends Record implements RecordBase { title: string; - subtext: string; + subtitle: string; constructor(data: Message) { super(data); this.title = data.title; - this.subtext = data.subtext; + this.subtitle = data.subtitle; } toPb() { - return this; + return { title: this.title, subtitle: this.subtitle }; } - static fromPb(data: DisplayMessagesResponse): Message { + static fromPb(data: MessageResponse): Message { return new Message({ id: data.id, title: data.title, - subtext: data.subtext + subtitle: data.subtitle } as Message); } } +export type ExpandedActiveMessageRecord = StatusResponse & { + expand: { message: MessageResponse }; +}; + export class ActiveMessage extends Record implements RecordBase { message: Message; visible: boolean; @@ -212,14 +216,14 @@ export class ActiveMessage extends Record implements RecordBase { } toPb() { - return { message: this.message.id, isVisible: this.visible }; + return { message: this.message.id, online: this.visible }; } - static fromPb(activeMessage: ActiveMessageResponse, message: Message): ActiveMessage { + static fromPb(activeMessage: StatusResponse, message: Message): ActiveMessage { return new ActiveMessage({ id: activeMessage.id, message: message, - visible: activeMessage.isVisible + visible: activeMessage.online } as ActiveMessage); } } diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index 7a09e8f..aee8e28 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -19,7 +19,7 @@
    {#if $activeMessage.visible}
  • {$activeMessage.message.title}
  • -
  • {$activeMessage.message.subtext}
  • +
  • {$activeMessage.message.subtitle}
  • {:else}
  • Vi er åpen!
  • {/if} diff --git a/src/routes/admin/message/+page.svelte b/src/routes/admin/message/+page.svelte index 6fbe8be..a7cd853 100644 --- a/src/routes/admin/message/+page.svelte +++ b/src/routes/admin/message/+page.svelte @@ -12,7 +12,7 @@ ); }; - const handleMessageTextChange = (event: Event, message: Message, field: "title" | "subtext") => { + const handleMessageTextChange = (event: Event, message: Message, field: "title" | "subtitle") => { messages.update( new Message({ ...message, @@ -54,9 +54,9 @@ handleMessageTextChange(event, message, "subtext")} + oninput={(event) => handleMessageTextChange(event, message, "subtitle")} />
diff --git a/src/routes/admin/orders/OrderList.svelte b/src/routes/admin/orders/OrderList.svelte index bc37385..d4ed4f6 100644 --- a/src/routes/admin/orders/OrderList.svelte +++ b/src/routes/admin/orders/OrderList.svelte @@ -1,10 +1,10 @@
@@ -17,8 +17,8 @@
diff --git a/src/routes/admin/orders/frontdesk/Cart.svelte b/src/routes/admin/orders/frontdesk/Cart.svelte index 0fde76f..ab129fc 100644 --- a/src/routes/admin/orders/frontdesk/Cart.svelte +++ b/src/routes/admin/orders/frontdesk/Cart.svelte @@ -1,7 +1,7 @@
diff --git a/src/routes/display/+layout.svelte b/src/routes/display/+layout.svelte index 42d0872..452f1e8 100644 --- a/src/routes/display/+layout.svelte +++ b/src/routes/display/+layout.svelte @@ -14,7 +14,7 @@ {#if $activeMessage?.visible}
{$activeMessage.message.title} - {$activeMessage.message.subtext} + {$activeMessage.message.subtitle}
{:else}
diff --git a/src/routes/display/+page.svelte b/src/routes/display/+page.svelte index d7be677..18ceae6 100644 --- a/src/routes/display/+page.svelte +++ b/src/routes/display/+page.svelte @@ -1,7 +1,7 @@
import orders from "$stores/orderStore"; - import { OrdersStateOptions } from "$lib/pocketbase"; + import { OrderStateOptions } from "$lib/pocketbase"; interface Props { - show: OrdersStateOptions[]; + show: OrderStateOptions[]; } let { show }: Props = $props();