Compare commits
2 Commits
c5e07e7c82
...
b245e6d8c1
| Author | SHA1 | Date | |
|---|---|---|---|
| b245e6d8c1 | |||
| ce33770e65 |
@ -4,7 +4,7 @@ import config from '@/payload.config'
|
||||
import React from 'react'
|
||||
|
||||
import UserFeed from '@/components/Feed/UserFeed'
|
||||
import { Book, Repository } from '@/payload-types'
|
||||
import { Book, Checkout, Repository } from '@/payload-types'
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
||||
import { TextShimmer } from '@/components/ui/text-shimmer'
|
||||
import { LoginForm } from '@/components/login-form'
|
||||
@ -59,6 +59,34 @@ export default async function HomePage() {
|
||||
},
|
||||
})) as PaginatedDocs<Repository>
|
||||
|
||||
let userCheckouts: PaginatedDocs<Checkout> | null = null
|
||||
if (user?.id)
|
||||
userCheckouts = await payload.find({
|
||||
collection: 'checkouts',
|
||||
depth: 3,
|
||||
limit: 10,
|
||||
select: {
|
||||
id: true,
|
||||
copy: true,
|
||||
dateDue: true,
|
||||
},
|
||||
sort: 'dateDue',
|
||||
where: {
|
||||
and: [
|
||||
{
|
||||
isReturned: {
|
||||
not_equals: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
'user.id': {
|
||||
equals: user.id,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
|
||||
return (
|
||||
<div className="home">
|
||||
<div className="relative isolate overflow-hidden py-24 sm:py-32 rounded-md">
|
||||
@ -136,7 +164,7 @@ export default async function HomePage() {
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="manage">
|
||||
<Manage repos={userRepos} />
|
||||
<Manage repos={userRepos} checkouts={userCheckouts} />
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
) : (
|
||||
|
||||
@ -21,6 +21,22 @@ const Checkouts: CollectionConfig = {
|
||||
relationTo: 'copies',
|
||||
hasMany: false,
|
||||
},
|
||||
{
|
||||
name: 'isReturned',
|
||||
type: 'checkbox',
|
||||
},
|
||||
{
|
||||
name: 'dateDue',
|
||||
type: 'date',
|
||||
},
|
||||
{
|
||||
name: 'ownerVerifiedReturnedDate',
|
||||
type: 'date',
|
||||
},
|
||||
{
|
||||
name: 'loanerReturnedDate',
|
||||
type: 'date',
|
||||
},
|
||||
{
|
||||
name: 'book',
|
||||
type: 'join',
|
||||
|
||||
174
src/components/Manage/CheckedOutBooks.tsx
Normal file
174
src/components/Manage/CheckedOutBooks.tsx
Normal file
@ -0,0 +1,174 @@
|
||||
import { Author, Book, Checkout, Copy, Repository } from '@/payload-types'
|
||||
import { Menu, MenuButton, MenuItem, MenuItems } from '@headlessui/react'
|
||||
import { EllipsisVerticalIcon } from '@heroicons/react/20/solid'
|
||||
import clsx from 'clsx'
|
||||
import { Loader2Icon } from 'lucide-react'
|
||||
import { PaginatedDocs, User } from 'payload'
|
||||
import { useCallback, useState } from 'react'
|
||||
|
||||
const statuses = {
|
||||
'Passed Due': 'text-gray-200 bg-red-500 ring-red-700/10',
|
||||
'Due Soon': 'text-amber-800 bg-amber-50 ring-amber-600/20',
|
||||
'': '',
|
||||
}
|
||||
|
||||
type Row = {
|
||||
id: number
|
||||
title: string
|
||||
authors: string[]
|
||||
owners: string[]
|
||||
repositoryAbbreviation: string
|
||||
status: 'Passed Due' | 'Due Soon' | ''
|
||||
dueDate: Date
|
||||
href: string
|
||||
}
|
||||
|
||||
type ListProps = { rows: Row[] }
|
||||
const CheckedOutBooksList = (props: ListProps) => {
|
||||
const { rows } = props
|
||||
|
||||
const [returningBookId, setReturningBookId] = useState(0)
|
||||
|
||||
const isReturningBook = useCallback((id: number) => id === returningBookId, [returningBookId])
|
||||
|
||||
const handleReturnClick = useCallback(
|
||||
(id: number) => {
|
||||
setReturningBookId(id)
|
||||
},
|
||||
[returningBookId],
|
||||
)
|
||||
|
||||
return (
|
||||
<ul role="list" className="divide-y divide-gray-100">
|
||||
{rows.map((r) => (
|
||||
<li key={r.id} className="flex items-center justify-between gap-x-6 py-5">
|
||||
<div className="min-w-0">
|
||||
<div className="flex items-start gap-x-3">
|
||||
<p className="text-sm/6 text-foreground">
|
||||
<span className="font-semibold">{`[${r.repositoryAbbreviation}] `}</span>
|
||||
<span>{r.title}</span>
|
||||
</p>
|
||||
{!!r.status && (
|
||||
<p
|
||||
className={clsx(
|
||||
statuses[r.status as keyof typeof statuses],
|
||||
'mt-0.5 rounded-md px-1.5 py-0.5 text-xs font-medium whitespace-nowrap ring-1 ring-inset',
|
||||
)}
|
||||
>
|
||||
{r.status}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="mt-1 flex items-center gap-x-2 text-xs/5 text-gray-500">
|
||||
<p className="whitespace-nowrap">
|
||||
Due on <time dateTime={r.dueDate.toString()}>{r.dueDate.toLocaleDateString()}</time>
|
||||
</p>
|
||||
<svg viewBox="0 0 2 2" className="size-0.5 fill-current">
|
||||
<circle r={1} cx={1} cy={1} />
|
||||
</svg>
|
||||
<p className="truncate">Borrowed from {r.owners.join(', ')}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-none items-center gap-x-4">
|
||||
<button
|
||||
type="button"
|
||||
disabled={!!returningBookId}
|
||||
className="hidden disabled:opacity-20 cursor-pointer disabled:cursor-auto rounded-md bg-background px-2.5 py-1.5 text-sm font-semibold text-muted-foreground shadow-xs ring-1 ring-gray-300 ring-inset hover:bg-gray-50 sm:flex items-center gap-1"
|
||||
onClick={() => handleReturnClick(r.id)}
|
||||
>
|
||||
{isReturningBook(r.id) && <Loader2Icon className="animate-spin" />}
|
||||
<span>Return</span>
|
||||
</button>
|
||||
<Menu as="div" className="relative flex-none">
|
||||
<MenuButton className="-m-2.5 block p-2.5 text-gray-500 hover:text-gray-900">
|
||||
<span className="sr-only">Open options</span>
|
||||
<EllipsisVerticalIcon aria-hidden="true" className="size-5" />
|
||||
</MenuButton>
|
||||
<MenuItems
|
||||
transition
|
||||
className="absolute right-0 z-10 mt-2 w-32 origin-top-right rounded-md bg-white py-2 shadow-lg ring-1 ring-gray-900/5 transition focus:outline-hidden data-closed:scale-95 data-closed:transform data-closed:opacity-0 data-enter:duration-100 data-enter:ease-out data-leave:duration-75 data-leave:ease-in"
|
||||
>
|
||||
<MenuItem>
|
||||
<button
|
||||
className="block cursor-pointer px-3 py-1 text-sm/6 text-gray-900 data-focus:bg-gray-50 data-focus:outline-hidden"
|
||||
type="button"
|
||||
onClick={() => console.log('return')}
|
||||
>
|
||||
Return<span className="sr-only">, {r.title}</span>
|
||||
</button>
|
||||
</MenuItem>
|
||||
<MenuItem>
|
||||
<a
|
||||
href={`/repositories/${r.repositoryAbbreviation}`}
|
||||
className="block px-3 py-1 text-sm/6 text-gray-900 data-focus:bg-gray-50 data-focus:outline-hidden"
|
||||
>
|
||||
Repository<span className="sr-only">, {r.repositoryAbbreviation}</span>
|
||||
</a>
|
||||
</MenuItem>
|
||||
<MenuItem>
|
||||
<a
|
||||
href={r.href}
|
||||
className="block px-3 py-1 text-sm/6 text-gray-900 data-focus:bg-gray-50 data-focus:outline-hidden"
|
||||
>
|
||||
View<span className="sr-only"> {r.title}</span>
|
||||
</a>
|
||||
</MenuItem>
|
||||
</MenuItems>
|
||||
</Menu>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)
|
||||
}
|
||||
|
||||
type Props = { initialCheckoutPage: PaginatedDocs<Checkout> | null }
|
||||
const CheckedOutBooks = (props: Props) => {
|
||||
const { initialCheckoutPage } = props
|
||||
|
||||
const checkouts = initialCheckoutPage?.docs
|
||||
const rows: Row[] =
|
||||
checkouts?.map((c) => {
|
||||
const copy = c.copy as Copy
|
||||
const book = copy.book as Book
|
||||
const authors = book.authors as Author[]
|
||||
const repository = copy.repository as Repository
|
||||
const owners = repository.owner as (number | User)[]
|
||||
|
||||
const parsedDateDue = c.dateDue ? new Date(c.dateDue) : new Date()
|
||||
const milisecondsUntilDue = parsedDateDue.getTime() - new Date().getTime()
|
||||
const secondsUntilDue = Math.floor(milisecondsUntilDue / 1000)
|
||||
const minutesUntilDue = Math.floor(secondsUntilDue / 60)
|
||||
const hoursUntilDue = Math.floor(minutesUntilDue / 60)
|
||||
const daysUntilDue = Math.floor(hoursUntilDue / 24)
|
||||
|
||||
let status = ''
|
||||
if (daysUntilDue <= 0) status = 'Passed Due'
|
||||
else if (daysUntilDue <= 5) status = 'Due Soon'
|
||||
|
||||
return {
|
||||
id: c.id || 0,
|
||||
title: book.title || '',
|
||||
authors: authors.map((a) => a.lf) || ([] as string[]),
|
||||
owners:
|
||||
owners.map((o) => `${(o as User).firstName} ${(o as User).lastName}`) || ([] as string[]),
|
||||
repositoryAbbreviation: repository.abbreviation || '',
|
||||
status,
|
||||
dueDate: parsedDateDue,
|
||||
href: `/books/${book.id}`,
|
||||
} as Row
|
||||
}) || []
|
||||
|
||||
if (!rows.length) return null
|
||||
|
||||
return (
|
||||
<section className="py-6">
|
||||
<div className="mb-4 flex justify-between items-end">
|
||||
<h3 className="px-4 text-lg font-semibold">Inbound Checked Out Books</h3>
|
||||
</div>
|
||||
<CheckedOutBooksList rows={rows} />
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
export default CheckedOutBooks
|
||||
@ -1,15 +1,17 @@
|
||||
'use client'
|
||||
|
||||
import { Repository } from '@/payload-types'
|
||||
import { Checkout, Repository } from '@/payload-types'
|
||||
import { PaginatedDocs } from 'payload'
|
||||
import RepoList from './RepoList'
|
||||
import HoldRequestNotifications from './HoldRequests'
|
||||
import CheckedOutBooks from './CheckedOutBooks'
|
||||
|
||||
type Props = {
|
||||
repos: PaginatedDocs<Repository> | null
|
||||
checkouts: PaginatedDocs<Checkout> | null
|
||||
}
|
||||
const Manage = (props: Props) => {
|
||||
const { repos } = props
|
||||
const { repos, checkouts } = props
|
||||
|
||||
return (
|
||||
<section>
|
||||
@ -20,6 +22,10 @@ const Manage = (props: Props) => {
|
||||
<div>
|
||||
<HoldRequestNotifications repos={repos} />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<CheckedOutBooks initialCheckoutPage={checkouts} />
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
@ -56,7 +56,6 @@ export default function SiteNavigation(props: { children: React.ReactNode }) {
|
||||
}).then(async (response) => {
|
||||
const userRequest = await response.json()
|
||||
setUser(userRequest.user)
|
||||
console.log(userRequest.user)
|
||||
})
|
||||
}, [user, setUser])
|
||||
|
||||
|
||||
@ -347,6 +347,10 @@ export interface Checkout {
|
||||
fromHold?: (number | null) | HoldRequest;
|
||||
user?: (number | null) | User;
|
||||
copy?: (number | null) | Copy;
|
||||
isReturned?: boolean | null;
|
||||
dateDue?: string | null;
|
||||
ownerVerifiedReturnedDate?: string | null;
|
||||
loanerReturnedDate?: string | null;
|
||||
book?: {
|
||||
docs?: (number | Copy)[];
|
||||
hasNextPage?: boolean;
|
||||
@ -591,6 +595,10 @@ export interface CheckoutsSelect<T extends boolean = true> {
|
||||
fromHold?: T;
|
||||
user?: T;
|
||||
copy?: T;
|
||||
isReturned?: T;
|
||||
dateDue?: T;
|
||||
ownerVerifiedReturnedDate?: T;
|
||||
loanerReturnedDate?: T;
|
||||
book?: T;
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user