Skip to content

Commit

Permalink
Merge pull request #65 from pvdthings/dev
Browse files Browse the repository at this point in the history
Web: Thing Details - Iteration 1
  • Loading branch information
dillonfagan authored Sep 18, 2024
2 parents 61dbacc + 17c2e5e commit fce7bf5
Show file tree
Hide file tree
Showing 38 changed files with 457 additions and 99 deletions.
11 changes: 11 additions & 0 deletions apps/api/apps/catalog/routes/things.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
const express = require('express');
const router = express.Router();
const { getCatalogData } = require('../services/catalog');
const { getThingDetails } = require('../services/thingDetails');

router.get('/', async (req, res) => {
try {
Expand All @@ -11,4 +12,14 @@ router.get('/', async (req, res) => {
}
});

router.get('/things/:id', async (req, res) => {
const { id } = req.params;
try {
res.send(await getThingDetails(id));
} catch (error) {
console.error(error);
res.status(error.status || 500).send({ errors: [error] });
}
});

module.exports = router;
35 changes: 35 additions & 0 deletions apps/api/apps/catalog/services/thingDetails.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
const { fetchThing } = require("../../../services/things");

async function getThingDetails(id) {
const details = await fetchThing(id);

return {
id: details.id,
name: details.name,
spanishName: details.name_es,
categories: details.categories,
availableDate: details.availableDate,
available: details.available,
stock: details.stock,
image: details.images?.length ? details.images[0].url : undefined,
items: details.items.filter((i) => i.location !== 'Providence Public Library').map(item => ({
id: item.id,
number: item.number,
brand: item.brand,
hidden: item.hidden,
status: mapItemStatus(item)
}))
};
}

function mapItemStatus(item) {
if (item.available) {
return 'available';
}

return 'checkedOut';
}

module.exports = {
getThingDetails
}
2 changes: 1 addition & 1 deletion apps/api/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "pvdthings-api",
"version": "1.19.0",
"version": "1.20.0",
"description": "",
"main": "server.js",
"scripts": {
Expand Down
1 change: 1 addition & 0 deletions apps/api/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ app.all('*', (req, res, next) => {
app.get('/', (_, res) => {
res.send('You have reached the Things API');
});
app.use('/web', things);
app.use('/things', things);
app.use('/lending', lending);
app.use('/auth', auth);
Expand Down
1 change: 1 addition & 0 deletions apps/api/services/things/mapThingDetails.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ function mapThingDetails(record, items = [], linkedThings = []) {
name_es: record.get('name_es'),
stock: Number(record.get('Stock')),
available: Number(record.get('Available')),
availableDate: record.get('Next Due Back')?.[0],
hidden: Boolean(record.get('Hidden')),
categories: record.get('Category') || [],
eyeProtection: Boolean(record.get('Eye Protection')),
Expand Down
12 changes: 10 additions & 2 deletions apps/web/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 5 additions & 2 deletions apps/web/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@library_os/web",
"version": "0.15.7",
"version": "0.16.0",
"private": true,
"scripts": {
"dev": "vite dev",
Expand Down Expand Up @@ -32,5 +32,8 @@
"typescript": "^5.0.0",
"vite": "^5.2.10"
},
"type": "module"
"type": "module",
"dependencies": {
"@phosphor-icons/web": "^2.1.1"
}
}
3 changes: 3 additions & 0 deletions apps/web/src/lib/components/AbsoluteCenter.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<div class="absolute top-1/2 left-1/2 -translate-y-1/2 -translate-x-1/2">
<slot />
</div>
9 changes: 9 additions & 0 deletions apps/web/src/lib/components/Badge/Badge.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<script lang="ts">
import { style, type BadgeType } from "./type";
export let type: BadgeType = 'neutral';
</script>

<div class="badge {style(type)} badge-lg">
<slot />
</div>
2 changes: 2 additions & 0 deletions apps/web/src/lib/components/Badge/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { default as Badge } from './Badge.svelte';
export * from './type';
21 changes: 21 additions & 0 deletions apps/web/src/lib/components/Badge/type.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
export type BadgeType =
'neutral'
| 'primary'
| 'warning'
| 'success'
| 'error';

export function style(type: BadgeType) {
switch (type) {
case 'neutral':
return 'badge-neutral';
case 'primary':
return 'badge-primary';
case 'warning':
return 'badge-warning';
case 'success':
return 'badge-success'
case 'error':
return 'badge-error';
}
}
10 changes: 10 additions & 0 deletions apps/web/src/lib/components/CloseButton.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<script lang="ts">
import CloseIcon from "$lib/icons/x-mark.svg";
let className: string;
export { className as class };
</script>

<button class="btn btn-circle btn-ghost outline-none {className}" on:click>
<img src={CloseIcon} alt="Close" height="24" width="24" />
</button>
1 change: 1 addition & 0 deletions apps/web/src/lib/components/Divider.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<div class="divider" />
1 change: 1 addition & 0 deletions apps/web/src/lib/components/LoadingSpinner.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<span class="loading loading-spinner loading-lg text-indigo-500" />
6 changes: 2 additions & 4 deletions apps/web/src/lib/components/Modal/Modal.svelte
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<script lang="ts">
import CloseIcon from "$lib/icons/x-mark.svg";
import { createEventDispatcher, onMount } from "svelte";
import CloseButton from "../CloseButton.svelte";
export let show: boolean = false;
export let title: string = undefined;
Expand All @@ -21,9 +21,7 @@

<dialog bind:this={dialog} class="modal modal-bottom sm:modal-middle">
<div class="modal-box relative modal-responsive">
<button class="btn btn-circle btn-ghost outline-none absolute right-2 top-2" on:click={closeModal}>
<img src={CloseIcon} alt="Close" height="24" width="24" />
</button>
<CloseButton class="absolute right-2 top-2" on:click={closeModal} />
{#if title}
<h2 class="font-bold font-display text-2xl mb-3">{title}</h2>
{/if}
Expand Down
39 changes: 37 additions & 2 deletions apps/web/src/lib/components/Shell/Shell.svelte
Original file line number Diff line number Diff line change
@@ -1,3 +1,38 @@
<div class="flex flex-col w-screen h-screen overflow-hidden">
<slot />
<script lang="ts">
import { setContext } from "svelte";
let drawerToggle: HTMLLabelElement;
let drawerContent: any;
let drawerContentProps: any;
const drawer = {
open: (content: any, props = {}) => {
drawerContent = content;
drawerContentProps = props;
drawerToggle.click();
},
close: () => {
drawerToggle.click();
drawerContent = null;
drawerContentProps = null;
}
};
setContext('shell', {
drawer
});
</script>

<div class="drawer drawer-end">
<input id="drawer-toggle" type="checkbox" class="drawer-toggle" />
<div class="flex flex-col h-dvh w-screen overflow-hidden drawer-content">
<label bind:this={drawerToggle} for="drawer-toggle" />
<slot />
</div>
<div class="drawer-side z-50">
<label for="drawer-toggle" aria-label="close sidebar" class="drawer-overlay"></label>
<div class="bg-base-200 text-base-content flex flex-col h-dvh w-screen overflow-hidden md:w-80 lg:w-96 relative">
<svelte:component this={drawerContent} {...drawerContentProps} />
</div>
</div>
</div>
14 changes: 14 additions & 0 deletions apps/web/src/lib/components/Shell/ShellContext.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
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');
}
3 changes: 3 additions & 0 deletions apps/web/src/lib/components/Wrap.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<div class="flex flex-wrap gap-2">
<slot />
</div>
17 changes: 17 additions & 0 deletions apps/web/src/lib/components/things/Details/BookmarkButton.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<script lang="ts">
import type { ThingID } from "$lib/models/Thing";
import { bookmarks } from "$lib/stores/bookmarks";
export let id: ThingID;
$: bookmarked = bookmarks.isBookmarked(id);
$: iconVariant = $bookmarked ? 'ph-fill' : 'ph-bold';
const toggle = () => {
bookmarks.addRemove(id);
};
</script>

<button on:click={toggle} class="fixed bottom-4 right-4 btn btn-circle btn-lg btn-primary shadow-lg z-50">
<span class="{iconVariant} ph-bookmark-simple text-white text-4xl" />
</button>
89 changes: 89 additions & 0 deletions apps/web/src/lib/components/things/Details/Details.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
<script lang="ts">
import BoxIcon from '$lib/icons/box.svg';
import CloseButton from '$lib/components/CloseButton.svelte';
import { getShellContext } from '$lib/components/Shell/ShellContext';
import InventoryItem from './InventoryItem.svelte';
import BookmarkButton from './BookmarkButton.svelte';
import { bookmarks } from '$lib/stores/bookmarks';
import { locale, t } from '$lib/language/translate';
import Divider from "$lib/components/Divider.svelte";
import Wrap from "$lib/components/Wrap.svelte";
import Title from "./Title.svelte";
import List from "./List.svelte";
import Section from "./Section.svelte";
import { Badge, type BadgeType } from "$lib/components/Badge";
import type { InventoryItemModel } from '$lib/models/ThingDetails';
export let id: string;
export let name: string;
export let image: string|undefined;
export let stock: number;
export let available: number;
export let availableDate: string|undefined;
export let categories: string[];
export let items: InventoryItemModel[];
const { drawer } = getShellContext();
$: stockBadgeVariant = (available ? 'success' : 'error') as BadgeType;
$: bookmarked = bookmarks.isBookmarked(id);
$: isBookmarked = $bookmarked;
</script>

<BookmarkButton {id} />
<section class="flex-grow-0 flex-shrink-0 h-48 md:h-64 border-b border-base-300 overflow-hidden relative shadow-sm">
<img
src={image ?? BoxIcon}
alt={name}
class="object-center object-contain bg-white h-full w-full"
/>
<CloseButton class="absolute top-4 right-4" on:click={drawer.close} />
</section>
<div class="p-4 flex flex-col flex-grow overflow-y-scroll">
<section>
<Title>{name}</Title>
<Wrap>
<Badge type={stockBadgeVariant}>{available} / {stock}</Badge>
{#if !available && availableDate}
<Badge>{$t('Due Back')} {new Date(availableDate).toLocaleDateString($locale)}</Badge>
{/if}
{#if isBookmarked}
<Badge type='primary'>{$t('Bookmarked')}</Badge>
{/if}
</Wrap>
</section>
<Divider />
<Section title={$t('Categories')}>
<Wrap>
{#if categories.length}
{#each categories as category}
<Badge>{$t(category)}</Badge>
{/each}
{:else}
<div>None</div>
{/if}
</Wrap>
</Section>
<Divider />
<Section title={$t('Inventory')}>
{@const availableItems = items.filter((i) => i.status === 'available' && !i.hidden)}
{@const unavailableItems = items.filter((i) => i.status === 'checkedOut' || i.hidden)}

{#if availableItems.length}
<List title={$t('Available')}>
{#each availableItems as item}
<InventoryItem number={item.number} brand={item.brand} status={item.status} />
{/each}
</List>
{/if}

{#if unavailableItems.length}
<List title={$t('Unavailable')}>
{#each unavailableItems as item}
<InventoryItem number={item.number} brand={item.brand} status={item.status} />
{/each}
</List>
{/if}
</Section>
</div>
Loading

0 comments on commit fce7bf5

Please sign in to comment.