diff --git a/apps/api/apps/catalog/routes/things.js b/apps/api/apps/catalog/routes/things.js index 63ff70d..0b91a60 100644 --- a/apps/api/apps/catalog/routes/things.js +++ b/apps/api/apps/catalog/routes/things.js @@ -2,6 +2,7 @@ const express = require('express'); const router = express.Router(); const { getCatalogData } = require('../services/catalog'); const { getThingDetails } = require('../services/thingDetails'); +const { getItemDetails } = require('../services/itemDetails'); router.get('/', async (req, res) => { try { @@ -22,4 +23,14 @@ router.get('/things/:id', async (req, res) => { } }); +router.get('/items/:id', async (req, res) => { + const { id } = req.params; + try { + res.send(await getItemDetails(id)); + } catch (error) { + console.error(error); + res.status(error.status || 500).send({ errors: [error] }); + } +}); + module.exports = router; \ No newline at end of file diff --git a/apps/api/apps/catalog/services/itemDetails.js b/apps/api/apps/catalog/services/itemDetails.js new file mode 100644 index 0000000..160a95f --- /dev/null +++ b/apps/api/apps/catalog/services/itemDetails.js @@ -0,0 +1,25 @@ +const { fetchItem } = require("../../../services/inventory"); +const mapItemStatus = require("./mapItemStatus"); + +async function getItemDetails(id) { + const details = await fetchItem(null, { recordId: id }); + + return { + id: details.id, + name: details.name, + number: details.number, + spanishName: details.name_es, + available: details.available, + availableDate: details.dueBack, + condition: details.condition, + eyeProtection: details.eyeProtection, + totalLoans: details.totalLoans, + image: details.images.length ? details.images[0] : undefined, + manuals: details.manuals?.map((m) => m.url) || [], + status: mapItemStatus(details) + }; +} + +module.exports = { + getItemDetails +} \ No newline at end of file diff --git a/apps/api/apps/catalog/services/mapItemStatus.js b/apps/api/apps/catalog/services/mapItemStatus.js new file mode 100644 index 0000000..dafc981 --- /dev/null +++ b/apps/api/apps/catalog/services/mapItemStatus.js @@ -0,0 +1,9 @@ +function mapItemStatus(item) { + if (item.available) { + return 'available'; + } + + return 'checkedOut'; +} + +module.exports = mapItemStatus; \ No newline at end of file diff --git a/apps/api/apps/catalog/services/thingDetails.js b/apps/api/apps/catalog/services/thingDetails.js index 8d9ced8..a2f7434 100644 --- a/apps/api/apps/catalog/services/thingDetails.js +++ b/apps/api/apps/catalog/services/thingDetails.js @@ -1,4 +1,5 @@ const { fetchThing } = require("../../../services/things"); +const mapItemStatus = require("./mapItemStatus"); async function getThingDetails(id) { const details = await fetchThing(id); @@ -22,14 +23,6 @@ async function getThingDetails(id) { }; } -function mapItemStatus(item) { - if (item.available) { - return 'available'; - } - - return 'checkedOut'; -} - module.exports = { getThingDetails } \ No newline at end of file diff --git a/apps/api/package.json b/apps/api/package.json index 6d4278e..bd55a86 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -1,6 +1,6 @@ { "name": "pvdthings-api", - "version": "1.20.1", + "version": "1.21.0", "description": "", "main": "server.js", "scripts": { diff --git a/apps/api/services/inventory/mapItem.js b/apps/api/services/inventory/mapItem.js index 4430eb7..8e7c16b 100644 --- a/apps/api/services/inventory/mapItem.js +++ b/apps/api/services/inventory/mapItem.js @@ -6,12 +6,14 @@ function mapItem(record) { id: record.id, number: Number(record.get('ID')), name: record.get('Name')[0], + name_es: record.get('name_es')?.[0], available: record.get('Active Loans') === 0 && !hidden && !isThingHidden, hidden: hidden || isThingHidden, brand: record.get('Brand'), description: record.get('Description'), + dueBack: record.get('Due Back')?.[0], estimatedValue: record.get('Estimated Value'), eyeProtection: Boolean(record.get('Eye Protection')), condition: record.get('Condition'), diff --git a/apps/api/services/inventory/service.js b/apps/api/services/inventory/service.js index 1e20f33..06932c0 100644 --- a/apps/api/services/inventory/service.js +++ b/apps/api/services/inventory/service.js @@ -7,6 +7,7 @@ const inventoryFields = [ 'Name', 'Brand', 'Description', + 'Due Back', 'Eye Protection', 'Active Loans', 'Total Loans', @@ -30,7 +31,13 @@ const fetchItems = async () => { return records.map((r) => mapItem(r)); } -const fetchItem = async (id) => { +// Tech Debt: "id" here is actually the item number +const fetchItem = async (id, { recordId } = { recordId: undefined }) => { + if (recordId) { + const record = await items.find(recordId); + return mapItem(record); + } + const records = await items.select({ view: 'api_fetch_things', fields: inventoryFields, diff --git a/apps/web/package.json b/apps/web/package.json index 33d2db6..b01d228 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -1,6 +1,6 @@ { "name": "@library_os/web", - "version": "0.16.1", + "version": "0.17.0", "private": true, "scripts": { "dev": "vite dev", diff --git a/apps/web/src/lib/components/Shell/Drawer/NavigationButton/BackButton.svelte b/apps/web/src/lib/components/Shell/Drawer/NavigationButton/BackButton.svelte new file mode 100644 index 0000000..d7add43 --- /dev/null +++ b/apps/web/src/lib/components/Shell/Drawer/NavigationButton/BackButton.svelte @@ -0,0 +1,10 @@ + + + \ No newline at end of file diff --git a/apps/web/src/lib/components/Shell/Drawer/NavigationButton/CloseButton.svelte b/apps/web/src/lib/components/Shell/Drawer/NavigationButton/CloseButton.svelte new file mode 100644 index 0000000..cc4058b --- /dev/null +++ b/apps/web/src/lib/components/Shell/Drawer/NavigationButton/CloseButton.svelte @@ -0,0 +1,10 @@ + + + \ No newline at end of file diff --git a/apps/web/src/lib/components/Shell/Drawer/NavigationButton/NavigationButton.svelte b/apps/web/src/lib/components/Shell/Drawer/NavigationButton/NavigationButton.svelte new file mode 100644 index 0000000..2d95a99 --- /dev/null +++ b/apps/web/src/lib/components/Shell/Drawer/NavigationButton/NavigationButton.svelte @@ -0,0 +1,14 @@ + + +{#if $poppable} + +{:else} + +{/if} \ No newline at end of file diff --git a/apps/web/src/lib/components/Shell/Drawer/NavigationButton/index.ts b/apps/web/src/lib/components/Shell/Drawer/NavigationButton/index.ts new file mode 100644 index 0000000..8e6a1f1 --- /dev/null +++ b/apps/web/src/lib/components/Shell/Drawer/NavigationButton/index.ts @@ -0,0 +1 @@ +export { default as NavigationButton } from "./NavigationButton.svelte"; \ No newline at end of file diff --git a/apps/web/src/lib/components/Shell/Drawer/drawer.ts b/apps/web/src/lib/components/Shell/Drawer/drawer.ts new file mode 100644 index 0000000..f35d5b8 --- /dev/null +++ b/apps/web/src/lib/components/Shell/Drawer/drawer.ts @@ -0,0 +1,53 @@ +import { derived, writable } from 'svelte/store'; + +const emptyState = Object.freeze({ views: [] }); + +let previousViewsLength = 0; + +export type DrawerState = { + views: DrawerContent[]; +}; + +export type DrawerContent = { + component: any; + props: any; +}; + +const state = writable(emptyState); + +export const view = derived(state, (s) => { + return s.views.length ? s.views[0] : undefined; +}); + +export const push = (component: any, props: any) => { + state.update((s) => ({ + views: [{ component, props }, ...s.views] + })); +}; + +export const pop = () => { + state.update((s) => ({ + views: s.views.slice(1) + })); +}; + +export const poppable = derived(state, (s) => s.views.length && s.views.length > 1); + +export const close = () => { + state.update((s) => emptyState); +}; + +// pushing second view does not work +export const toggle = (callback: () => void) => { + return state.subscribe(s => { + if (previousViewsLength === 0 && s.views.length) { + callback(); + } + + if (previousViewsLength && !s.views.length) { + callback(); + } + + previousViewsLength = s.views.length; + }); +}; \ No newline at end of file diff --git a/apps/web/src/lib/components/Shell/Drawer/index.ts b/apps/web/src/lib/components/Shell/Drawer/index.ts new file mode 100644 index 0000000..62ebccc --- /dev/null +++ b/apps/web/src/lib/components/Shell/Drawer/index.ts @@ -0,0 +1,2 @@ +export * from "./drawer"; +export * from "./NavigationButton"; \ No newline at end of file diff --git a/apps/web/src/lib/components/Shell/Shell.svelte b/apps/web/src/lib/components/Shell/Shell.svelte index ea9e55c..4fbdac5 100644 --- a/apps/web/src/lib/components/Shell/Shell.svelte +++ b/apps/web/src/lib/components/Shell/Shell.svelte @@ -1,26 +1,13 @@
@@ -30,9 +17,12 @@
- -
- +
+
+ {#if $view} + + + {/if}
\ No newline at end of file diff --git a/apps/web/src/lib/components/Shell/ShellContext.ts b/apps/web/src/lib/components/Shell/ShellContext.ts deleted file mode 100644 index 543227b..0000000 --- a/apps/web/src/lib/components/Shell/ShellContext.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { getContext } from "svelte"; - -export type ShellContext = { - drawer: DrawerContext; -}; - -export type DrawerContext = { - open: (content: any, props: any) => void; - close: () => void; -}; - -export function getShellContext(): ShellContext { - return getContext('shell'); -} \ No newline at end of file diff --git a/apps/web/src/lib/components/things/Details/Details.svelte b/apps/web/src/lib/components/things/Details/Details.svelte index 1219ff4..5bed547 100644 --- a/apps/web/src/lib/components/things/Details/Details.svelte +++ b/apps/web/src/lib/components/things/Details/Details.svelte @@ -1,44 +1,48 @@ -
+
{name} -
@@ -49,7 +53,7 @@ {$t('Due Back')} {new Date(availableDate).toLocaleDateString($locale)} {/if} {#if isBookmarked} - {$t('Bookmarked')} + {$t('Bookmarked')} {/if}
@@ -61,7 +65,7 @@ {$t(category)} {/each} {:else} -
None
+
{$t('None')}
{/if}
@@ -70,10 +74,19 @@ {@const availableItems = items.filter((i) => i.status === 'available' && !i.hidden)} {@const unavailableItems = items.filter((i) => i.status === 'checkedOut' || i.hidden)} + {#if !availableItems.length && !unavailableItems.length} + {$t('None')} + {/if} + {#if availableItems.length} {#each availableItems as item} - + onClickItem(item)} + /> {/each} {/if} @@ -81,7 +94,12 @@ {#if unavailableItems.length} {#each unavailableItems as item} - + onClickItem(item)} + /> {/each} {/if} diff --git a/apps/web/src/lib/components/things/Details/DetailsView.svelte b/apps/web/src/lib/components/things/Details/DetailsView.svelte index 708e044..8418ed7 100644 --- a/apps/web/src/lib/components/things/Details/DetailsView.svelte +++ b/apps/web/src/lib/components/things/Details/DetailsView.svelte @@ -4,6 +4,9 @@ import LoadingSpinner from '$lib/components/LoadingSpinner.svelte'; import Details from './Details.svelte'; import { locale } from '$lib/language/translate'; + import type { InventoryItemModel } from '$lib/models/ThingDetails'; + import { ItemDetailsView } from './Item'; + import { push } from '$lib/components/Shell/Drawer'; export let id: string; @@ -11,6 +14,10 @@ $: loading = $details.loading; $: thing = $details.value; $: thingName = $locale === 'en' ? thing?.name : thing?.spanishName ?? thing?.name; + + const onClickItem = (event: CustomEvent) => { + push(ItemDetailsView, { id: event.detail.id }); + }; {#if loading} @@ -27,5 +34,6 @@ available={thing.available} availableDate={thing.availableDate} items={thing.items} + on:clickItem={onClickItem} /> {/if} diff --git a/apps/web/src/lib/components/things/Details/InventoryItem.svelte b/apps/web/src/lib/components/things/Details/InventoryItem.svelte index 0a5da89..25d935f 100644 --- a/apps/web/src/lib/components/things/Details/InventoryItem.svelte +++ b/apps/web/src/lib/components/things/Details/InventoryItem.svelte @@ -6,7 +6,8 @@ export let status: 'available'|'checkedOut'; -
+ +
{#if status === 'available'} {/if} diff --git a/apps/web/src/lib/components/things/Details/Item/ItemDetails.svelte b/apps/web/src/lib/components/things/Details/Item/ItemDetails.svelte new file mode 100644 index 0000000..d362513 --- /dev/null +++ b/apps/web/src/lib/components/things/Details/Item/ItemDetails.svelte @@ -0,0 +1,62 @@ + + +
+ {'name'} +
+
+
+ #{number} + {name} + + {#if available} + {$t('Available')} + {/if} + {#if !available} + {$t('Unavailable')} + {/if} + {#if !available && availableDate} + {$t('Due Back')} {new Date(availableDate).toLocaleDateString($locale)} + {/if} + +
+ +
+ {brand ?? $t('None')} +
+ +
+
+ {#each manuals as url} + + + + {/each} + {#if !manuals.length} + {$t('None')} + {/if} +
+
+
\ No newline at end of file diff --git a/apps/web/src/lib/components/things/Details/Item/ItemDetailsView.svelte b/apps/web/src/lib/components/things/Details/Item/ItemDetailsView.svelte new file mode 100644 index 0000000..41dcdd4 --- /dev/null +++ b/apps/web/src/lib/components/things/Details/Item/ItemDetailsView.svelte @@ -0,0 +1,30 @@ + + +{#if loading} + + + +{:else} + +{/if} diff --git a/apps/web/src/lib/components/things/Details/Item/index.ts b/apps/web/src/lib/components/things/Details/Item/index.ts new file mode 100644 index 0000000..6d37c88 --- /dev/null +++ b/apps/web/src/lib/components/things/Details/Item/index.ts @@ -0,0 +1 @@ +export { default as ItemDetailsView } from "./ItemDetailsView.svelte"; \ No newline at end of file diff --git a/apps/web/src/lib/components/things/ThingCardView.svelte b/apps/web/src/lib/components/things/ThingCardView.svelte index c846a3e..2675839 100644 --- a/apps/web/src/lib/components/things/ThingCardView.svelte +++ b/apps/web/src/lib/components/things/ThingCardView.svelte @@ -4,18 +4,16 @@ import type { Thing } from '$lib/models/Thing'; import ThingCard from './ThingCard.svelte'; import { vibrate } from '$lib/utils/haptics'; - import { getShellContext } from '../Shell/ShellContext'; import Details from './Details'; + import { push } from '$lib/components/Shell/Drawer'; export let thing: Thing; $: bookmarked = $bookmarks.find((t) => t === thing.id) !== undefined; $: thingName = $locale === 'en' ? thing.name : thing.spanishName ?? thing.name; - const { drawer } = getShellContext(); - const openThingDetails = () => { - drawer.open(Details, { id: thing.id }); + push(Details, { id: thing.id }); vibrate(); }; diff --git a/apps/web/src/lib/language/translations.ts b/apps/web/src/lib/language/translations.ts index 9cc4866..ebd7f83 100644 --- a/apps/web/src/lib/language/translations.ts +++ b/apps/web/src/lib/language/translations.ts @@ -1,5 +1,6 @@ export default { en: { + "Attachments": "Attachments", "Button.Home": "Home", "Button.WishList": "Wish List", "Name": "Name", @@ -7,6 +8,8 @@ export default { "Categories": "Categories", "Inventory": "Inventory", "Input.Search": "Search...", + "Brand": "Brand", + "None": "None", "No Results": "No Results", "No Brand": "No Brand", "Donate": "Donate", @@ -49,6 +52,7 @@ export default { "Due Back Disclaimer": "We cannot guarantee that the item will be returned by the expected due date. Please check with the Lead Librarian during open hours." }, es: { + "Attachments": "Archivos adjuntos", "Button.Home": "Inicio", "Button.Donate": "Done", "Button.WishList": "Donaciones", @@ -57,7 +61,9 @@ export default { "Categories": "Categorías", "Inventory": "Inventario", "Input.Search": "Buscar...", + "None": "Nada", "No Results": "No hay resultados", + "Brand": "Marca", "No Brand": "No Marca", "Donate": "Donar", "How to Borrow": "Como pedir prestado", diff --git a/apps/web/src/lib/models/ItemDetails.ts b/apps/web/src/lib/models/ItemDetails.ts new file mode 100644 index 0000000..84fd77e --- /dev/null +++ b/apps/web/src/lib/models/ItemDetails.ts @@ -0,0 +1,14 @@ +export type ItemDetailsModel = { + id: string; + image?: string; + name: string; + spanishName?: string; + number: number; + available: boolean; + availableDate?: string; + brand?: string; + condition?: string; + hidden: boolean; + manuals: string[]; + status: 'available'|'checkedOut'; +}; \ No newline at end of file diff --git a/apps/web/src/lib/stores/items.ts b/apps/web/src/lib/stores/items.ts new file mode 100644 index 0000000..3efbb6b --- /dev/null +++ b/apps/web/src/lib/stores/items.ts @@ -0,0 +1,42 @@ +import type { ItemDetailsModel } from "$lib/models/ItemDetails"; +import { derived, get, readable, writable, type Readable } from "svelte/store"; +import type { Async } from "./types"; + +function createItemsRepository() { + const items = writable(new Map()); + const subscribe = items.subscribe; + const unsubscribe = items.subscribe(_ => {}); + + const details = (id: string): Readable> => { + const itemsValue = get(items); + + if (itemsValue.has(id)) { + return readable({ + loading: false, + value: itemsValue[id] + }); + } + + const result = writable({ + loading: true, + value: undefined + }); + + fetch(`/api/items/${id}`).then((r) => r.json()).then((model) => { + result.update((r) => ({ + loading: false, + value: model + })); + }); + + return derived(result, (r) => r); + }; + + return { + subscribe, + unsubscribe, + details + }; +} + +export const items = createItemsRepository(); \ No newline at end of file diff --git a/apps/web/src/lib/stores/things.ts b/apps/web/src/lib/stores/things.ts index 4a4a526..6967f72 100644 --- a/apps/web/src/lib/stores/things.ts +++ b/apps/web/src/lib/stores/things.ts @@ -1,10 +1,6 @@ import type { ThingDetailsModel } from "$lib/models/ThingDetails"; import { derived, get, readable, writable, type Readable } from "svelte/store"; - -type Async = { - loading: boolean; - value: T; -}; +import type { Async } from "./types"; function createThingsRepository() { const things = writable(new Map()); diff --git a/apps/web/src/lib/stores/types.ts b/apps/web/src/lib/stores/types.ts new file mode 100644 index 0000000..bb9bc51 --- /dev/null +++ b/apps/web/src/lib/stores/types.ts @@ -0,0 +1,4 @@ +export type Async = { + loading: boolean; + value: T; +}; \ No newline at end of file diff --git a/apps/web/src/routes/+layout.svelte b/apps/web/src/routes/+layout.svelte index a41afce..8315988 100644 --- a/apps/web/src/routes/+layout.svelte +++ b/apps/web/src/routes/+layout.svelte @@ -1,6 +1,7 @@