Compare commits

..

10 Commits

Author SHA1 Message Date
b16a677e65 feat: added checkbox to open copy to public 2025-05-16 09:52:19 -05:00
1d817b762b feat: owner return book 2025-05-14 10:27:59 -05:00
4ca5e0a299 feat: loading and error feed back on login form 2025-05-12 11:00:32 -05:00
a2bc9d0d5f feat: manage page 2025-05-05 10:46:51 -05:00
2b4ed75d72 fix: access to copies for hold 2025-05-05 10:27:57 -05:00
e264e88b39 fix: reset password and env vars 2025-05-04 20:17:29 -05:00
db20a093ef feat: update container config for prod 2025-05-04 15:44:15 -05:00
3662189539 feat: borrower return book
also change some var names and headings to reflect when data is in
borrower role and when data is in lender
2025-05-04 12:44:14 -05:00
5c328ab0a4 fix: outbound hold count 2025-05-04 11:14:14 -05:00
014e3eaa7c feat: header links driven by payload 2025-05-04 11:02:46 -05:00
29 changed files with 796 additions and 178 deletions

View File

@ -1,10 +1,15 @@
DATABASE_URI=postgres://postgres:<password>@127.0.0.1:5432/your-database-name DATABASE_URI=postgres://postgres:<password>@127.0.0.1:5432/your-database-name
PAYLOAD_SECRET=YOUR_SECRET_HERE PAYLOAD_SECRET=YOUR_SECRET_HERE
DOMIAN_NAME= PORT=9889
DOMAIN_NAME=localhost:3000
SERVER_URL=http://localhost:3000
SMTP_HOST= SMTP_HOST=
SMTP_USER= SMTP_USER=
SMTP_PASS= SMTP_PASS=
SMTP_PORT=587 SMTP_PORT=587
PASSWORD_RESET_EXPIRATION_IN_MINUTES= PASSWORD_RESET_EXPIRATION_IN_MINUTES=
ENV NEXT_TELEMETRY_DISABLED 1

View File

@ -28,7 +28,7 @@ COPY . .
# Next.js collects completely anonymous telemetry data about general usage. # Next.js collects completely anonymous telemetry data about general usage.
# Learn more here: https://nextjs.org/telemetry # Learn more here: https://nextjs.org/telemetry
# Uncomment the following line in case you want to disable telemetry during the build. # Uncomment the following line in case you want to disable telemetry during the build.
# ENV NEXT_TELEMETRY_DISABLED 1 ENV NEXT_TELEMETRY_DISABLED 1
RUN \ RUN \
if [ -f yarn.lock ]; then yarn run build; \ if [ -f yarn.lock ]; then yarn run build; \

View File

@ -1,43 +1,16 @@
version: '3'
services: services:
payload: payload:
container_name: "midrashim"
image: node:18-alpine image: node:18-alpine
ports: ports:
- '3000:3000' - '${PORT}:${PORT}'
volumes: volumes:
- .:/home/node/app - .:/home/node/app
- node_modules:/home/node/app/node_modules - node_modules:/home/node/app/node_modules
working_dir: /home/node/app/ working_dir: /home/node/app/
command: sh -c "corepack enable && corepack prepare pnpm@latest --activate && pnpm install && pnpm dev" command: sh -c "npm install && npm run build && npm start"
depends_on:
- mongo
# - postgres
env_file: env_file:
- .env - .env
# Ensure your DATABASE_URI uses 'mongo' as the hostname ie. mongodb://mongo/my-db-name
mongo:
image: mongo:latest
ports:
- '27017:27017'
command:
- --storageEngine=wiredTiger
volumes:
- data:/data/db
logging:
driver: none
# Uncomment the following to use postgres
# postgres:
# restart: always
# image: postgres:latest
# volumes:
# - pgdata:/var/lib/postgresql/data
# ports:
# - "5432:5432"
volumes: volumes:
data: data:
# pgdata:
node_modules: node_modules:

View File

@ -11,6 +11,10 @@ const nextConfig = {
source: '/search', source: '/search',
destination: '/books', destination: '/books',
}, },
{
source: '/profile',
destination: '/manage',
},
] ]
}, },
} }

13
package-lock.json generated
View File

@ -12,6 +12,7 @@
"@headlessui/react": "^2.2.1", "@headlessui/react": "^2.2.1",
"@heroicons/react": "^2.2.0", "@heroicons/react": "^2.2.0",
"@hookform/resolvers": "^5.0.1", "@hookform/resolvers": "^5.0.1",
"@next/env": "^15.3.1",
"@payloadcms/db-postgres": "3.31.0", "@payloadcms/db-postgres": "3.31.0",
"@payloadcms/next": "3.31.0", "@payloadcms/next": "3.31.0",
"@payloadcms/payload-cloud": "3.31.0", "@payloadcms/payload-cloud": "3.31.0",
@ -3337,9 +3338,9 @@
} }
}, },
"node_modules/@next/env": { "node_modules/@next/env": {
"version": "15.2.3", "version": "15.3.1",
"resolved": "https://registry.npmjs.org/@next/env/-/env-15.2.3.tgz", "resolved": "https://registry.npmjs.org/@next/env/-/env-15.3.1.tgz",
"integrity": "sha512-a26KnbW9DFEUsSxAxKBORR/uD9THoYoKbkpFywMN/AFvboTt94b8+g/07T8J6ACsdLag8/PDU60ov4rPxRAixw==", "integrity": "sha512-cwK27QdzrMblHSn9DZRV+DQscHXRuJv6MydlJRpFSqJWZrTYMLzKDeyueJNN9MGd8NNiUKzDQADAf+dMLXX7YQ==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/@next/eslint-plugin-next": { "node_modules/@next/eslint-plugin-next": {
@ -12069,6 +12070,12 @@
"react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc" "react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc"
} }
}, },
"node_modules/next/node_modules/@next/env": {
"version": "15.2.3",
"resolved": "https://registry.npmjs.org/@next/env/-/env-15.2.3.tgz",
"integrity": "sha512-a26KnbW9DFEUsSxAxKBORR/uD9THoYoKbkpFywMN/AFvboTt94b8+g/07T8J6ACsdLag8/PDU60ov4rPxRAixw==",
"license": "MIT"
},
"node_modules/next/node_modules/postcss": { "node_modules/next/node_modules/postcss": {
"version": "8.4.31", "version": "8.4.31",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz",

View File

@ -19,6 +19,7 @@
"@headlessui/react": "^2.2.1", "@headlessui/react": "^2.2.1",
"@heroicons/react": "^2.2.0", "@heroicons/react": "^2.2.0",
"@hookform/resolvers": "^5.0.1", "@hookform/resolvers": "^5.0.1",
"@next/env": "^15.3.1",
"@payloadcms/db-postgres": "3.31.0", "@payloadcms/db-postgres": "3.31.0",
"@payloadcms/next": "3.31.0", "@payloadcms/next": "3.31.0",
"@payloadcms/payload-cloud": "3.31.0", "@payloadcms/payload-cloud": "3.31.0",

View File

@ -1,5 +1,8 @@
import SiteNavigation from '@/components/SiteNavigation' import SiteNavigation from '@/components/SiteNavigation'
import React from 'react' import React from 'react'
import { getPayload } from 'payload'
import config from '@/payload.config'
import { Header } from '@/payload-types'
export const metadata = { export const metadata = {
description: 'House of Study for Temple Beth El', description: 'House of Study for Temple Beth El',
@ -9,5 +12,19 @@ export const metadata = {
export default async function RootLayout(props: { children: React.ReactNode }) { export default async function RootLayout(props: { children: React.ReactNode }) {
const { children } = props const { children } = props
return <SiteNavigation>{children}</SiteNavigation> const payloadConfig = await config
const payload = await getPayload({ config: payloadConfig })
const headerGlobals = (await payload.findGlobal({
slug: 'header',
})) as Header
const navItems =
headerGlobals.headerLinks?.map((l) => ({
label: l.label || '',
href: l.href || '',
newTab: l.newTab || false,
})) || []
return <SiteNavigation navItems={navItems}>{children}</SiteNavigation>
} }

View File

@ -0,0 +1,18 @@
'use client'
import Manage from '@/components/Manage/Manage'
import { Checkout, Repository, User } from '@/payload-types'
import { PaginatedDocs } from 'payload'
type Props = {
repos: PaginatedDocs<Repository> | null
borrows: PaginatedDocs<Checkout> | null
loans: PaginatedDocs<Checkout> | null
user: User | null
}
const ManagePageClient = (props: Props) => {
const { repos, borrows, loans, user } = props
return <Manage repos={repos} borrows={borrows} user={user} loans={loans} />
}
export default ManagePageClient

View File

@ -0,0 +1,119 @@
import { headers as getHeaders } from 'next/headers.js'
import { getPayload, PaginatedDocs } from 'payload'
import { Checkout, Repository } from '@/payload-types'
import config from '@/payload.config'
import ManagePageClient from './page.client'
import { PageBreadCrumb, Route } from '@/components/PageBreadCrumb'
const breadcrumRoutes: Route[] = [
{
label: 'Home',
href: '/',
},
{
label: 'Manage',
href: '/manage',
},
]
const ManagePage = async () => {
const headers = await getHeaders()
const payloadConfig = await config
const payload = await getPayload({ config: payloadConfig })
const { user } = await payload.auth({ headers })
let userRepos: PaginatedDocs<Repository> | null = null
if (user?.id)
userRepos = (await payload.find({
collection: 'repositories',
depth: 3,
limit: 10,
select: {
name: true,
abbreviation: true,
image: true,
description: true,
dateOpenToPublic: true,
holdRequests: true,
},
where: {
'owner.id': {
equals: user.id,
},
},
joins: {
holdRequests: {
limit: 100,
},
},
})) as PaginatedDocs<Repository>
let userBorrows: PaginatedDocs<Checkout> | null = null
if (user?.id)
userBorrows = await payload.find({
collection: 'checkouts',
depth: 3,
limit: 10,
select: {
id: true,
copy: true,
dateDue: true,
ownerVerifiedReturnedDate: true,
loaneeReturnedDate: true,
},
sort: 'dateDue',
where: {
and: [
{
isReturned: {
not_equals: true,
},
},
{
'user.id': {
equals: user.id,
},
},
],
},
})
let userLoans: PaginatedDocs<Checkout> | null = null
if (user?.id)
userLoans = await payload.find({
collection: 'checkouts',
depth: 2,
limit: 10,
select: {
id: true,
copy: true,
dateDue: true,
ownerVerifiedReturnedDate: true,
loaneeReturnedDate: true,
},
sort: 'dateDue',
where: {
and: [
{
isReturned: {
not_equals: true,
},
},
{
'copy.repository.owner.id': {
equals: user.id,
},
},
],
},
})
return (
<>
<PageBreadCrumb routes={breadcrumRoutes} />
<ManagePageClient repos={userRepos} borrows={userBorrows} user={user} loans={userLoans} />
</>
)
}
export default ManagePage

View File

@ -59,9 +59,9 @@ export default async function HomePage() {
}, },
})) as PaginatedDocs<Repository> })) as PaginatedDocs<Repository>
let userCheckouts: PaginatedDocs<Checkout> | null = null let userBorrows: PaginatedDocs<Checkout> | null = null
if (user?.id) if (user?.id)
userCheckouts = await payload.find({ userBorrows = await payload.find({
collection: 'checkouts', collection: 'checkouts',
depth: 3, depth: 3,
limit: 10, limit: 10,
@ -69,6 +69,8 @@ export default async function HomePage() {
id: true, id: true,
copy: true, copy: true,
dateDue: true, dateDue: true,
ownerVerifiedReturnedDate: true,
loaneeReturnedDate: true,
}, },
sort: 'dateDue', sort: 'dateDue',
where: { where: {
@ -87,6 +89,36 @@ export default async function HomePage() {
}, },
}) })
let userLoans: PaginatedDocs<Checkout> | null = null
if (user?.id)
userLoans = await payload.find({
collection: 'checkouts',
depth: 2,
limit: 10,
select: {
id: true,
copy: true,
dateDue: true,
ownerVerifiedReturnedDate: true,
loaneeReturnedDate: true,
},
sort: 'dateDue',
where: {
and: [
{
isReturned: {
not_equals: true,
},
},
{
'copy.repository.owner.id': {
equals: user.id,
},
},
],
},
})
return ( return (
<div className="home"> <div className="home">
<HomeHero user={user} /> <HomeHero user={user} />
@ -108,7 +140,7 @@ export default async function HomePage() {
</TabsContent> </TabsContent>
<TabsContent value="manage"> <TabsContent value="manage">
<Manage repos={userRepos} user={user} checkouts={userCheckouts} /> <Manage repos={userRepos} user={user} borrows={userBorrows} loans={userLoans} />
</TabsContent> </TabsContent>
</Tabs> </Tabs>
) : ( ) : (

View File

@ -42,7 +42,7 @@ const Checkouts: CollectionConfig = {
type: 'date', type: 'date',
}, },
{ {
name: 'loanerReturnedDate', name: 'loaneeReturnedDate',
type: 'date', type: 'date',
}, },
{ {

View File

@ -1,3 +1,4 @@
import { authenticated } from "@/access/authenticated";
import { Copy } from "@/payload-types"; import { Copy } from "@/payload-types";
import { CollectionBeforeValidateHook, CollectionConfig } from "payload"; import { CollectionBeforeValidateHook, CollectionConfig } from "payload";
@ -38,12 +39,22 @@ export const Copies: CollectionConfig = {
limits: [10, 20], limits: [10, 20],
}, },
}, },
access: {
read: () => true,
update: authenticated,
create: authenticated,
delete: authenticated,
},
fields: [ fields: [
{ {
name: 'label', name: 'label',
type: 'text', type: 'text',
}, },
{
name: 'isOpenToPublic',
type: 'checkbox',
},
{ {
name: 'condition', name: 'condition',
label: 'Condition out of 5', label: 'Condition out of 5',

View File

@ -1,8 +1,11 @@
import { admin } from '@/access/admin' import { admin } from '@/access/admin'
import type { CollectionConfig } from 'payload' import type { CollectionConfig } from 'payload'
const expirationInMinutes = parseInt(process.env.PASSWORD_RESET_EXPIRATION_IN_MINUTES || '30') import '../envConfig'
const domain = process.env.DOMAIN_NAME || 'localhost:3000'
const expirationInMinutes = parseInt(process.env.PASSWORD_RESET_EXPIRATION_IN_MINUTES || '30', 10)
const domain = process.env.DOMAIN_NAME || ''
const serverUrl = process.env.SERVER_URL || ''
export const Users: CollectionConfig = { export const Users: CollectionConfig = {
slug: 'users', slug: 'users',
@ -13,31 +16,13 @@ export const Users: CollectionConfig = {
admin: admin admin: admin
}, },
auth: { auth: {
// verify: {
// generateEmailSubject: () => {
// return `Verify Account for ${domain}`
// },
// generateEmailHTML: ({ req, token, user }) => {
// const url = `https://${domain}/verify?token=${token}`
// return `
// <!doctype html>
// <html>
// <body>
// <h1>Verify Account for ${domain}</h1>
// <p>Hey ${user.email}, verify your email by clicking here: ${url}</p>
// <p>If you have not recently been signed up for ${domain} then please ignore this email.</p>
// </body>
// </html>
// `
// },
// },
forgotPassword: { forgotPassword: {
expiration: (60000 * expirationInMinutes),
generateEmailSubject: () => { generateEmailSubject: () => {
console.log({ domain, serverUrl, expirationInMinutes })
return `Reset password request for ${domain}` return `Reset password request for ${domain}`
}, },
generateEmailHTML: (props) => { generateEmailHTML: (props) => {
const resetPasswordURL = `https://${domain}/forgotPassword?token=${props?.token}` const resetPasswordURL = `${serverUrl}/forgotPassword?token=${props?.token}`
return ` return `
<!doctype html> <!doctype html>
@ -58,7 +43,6 @@ export const Users: CollectionConfig = {
}, },
fields: [ fields: [
// Email added by default // Email added by default
// Add more fields as needed
{ {
name: 'role', name: 'role',
type: 'select', type: 'select',

View File

@ -25,7 +25,7 @@ const UserFeed = async (props: Props) => {
const payloadConfig = await config const payloadConfig = await config
const payload = await getPayload({ config: payloadConfig }) const payload = await getPayload({ config: payloadConfig })
const holdRequests = (await payload.find({ const borrowRequests = (await payload.find({
collection: 'holdRequests', collection: 'holdRequests',
limit: 10, limit: 10,
depth: 3, depth: 3,
@ -97,7 +97,13 @@ const UserFeed = async (props: Props) => {
}, },
})) as PaginatedDocs<Checkout> })) as PaginatedDocs<Checkout>
const totalHoldNotifications = repos?.docs.flatMap((r) => r.holdRequests?.docs).length || 0 const totalHoldNotifications =
repos?.docs
.flatMap((r) => r.holdRequests?.docs)
.filter((r) => {
const repo = r as HoldRequest
return !repo.isRejected && !repo.isCheckedOut
}).length || 0
const outBoundLoanCount = loanedOutBooks?.totalDocs || 0 const outBoundLoanCount = loanedOutBooks?.totalDocs || 0
const currentlyHoldingCount = currentlyHeldBooks?.totalDocs || 0 const currentlyHoldingCount = currentlyHeldBooks?.totalDocs || 0
@ -106,7 +112,7 @@ const UserFeed = async (props: Props) => {
name: 'Hold Request', name: 'Hold Request',
iconSrc: '/images/mail.svg', iconSrc: '/images/mail.svg',
value: totalHoldNotifications, value: totalHoldNotifications,
href: '#', href: '/manage',
ctaText: 'Answer Requests', ctaText: 'Answer Requests',
shouldAnimate: totalHoldNotifications > 0, shouldAnimate: totalHoldNotifications > 0,
}, },
@ -114,7 +120,7 @@ const UserFeed = async (props: Props) => {
name: 'Loaned Out', name: 'Loaned Out',
iconSrc: '/images/book-loan.svg', iconSrc: '/images/book-loan.svg',
value: outBoundLoanCount, value: outBoundLoanCount,
href: '#', href: '/manage',
ctaText: 'Handle Returns', ctaText: 'Handle Returns',
shouldAnimate: false, shouldAnimate: false,
}, },
@ -122,7 +128,7 @@ const UserFeed = async (props: Props) => {
name: 'Currently Holding', name: 'Currently Holding',
iconSrc: '/images/book-shelf.svg', iconSrc: '/images/book-shelf.svg',
value: currentlyHoldingCount, value: currentlyHoldingCount,
href: '#', href: '/manage',
ctaText: 'Loan Out', ctaText: 'Loan Out',
shouldAnimate: false, shouldAnimate: false,
}, },
@ -131,7 +137,7 @@ const UserFeed = async (props: Props) => {
return ( return (
<section> <section>
<div className="my-6"> <div className="my-6">
<h3 className="text-lg font-semibold text-foreground">Outbound Activity</h3> <h3 className="text-lg font-semibold text-foreground">Loan Activity</h3>
<dl className="mt-5 grid grid-cols-1 gap-5 sm:grid-cols-3"> <dl className="mt-5 grid grid-cols-1 gap-5 sm:grid-cols-3">
{stats.map((s) => { {stats.map((s) => {
return ( return (
@ -176,11 +182,11 @@ const UserFeed = async (props: Props) => {
</div> </div>
<div className="my-6"> <div className="my-6">
<h2 className="text-lg font-semibold text-foreground">Inbound Activity</h2> <h2 className="text-lg font-semibold text-foreground">Borrow Activity</h2>
<div className="my-3"> <div className="my-3">
<h3 className="text-base font-semibold text-muted-foreground mb-4">Your Holds</h3> <h3 className="text-base font-semibold text-muted-foreground mb-4">Your Holds</h3>
<ul className="grid grid-cols-1 gap-y-6 sm:grid-cols-3 md:grid-cols-4 last-child-adjustment"> <ul className="grid grid-cols-1 gap-y-6 sm:grid-cols-3 md:grid-cols-4 last-child-adjustment">
{holdRequests.docs?.map((h) => { {borrowRequests.docs?.map((h) => {
const book = h.book as Book const book = h.book as Book
const repository = h.repository as Repository const repository = h.repository as Repository

View File

@ -0,0 +1,228 @@
'use client'
import { Author, Book, Checkout, Copy, Repository } from '@/payload-types'
import { useGlobal } from '@/providers/GlobalProvider'
import { getUserBorrows } from '@/serverActions/GetUserBorrows'
import { loaneeReturnCheckout } from '@/serverActions/ReturnCheckout'
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, useMemo, useState } from 'react'
import { toast } from 'sonner'
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',
'Owner Action': 'text-emerald-800 bg-emerald-50 ring-emerald-600/20',
'': '',
}
type Row = {
id: number
title: string
authors: string[]
owners: string[]
repositoryAbbreviation: string
status: 'Passed Due' | 'Due Soon' | ''
dueDate: Date
href: string
doesNeedOwnerVerify: boolean
}
type ListProps = {
rows: Row[]
onUpdate: () => Promise<void>
}
const BorrowedBooksList = (props: ListProps) => {
const { rows, onUpdate } = props
const [returningBookId, setReturningBookId] = useState(0)
const isReturningBook = useCallback((id: number) => id === returningBookId, [returningBookId])
const handleReturnClick = useCallback(
async (id: number) => {
if (returningBookId) return
setReturningBookId(id)
const updatedCheckout = await loaneeReturnCheckout({ checkoutId: id })
if (!updatedCheckout) {
setReturningBookId(0)
toast('There was an issue returning your book')
return
}
await onUpdate()
setReturningBookId(0)
toast('Book was returned, awaiting owner verification.')
},
[returningBookId, setReturningBookId, onUpdate],
)
return (
<ul role="list" className="divide-y divide-gray-100">
{rows.map((r) => (
<li
key={r.id}
className={clsx(
'flex relative items-center justify-between gap-x-6 py-5',
isReturningBook(r.id) ? 'pointer-events-none opacity-30' : '',
r.doesNeedOwnerVerify ? 'pointer-events-none opacity-30' : '',
)}
>
<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">
{r.doesNeedOwnerVerify ? (
<span>Awaiting Owner Verification</span>
) : (
<span>
Due on{' '}
<time dateTime={r.dueDate.toString()}>{r.dueDate.toLocaleDateString()}</time>
</span>
)}
</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 w-full text-left cursor-pointer px-3 py-1 text-sm/6 text-gray-900 data-focus:bg-gray-50 data-focus:outline-hidden"
type="button"
onClick={() => handleReturnClick(r.id)}
>
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 = { initialBorrows: PaginatedDocs<Checkout> | null }
const BorrowedBooks = (props: Props) => {
const { initialBorrows } = props
const { user } = useGlobal()
const [borrows, setBorrows] = useState<PaginatedDocs<Checkout> | null>(initialBorrows)
const onUpdate = useCallback(async () => {
if (!user) return
const updatedUserBorrows = await getUserBorrows({ userId: user.id })
setBorrows(updatedUserBorrows)
}, [setBorrows, user])
const rows: Row[] = useMemo(
() =>
borrows?.docs?.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)
const doesNeedOwnerVerify = c.loaneeReturnedDate && !c.ownerVerifiedReturnedDate
let status = ''
if (doesNeedOwnerVerify) status = 'Owner Action'
else 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}`,
doesNeedOwnerVerify,
} as Row
}) || [],
[borrows],
)
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">Borrowed Books</h3>
</div>
<BorrowedBooksList onUpdate={onUpdate} rows={rows} />
</section>
)
}
export default BorrowedBooks

View File

@ -19,7 +19,13 @@ const HoldRequestNotifications = (props: Props) => {
const [openedModalId, setOpenedModalId] = useState<number | null>(null) const [openedModalId, setOpenedModalId] = useState<number | null>(null)
const totalHoldNotifications = repos?.docs.flatMap((r) => r.holdRequests?.docs).length || 0 const totalHoldNotifications =
repos?.docs
.flatMap((r) => r.holdRequests?.docs)
.filter((r) => {
const request = r as HoldRequest
return !request.isCheckedOut && !request.isRejected
}).length || 0
const [isRejectingId, setIsRejectingId] = useState<number | null>() const [isRejectingId, setIsRejectingId] = useState<number | null>()
@ -155,7 +161,7 @@ const HoldRequestNotifications = (props: Props) => {
return ( return (
<section className="py-6"> <section className="py-6">
<div className="mb-4 flex justify-between items-end"> <div className="mb-4 flex justify-between items-end">
<h3 className="px-4 text-lg font-semibold">Inbound Hold Requests</h3> <h3 className="px-4 text-lg font-semibold">Incoming Hold Requests</h3>
{!!totalHoldNotifications && ( {!!totalHoldNotifications && (
<span className="font-bold">{totalHoldNotifications} Unaddressed</span> <span className="font-bold">{totalHoldNotifications} Unaddressed</span>
)} )}

View File

@ -1,10 +1,16 @@
'use client'
import { Author, Book, Checkout, Copy, Repository } from '@/payload-types' import { Author, Book, Checkout, Copy, Repository } from '@/payload-types'
import { useGlobal } from '@/providers/GlobalProvider'
import { getUserLoans } from '@/serverActions/GetUserLoans'
import { ownerReturnCheckout } from '@/serverActions/ReturnCheckout'
import { Menu, MenuButton, MenuItem, MenuItems } from '@headlessui/react' import { Menu, MenuButton, MenuItem, MenuItems } from '@headlessui/react'
import { EllipsisVerticalIcon } from '@heroicons/react/20/solid' import { EllipsisVerticalIcon } from '@heroicons/react/20/solid'
import clsx from 'clsx' import clsx from 'clsx'
import { Loader2Icon } from 'lucide-react' import { Loader2Icon } from 'lucide-react'
import { PaginatedDocs, User } from 'payload' import { PaginatedDocs, User } from 'payload'
import { useCallback, useState } from 'react' import { useCallback, useMemo, useState } from 'react'
import { toast } from 'sonner'
const statuses = { const statuses = {
'Passed Due': 'text-gray-200 bg-red-500 ring-red-700/10', 'Passed Due': 'text-gray-200 bg-red-500 ring-red-700/10',
@ -23,22 +29,46 @@ type Row = {
href: string href: string
} }
type ListProps = { rows: Row[] } type ListProps = {
const CheckedOutBooksList = (props: ListProps) => { rows: Row[]
const { rows } = props onUpdate: () => Promise<void>
}
const LoanedBooksList = (props: ListProps) => {
const { rows, onUpdate } = props
const [returningBookId, setReturningBookId] = useState(0) const [returningBookId, setReturningBookId] = useState(0)
const isReturningBook = useCallback((id: number) => id === returningBookId, [returningBookId]) const isReturningBook = useCallback((id: number) => id === returningBookId, [returningBookId])
const handleReturnClick = useCallback((id: number) => { const handleReturnClick = useCallback(
async (id: number) => {
if (returningBookId) return
setReturningBookId(id) setReturningBookId(id)
}, [])
const updatedCheckout = await ownerReturnCheckout({ checkoutId: id })
if (!updatedCheckout) {
setReturningBookId(0)
toast('There was an issue returning your book')
return
}
await onUpdate()
setReturningBookId(0)
toast('Book was returned into your collection')
},
[returningBookId, setReturningBookId, onUpdate],
)
return ( return (
<ul role="list" className="divide-y divide-gray-100"> <ul role="list" className="divide-y divide-gray-100">
{rows.map((r) => ( {rows.map((r) => (
<li key={r.id} className="flex items-center justify-between gap-x-6 py-5"> <li
key={r.id}
className={clsx(
'flex relative items-center justify-between gap-x-6 py-5',
isReturningBook(r.id) ? 'pointer-events-none opacity-30' : '',
)}
>
<div className="min-w-0"> <div className="min-w-0">
<div className="flex items-start gap-x-3"> <div className="flex items-start gap-x-3">
<p className="text-sm/6 text-foreground"> <p className="text-sm/6 text-foreground">
@ -58,7 +88,10 @@ const CheckedOutBooksList = (props: ListProps) => {
</div> </div>
<div className="mt-1 flex items-center gap-x-2 text-xs/5 text-gray-500"> <div className="mt-1 flex items-center gap-x-2 text-xs/5 text-gray-500">
<p className="whitespace-nowrap"> <p className="whitespace-nowrap">
Due on <time dateTime={r.dueDate.toString()}>{r.dueDate.toLocaleDateString()}</time> <span>
Due on{' '}
<time dateTime={r.dueDate.toString()}>{r.dueDate.toLocaleDateString()}</time>
</span>
</p> </p>
<svg viewBox="0 0 2 2" className="size-0.5 fill-current"> <svg viewBox="0 0 2 2" className="size-0.5 fill-current">
<circle r={1} cx={1} cy={1} /> <circle r={1} cx={1} cy={1} />
@ -87,21 +120,21 @@ const CheckedOutBooksList = (props: ListProps) => {
> >
<MenuItem> <MenuItem>
<button <button
className="block cursor-pointer px-3 py-1 text-sm/6 text-gray-900 data-focus:bg-gray-50 data-focus:outline-hidden" className="block w-full text-left cursor-pointer px-3 py-1 text-sm/6 text-gray-900 data-focus:bg-gray-50 data-focus:outline-hidden"
type="button" type="button"
onClick={() => console.log('return')} onClick={() => handleReturnClick(r.id)}
> >
Return<span className="sr-only">, {r.title}</span> Return<span className="sr-only">, {r.title}</span>
</button> </button>
</MenuItem> </MenuItem>
<MenuItem> {/*<MenuItem>
<a <a
href={`/repositories/${r.repositoryAbbreviation}`} 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" 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> Repository<span className="sr-only">, {r.repositoryAbbreviation}</span>
</a> </a>
</MenuItem> </MenuItem>*/}
<MenuItem> <MenuItem>
<a <a
href={r.href} href={r.href}
@ -119,13 +152,24 @@ const CheckedOutBooksList = (props: ListProps) => {
) )
} }
type Props = { initialCheckoutPage: PaginatedDocs<Checkout> | null } type Props = { initialLoans: PaginatedDocs<Checkout> | null }
const CheckedOutBooks = (props: Props) => { const LoanedBooks = (props: Props) => {
const { initialCheckoutPage } = props const { initialLoans } = props
const checkouts = initialCheckoutPage?.docs const { user } = useGlobal()
const rows: Row[] =
checkouts?.map((c) => { const [loans, setLoans] = useState<PaginatedDocs<Checkout> | null>(initialLoans)
const onUpdate = useCallback(async () => {
if (!user) return
const updatedUserLoans = await getUserLoans({ userId: user.id })
setLoans(updatedUserLoans)
}, [setLoans, user])
const rows: Row[] = useMemo(
() =>
loans?.docs?.map((c) => {
const copy = c.copy as Copy const copy = c.copy as Copy
const book = copy.book as Book const book = copy.book as Book
const authors = book.authors as Author[] const authors = book.authors as Author[]
@ -148,24 +192,27 @@ const CheckedOutBooks = (props: Props) => {
title: book.title || '', title: book.title || '',
authors: authors.map((a) => a.lf) || ([] as string[]), authors: authors.map((a) => a.lf) || ([] as string[]),
owners: owners:
owners.map((o) => `${(o as User).firstName} ${(o as User).lastName}`) || ([] as string[]), owners.map((o) => `${(o as User).firstName} ${(o as User).lastName}`) ||
([] as string[]),
repositoryAbbreviation: repository.abbreviation || '', repositoryAbbreviation: repository.abbreviation || '',
status, status,
dueDate: parsedDateDue, dueDate: parsedDateDue,
href: `/books/${book.id}`, href: `/books/${book.id}`,
} as Row } as Row
}) || [] }) || [],
[loans],
)
if (!rows.length) return null if (!rows.length) return null
return ( return (
<section className="py-6"> <section className="py-6">
<div className="mb-4 flex justify-between items-end"> <div className="mb-4 flex justify-between items-end">
<h3 className="px-4 text-lg font-semibold">Inbound Checked Out Books</h3> <h3 className="px-4 text-lg font-semibold">Loaned Out Books</h3>
</div> </div>
<CheckedOutBooksList rows={rows} /> <LoanedBooksList onUpdate={onUpdate} rows={rows} />
</section> </section>
) )
} }
export default CheckedOutBooks export default LoanedBooks

View File

@ -4,15 +4,17 @@ import { Checkout, Repository, User } 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' import BorrowedBooks from './BorrowedBooks'
import LoanedBooks from './LoanedBooks'
type Props = { type Props = {
repos: PaginatedDocs<Repository> | null repos: PaginatedDocs<Repository> | null
checkouts: PaginatedDocs<Checkout> | null borrows: PaginatedDocs<Checkout> | null
loans: PaginatedDocs<Checkout> | null
user: User | null user: User | null
} }
const Manage = (props: Props) => { const Manage = (props: Props) => {
const { repos, checkouts, user } = props const { repos, borrows, loans, user } = props
return ( return (
<section> <section>
@ -23,7 +25,11 @@ const Manage = (props: Props) => {
<div>{user && <HoldRequestNotifications repos={repos} user={user} />}</div> <div>{user && <HoldRequestNotifications repos={repos} user={user} />}</div>
<div> <div>
<CheckedOutBooks initialCheckoutPage={checkouts} /> <BorrowedBooks initialBorrows={borrows} />
</div>
<div>
<LoanedBooks initialLoans={loans} />
</div> </div>
</section> </section>
) )

View File

@ -28,21 +28,24 @@ import {
import { StackedLayout } from '@/components/stacked-layout' import { StackedLayout } from '@/components/stacked-layout'
import { Media } from '@/payload-types' import { Media } from '@/payload-types'
import { useGlobal } from '@/providers/GlobalProvider' import { useGlobal } from '@/providers/GlobalProvider'
import { ArrowRightStartOnRectangleIcon, LightBulbIcon, UserIcon } from '@heroicons/react/16/solid' import { ArrowRightStartOnRectangleIcon, UserIcon } from '@heroicons/react/16/solid'
import { MagnifyingGlassIcon, SunIcon, MoonIcon } from '@heroicons/react/20/solid' import { MagnifyingGlassIcon, SunIcon, MoonIcon } from '@heroicons/react/20/solid'
import { useTheme } from 'next-themes' import { useTheme } from 'next-themes'
import Link from 'next/link' import Link from 'next/link'
import React, { useMemo } from 'react' import React, { ReactNode, useMemo } from 'react'
const navItems = [ type NavItem = {
{ label: 'Home', url: '/' }, label: string
{ label: 'Browse', url: '/books' }, href: string
{ label: 'Events', url: '/events' }, newTab: boolean
{ label: 'Settings', url: '/settings' }, }
]
export default function SiteNavigation(props: { children: React.ReactNode }) { type Props = {
const { children } = props children: ReactNode
navItems: NavItem[]
}
export default function SiteNavigation(props: Props) {
const { children, navItems } = props
const { theme, setTheme } = useTheme() const { theme, setTheme } = useTheme()
const { user, setUser } = useGlobal() const { user, setUser } = useGlobal()
@ -70,19 +73,17 @@ export default function SiteNavigation(props: { children: React.ReactNode }) {
navbar={ navbar={
<Navbar> <Navbar>
<Dropdown> <Dropdown>
<DropdownButton <DropdownButton as={NavbarItem} className="max-lg:hidden">
as={NavbarItem} <Link href="/" className="flex items-center justify-around gap-3">
onClick={() => (window.location.href = '/')} <Avatar src="/api/media/file/bethel-logo.jpg" className="size-8" />
className="max-lg:hidden"
>
<Avatar src="/api/media/file/bethel-logo.jpg" />
<NavbarLabel>Midrashim</NavbarLabel> <NavbarLabel>Midrashim</NavbarLabel>
</Link>
</DropdownButton> </DropdownButton>
</Dropdown> </Dropdown>
<NavbarDivider className="max-lg:hidden" /> <NavbarDivider className="max-lg:hidden" />
<NavbarSection className="max-lg:hidden"> <NavbarSection className="max-lg:hidden">
{navItems.map(({ label, url }) => ( {navItems.map(({ label, href, newTab }) => (
<NavbarItem key={label} href={url}> <NavbarItem key={label} target={newTab ? '_blank' : ''} href={href}>
{label} {label}
</NavbarItem> </NavbarItem>
))} ))}
@ -116,10 +117,6 @@ export default function SiteNavigation(props: { children: React.ReactNode }) {
<DropdownLabel>My profile</DropdownLabel> <DropdownLabel>My profile</DropdownLabel>
</DropdownItem> </DropdownItem>
)} )}
<DropdownItem href="/feedback">
<LightBulbIcon />
<DropdownLabel>Share feedback</DropdownLabel>
</DropdownItem>
{!!user && ( {!!user && (
<> <>
@ -149,8 +146,8 @@ export default function SiteNavigation(props: { children: React.ReactNode }) {
</SidebarHeader> </SidebarHeader>
<SidebarBody> <SidebarBody>
<SidebarSection> <SidebarSection>
{navItems.map(({ label, url }) => ( {navItems.map(({ label, href, newTab }) => (
<SidebarItem key={label} href={url}> <SidebarItem key={label} href={href} target={newTab ? '_blank' : ''}>
{label} {label}
</SidebarItem> </SidebarItem>
))} ))}

View File

@ -8,6 +8,8 @@ import { Label } from '@/components/ui/label'
import { useRouter, useSearchParams } from 'next/navigation' import { useRouter, useSearchParams } from 'next/navigation'
import { useState } from 'react' import { useState } from 'react'
import { useGlobal } from '@/providers/GlobalProvider' import { useGlobal } from '@/providers/GlobalProvider'
import { Loader2 } from 'lucide-react'
import { toast } from 'sonner'
export function LoginForm({ className, ...props }: React.ComponentProps<'div'>) { export function LoginForm({ className, ...props }: React.ComponentProps<'div'>) {
const router = useRouter() const router = useRouter()
@ -39,14 +41,19 @@ export function LoginForm({ className, ...props }: React.ComponentProps<'div'>)
password, password,
}), }),
}) })
const user = await loginReq.json() const loginResponse = await loginReq.json()
setUser(user.user) if (loginResponse.errors?.length) {
loginResponse.errors.forEach((e: Error) => {
toast(e.message)
})
}
if (user.token) router.push(searchParams.get('redirectUrl') || '/') if (loginResponse.user) setUser(loginResponse.user)
} catch (error) {
console.error('Login failed:', error) if (loginResponse.token) router.push(searchParams.get('redirectUrl') || '/')
} finally { } catch (_) {
toast('Unknown issue while authenticating. Try again')
setIsLoading(false) setIsLoading(false)
} }
} }
@ -85,16 +92,11 @@ export function LoginForm({ className, ...props }: React.ComponentProps<'div'>)
</div> </div>
<Input id="password" name="password" type="password" required /> <Input id="password" name="password" type="password" required />
</div> </div>
<Button type="submit" className="w-full"> <Button disabled={isLoading} type="submit" className="w-full">
Login {isLoading && <Loader2 />}
<span>Login</span>
</Button> </Button>
</div> </div>
<div className="text-center text-sm">
Don&apos;t have an account?{' '}
<a href="/requestAccess" className="underline underline-offset-4">
Request Access
</a>
</div>
</div> </div>
</form> </form>
</CardContent> </CardContent>

4
src/envConfig.ts Normal file
View File

@ -0,0 +1,4 @@
import { loadEnvConfig } from '@next/env'
const projectDir = process.cwd()
loadEnvConfig(projectDir)

View File

@ -21,9 +21,8 @@ export const Header: GlobalConfig = {
type: 'text', type: 'text',
}, },
{ {
name: 'page', name: 'href',
type: 'relationship', type: 'text'
relationTo: 'pages'
}, },
{ {
name: 'newTab', name: 'newTab',

View File

@ -350,7 +350,7 @@ export interface Checkout {
isReturned?: boolean | null; isReturned?: boolean | null;
dateDue?: string | null; dateDue?: string | null;
ownerVerifiedReturnedDate?: string | null; ownerVerifiedReturnedDate?: string | null;
loanerReturnedDate?: string | null; loaneeReturnedDate?: string | null;
book?: { book?: {
docs?: (number | Copy)[]; docs?: (number | Copy)[];
hasNextPage?: boolean; hasNextPage?: boolean;
@ -598,7 +598,7 @@ export interface CheckoutsSelect<T extends boolean = true> {
isReturned?: T; isReturned?: T;
dateDue?: T; dateDue?: T;
ownerVerifiedReturnedDate?: T; ownerVerifiedReturnedDate?: T;
loanerReturnedDate?: T; loaneeReturnedDate?: T;
book?: T; book?: T;
updatedAt?: T; updatedAt?: T;
createdAt?: T; createdAt?: T;
@ -678,7 +678,7 @@ export interface Header {
headerLinks?: headerLinks?:
| { | {
label?: string | null; label?: string | null;
page?: (number | null) | Page; href?: string | null;
newTab?: boolean | null; newTab?: boolean | null;
id?: string | null; id?: string | null;
}[] }[]
@ -696,7 +696,7 @@ export interface HeaderSelect<T extends boolean = true> {
| T | T
| { | {
label?: T; label?: T;
page?: T; href?: T;
newTab?: T; newTab?: T;
id?: T; id?: T;
}; };

View File

@ -19,6 +19,8 @@ import { Pages } from './collections/Pages/Pages'
import HoldRequests from './collections/Checkouts/HoldRequests' import HoldRequests from './collections/Checkouts/HoldRequests'
import Checkouts from './collections/Checkouts/Checkouts' import Checkouts from './collections/Checkouts/Checkouts'
import './envConfig.ts'
const filename = fileURLToPath(import.meta.url) const filename = fileURLToPath(import.meta.url)
const dirname = path.dirname(filename) const dirname = path.dirname(filename)
@ -30,8 +32,8 @@ export default buildConfig({
}, },
dateFormat: 'MM/dd/yyyy', dateFormat: 'MM/dd/yyyy',
}, },
//cors: [process.env.DOMAIN_NAME || ''], cors: [process.env.SERVER_URL || ''],
//csrf: [process.env.DOMAIN_NAME || ''], csrf: [process.env.SERVER_URL || ''],
upload: { upload: {
limits: { limits: {
fileSize: 5000000, // in bytes fileSize: 5000000, // in bytes

View File

@ -8,7 +8,7 @@ type GlobalProps = {
} }
type GlobalState = { type GlobalState = {
setUser: (users?: User) => void setUser: (user?: User) => void
} & GlobalProps } & GlobalProps
const defaultState = { const defaultState = {

View File

@ -0,0 +1,53 @@
'use server'
import { getPayload } from 'payload'
import config from '@/payload.config'
import type { PaginatedDocs } from "payload"
import { Checkout } from '@/payload-types'
type Props = {
userId: number
}
export const getUserBorrows = async (props: Props) => {
const { userId } = props
const payloadConfig = await config
const payload = await getPayload({ config: payloadConfig })
let userBorrows: PaginatedDocs<Checkout> | null = null
if (userId)
userBorrows = await payload.find({
collection: 'checkouts',
depth: 3,
limit: 10,
select: {
id: true,
copy: true,
dateDue: true,
ownerVerifiedReturnedDate: true,
loaneeReturnedDate: true,
},
sort: 'dateDue',
where: {
and: [
{
isReturned: {
not_equals: true,
},
},
{
'user.id': {
equals: userId,
},
},
{
ownerVerifiedReturnedDate: {
equals: null,
},
}
],
},
}) as PaginatedDocs<Checkout>
return userBorrows
}

View File

@ -0,0 +1,48 @@
'use server'
import { getPayload } from 'payload'
import config from '@/payload.config'
import type { PaginatedDocs } from "payload"
import { Checkout } from '@/payload-types'
type Props = {
userId: number
}
export const getUserLoans = async (props: Props) => {
const { userId } = props
const payloadConfig = await config
const payload = await getPayload({ config: payloadConfig })
let userLoans: PaginatedDocs<Checkout> | null = null
if (userId)
userLoans = await payload.find({
collection: 'checkouts',
depth: 3,
limit: 10,
select: {
id: true,
copy: true,
dateDue: true,
ownerVerifiedReturnedDate: true,
loaneeReturnedDate: true,
},
sort: 'dateDue',
where: {
and: [
{
isReturned: {
not_equals: true,
},
},
{
'user.id': {
equals: userId,
},
},
],
},
}) as PaginatedDocs<Checkout>
return userLoans
}

View File

@ -17,16 +17,14 @@ export const resetPassword = async (props: Props): Promise<boolean> => {
const payload = await getPayload({ config: payloadConfig }) const payload = await getPayload({ config: payloadConfig })
try { try {
const result = await payload.resetPassword({ await payload.resetPassword({
collection: 'users', collection: 'users',
overrideAccess: false, overrideAccess: true,
data: { data: {
password: password, password: password,
token: token, token: token,
} }
}) })
console.log('result')
console.log(result)
return true return true
} catch (err) { } catch (err) {
console.log(err) console.log(err)

View File

@ -0,0 +1,51 @@
'use server'
import { getPayload } from 'payload'
import config from '@/payload.config'
import { Checkout } from '@/payload-types'
type Props = {
checkoutId: number
}
export const loaneeReturnCheckout = async (props: Props): Promise<Checkout | null> => {
const { checkoutId } = props
const payloadConfig = await config
const payload = await getPayload({ config: payloadConfig })
try {
const updatedCheckout = await payload.update({
collection: 'checkouts',
id: checkoutId,
data: {
loaneeReturnedDate: new Date().toDateString(),
}
})
return updatedCheckout
} catch (err) {
console.log(err)
return null
}
}
export const ownerReturnCheckout = async (props: Props): Promise<Checkout | null> => {
const { checkoutId } = props
const payloadConfig = await config
const payload = await getPayload({ config: payloadConfig })
try {
const updatedCheckout = await payload.update({
collection: 'checkouts',
id: checkoutId,
data: {
ownerVerifiedReturnedDate: new Date().toDateString(),
isReturned: true,
}
})
return updatedCheckout
} catch (err) {
console.log(err)
return null
}
}