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 React from 'react'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import UserFeed from '@/components/Feed/UserFeed'
 | 
					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 { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
 | 
				
			||||||
import { TextShimmer } from '@/components/ui/text-shimmer'
 | 
					import { TextShimmer } from '@/components/ui/text-shimmer'
 | 
				
			||||||
import { LoginForm } from '@/components/login-form'
 | 
					import { LoginForm } from '@/components/login-form'
 | 
				
			||||||
@ -59,6 +59,34 @@ export default async function HomePage() {
 | 
				
			|||||||
      },
 | 
					      },
 | 
				
			||||||
    })) as PaginatedDocs<Repository>
 | 
					    })) 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 (
 | 
					  return (
 | 
				
			||||||
    <div className="home">
 | 
					    <div className="home">
 | 
				
			||||||
      <div className="relative isolate overflow-hidden py-24 sm:py-32 rounded-md">
 | 
					      <div className="relative isolate overflow-hidden py-24 sm:py-32 rounded-md">
 | 
				
			||||||
@ -136,7 +164,7 @@ export default async function HomePage() {
 | 
				
			|||||||
          </TabsContent>
 | 
					          </TabsContent>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
          <TabsContent value="manage">
 | 
					          <TabsContent value="manage">
 | 
				
			||||||
            <Manage repos={userRepos} />
 | 
					            <Manage repos={userRepos} checkouts={userCheckouts} />
 | 
				
			||||||
          </TabsContent>
 | 
					          </TabsContent>
 | 
				
			||||||
        </Tabs>
 | 
					        </Tabs>
 | 
				
			||||||
      ) : (
 | 
					      ) : (
 | 
				
			||||||
 | 
				
			|||||||
@ -21,6 +21,22 @@ const Checkouts: CollectionConfig = {
 | 
				
			|||||||
      relationTo: 'copies',
 | 
					      relationTo: 'copies',
 | 
				
			||||||
      hasMany: false,
 | 
					      hasMany: false,
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					      name: 'isReturned',
 | 
				
			||||||
 | 
					      type: 'checkbox',
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					      name: 'dateDue',
 | 
				
			||||||
 | 
					      type: 'date',
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					      name: 'ownerVerifiedReturnedDate',
 | 
				
			||||||
 | 
					      type: 'date',
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					      name: 'loanerReturnedDate',
 | 
				
			||||||
 | 
					      type: 'date',
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
      name: 'book',
 | 
					      name: 'book',
 | 
				
			||||||
      type: 'join',
 | 
					      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'
 | 
					'use client'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import { Repository } from '@/payload-types'
 | 
					import { Checkout, Repository } from '@/payload-types'
 | 
				
			||||||
import { PaginatedDocs } from 'payload'
 | 
					import { PaginatedDocs } from 'payload'
 | 
				
			||||||
import RepoList from './RepoList'
 | 
					import RepoList from './RepoList'
 | 
				
			||||||
import HoldRequestNotifications from './HoldRequests'
 | 
					import HoldRequestNotifications from './HoldRequests'
 | 
				
			||||||
 | 
					import CheckedOutBooks from './CheckedOutBooks'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type Props = {
 | 
					type Props = {
 | 
				
			||||||
  repos: PaginatedDocs<Repository> | null
 | 
					  repos: PaginatedDocs<Repository> | null
 | 
				
			||||||
 | 
					  checkouts: PaginatedDocs<Checkout> | null
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
const Manage = (props: Props) => {
 | 
					const Manage = (props: Props) => {
 | 
				
			||||||
  const { repos } = props
 | 
					  const { repos, checkouts } = props
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <section>
 | 
					    <section>
 | 
				
			||||||
@ -20,6 +22,10 @@ const Manage = (props: Props) => {
 | 
				
			|||||||
      <div>
 | 
					      <div>
 | 
				
			||||||
        <HoldRequestNotifications repos={repos} />
 | 
					        <HoldRequestNotifications repos={repos} />
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <div>
 | 
				
			||||||
 | 
					        <CheckedOutBooks initialCheckoutPage={checkouts} />
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
    </section>
 | 
					    </section>
 | 
				
			||||||
  )
 | 
					  )
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -56,7 +56,6 @@ export default function SiteNavigation(props: { children: React.ReactNode }) {
 | 
				
			|||||||
    }).then(async (response) => {
 | 
					    }).then(async (response) => {
 | 
				
			||||||
      const userRequest = await response.json()
 | 
					      const userRequest = await response.json()
 | 
				
			||||||
      setUser(userRequest.user)
 | 
					      setUser(userRequest.user)
 | 
				
			||||||
      console.log(userRequest.user)
 | 
					 | 
				
			||||||
    })
 | 
					    })
 | 
				
			||||||
  }, [user, setUser])
 | 
					  }, [user, setUser])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -347,6 +347,10 @@ export interface Checkout {
 | 
				
			|||||||
  fromHold?: (number | null) | HoldRequest;
 | 
					  fromHold?: (number | null) | HoldRequest;
 | 
				
			||||||
  user?: (number | null) | User;
 | 
					  user?: (number | null) | User;
 | 
				
			||||||
  copy?: (number | null) | Copy;
 | 
					  copy?: (number | null) | Copy;
 | 
				
			||||||
 | 
					  isReturned?: boolean | null;
 | 
				
			||||||
 | 
					  dateDue?: string | null;
 | 
				
			||||||
 | 
					  ownerVerifiedReturnedDate?: string | null;
 | 
				
			||||||
 | 
					  loanerReturnedDate?: string | null;
 | 
				
			||||||
  book?: {
 | 
					  book?: {
 | 
				
			||||||
    docs?: (number | Copy)[];
 | 
					    docs?: (number | Copy)[];
 | 
				
			||||||
    hasNextPage?: boolean;
 | 
					    hasNextPage?: boolean;
 | 
				
			||||||
@ -591,6 +595,10 @@ export interface CheckoutsSelect<T extends boolean = true> {
 | 
				
			|||||||
  fromHold?: T;
 | 
					  fromHold?: T;
 | 
				
			||||||
  user?: T;
 | 
					  user?: T;
 | 
				
			||||||
  copy?: T;
 | 
					  copy?: T;
 | 
				
			||||||
 | 
					  isReturned?: T;
 | 
				
			||||||
 | 
					  dateDue?: T;
 | 
				
			||||||
 | 
					  ownerVerifiedReturnedDate?: T;
 | 
				
			||||||
 | 
					  loanerReturnedDate?: T;
 | 
				
			||||||
  book?: T;
 | 
					  book?: T;
 | 
				
			||||||
  updatedAt?: T;
 | 
					  updatedAt?: T;
 | 
				
			||||||
  createdAt?: T;
 | 
					  createdAt?: T;
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user