feat: theme control, user manage feed, globl provider

This commit is contained in:
Yehoshua Sandler 2025-04-28 16:26:18 -05:00
parent 6d32990cb6
commit f291efcc18
25 changed files with 700 additions and 255 deletions

47
package-lock.json generated
View File

@ -17,6 +17,7 @@
"@payloadcms/payload-cloud": "3.31.0", "@payloadcms/payload-cloud": "3.31.0",
"@payloadcms/richtext-lexical": "3.31.0", "@payloadcms/richtext-lexical": "3.31.0",
"@radix-ui/react-label": "^2.1.4", "@radix-ui/react-label": "^2.1.4",
"@radix-ui/react-separator": "^1.1.4",
"@radix-ui/react-slot": "^1.2.0", "@radix-ui/react-slot": "^1.2.0",
"@radix-ui/react-tabs": "^1.1.4", "@radix-ui/react-tabs": "^1.1.4",
"@tailwindcss/cli": "^4.1.4", "@tailwindcss/cli": "^4.1.4",
@ -4285,6 +4286,52 @@
} }
} }
}, },
"node_modules/@radix-ui/react-separator": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.4.tgz",
"integrity": "sha512-2fTm6PSiUm8YPq9W0E4reYuv01EE3aFSzt8edBiXqPHshF8N9+Kymt/k0/R+F3dkY5lQyB/zPtrP82phskLi7w==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-primitive": "2.1.0"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-primitive": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.0.tgz",
"integrity": "sha512-/J/FhLdK0zVcILOwt5g+dH4KnkonCtkVJsa2G6JmvbbtZfBEI1gMsO3QMjseL4F/SwfAMt1Vc/0XKYKq+xJ1sw==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-slot": "1.2.0"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-slot": { "node_modules/@radix-ui/react-slot": {
"version": "1.2.0", "version": "1.2.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.0.tgz", "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.0.tgz",

View File

@ -24,6 +24,7 @@
"@payloadcms/payload-cloud": "3.31.0", "@payloadcms/payload-cloud": "3.31.0",
"@payloadcms/richtext-lexical": "3.31.0", "@payloadcms/richtext-lexical": "3.31.0",
"@radix-ui/react-label": "^2.1.4", "@radix-ui/react-label": "^2.1.4",
"@radix-ui/react-separator": "^1.1.4",
"@radix-ui/react-slot": "^1.2.0", "@radix-ui/react-slot": "^1.2.0",
"@radix-ui/react-tabs": "^1.1.4", "@radix-ui/react-tabs": "^1.1.4",
"@tailwindcss/cli": "^4.1.4", "@tailwindcss/cli": "^4.1.4",

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="48px" height="48px"><linearGradient id="SVGID_1_" x1="37.081" x2="10.918" y1="10.918" y2="37.081" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#60fea4"/><stop offset=".033" stop-color="#6afeaa"/><stop offset=".197" stop-color="#97fec4"/><stop offset=".362" stop-color="#bdffd9"/><stop offset=".525" stop-color="#daffea"/><stop offset=".687" stop-color="#eefff5"/><stop offset=".846" stop-color="#fbfffd"/><stop offset="1" stop-color="#fff"/></linearGradient><circle cx="24" cy="24" r="18.5" fill="url(#SVGID_1_)"/><path fill="none" stroke="#10e36c" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="3" d="M35.401,38.773C32.248,41.21,28.293,42.66,24,42.66C13.695,42.66,5.34,34.305,5.34,24 c0-2.648,0.551-5.167,1.546-7.448"/><path fill="none" stroke="#10e36c" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="3" d="M12.077,9.646C15.31,6.957,19.466,5.34,24,5.34c10.305,0,18.66,8.354,18.66,18.66 c0,2.309-0.419,4.52-1.186,6.561"/><polyline fill="none" stroke="#10e36c" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="3" points="16.5,23.5 21.5,28.5 32,18"/></svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="48px" height="48px"><linearGradient id="SVGID_1_" x1="35.028" x2="7.331" y1="13.995" y2="41.692" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#fea460"/><stop offset=".033" stop-color="#feaa6a"/><stop offset=".197" stop-color="#fec497"/><stop offset=".362" stop-color="#ffd9bd"/><stop offset=".525" stop-color="#ffeada"/><stop offset=".687" stop-color="#fff5ee"/><stop offset=".846" stop-color="#fffdfb"/><stop offset="1" stop-color="#fff"/></linearGradient><path fill="url(#SVGID_1_)" d="M43.475,31.5H29.728V8.696H22.5V12.5h-6.418V7.101H8.584V31.5H4.525c-0.566,0-1.025,0.522-1.025,1.167v4.667 c0,0.644,0.459,1.167,1.025,1.167h38.95c0.566,0,1.025-0.522,1.025-1.167v-4.667C44.5,32.022,44.041,31.5,43.475,31.5z"/><path fill="none" stroke="#fe7c12" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="3" d="M11.298,38.5H4.5c-0.552,0-1-0.448-1-1v-5c0-0.552,0.448-1,1-1h39c0.552,0,1,0.448,1,1v5c0,0.552-0.448,1-1,1 H19"/><line x1="41.531" x2="41.531" y1="43.5" y2="38.5" fill="none" stroke="#fe7c12" stroke-linecap="round" stroke-miterlimit="10" stroke-width="3"/><line x1="6.469" x2="6.469" y1="43.5" y2="38.5" fill="none" stroke="#fe7c12" stroke-linecap="round" stroke-miterlimit="10" stroke-width="3"/><path fill="none" stroke="#fe7c12" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="3" d="M8.5,18v12.5c0,0.552,0.448,1,1,1h5c0.552,0,1-0.448,1-1v-23c0-0.552-0.448-1-1-1h-5c-0.552,0-1,0.448-1,1 v5.543"/><path fill="none" stroke="#fe7c12" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="3" d="M22.5,27.085V30.5c0,0.552,0.448,1,1,1h5c0.552,0,1-0.448,1-1v-21c0-0.552-0.448-1-1-1h-5 c-0.552,0-1,0.448-1,1v12.138"/><path fill="none" stroke="#fe7c12" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="3" d="M22.5,20.957V13.5c0-0.552-0.448-1-1-1h-5c-0.552,0-1,0.448-1,1v17c0,0.552,0.448,1,1,1h3.394"/><path fill="none" stroke="#fe7c12" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="3" d="M39.7,26.726l0.743,2.121c0.183,0.521-0.092,1.092-0.613,1.274l-3.775,1.322 c-0.521,0.183-1.092-0.092-1.274-0.613l-4.627-13.213c-0.183-0.521,0.092-1.092,0.613-1.274l3.775-1.322 c0.521-0.183,1.092,0.092,1.274,0.613L38.045,22"/></svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

1
public/images/reject.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0,0,256,256" width="48px" height="48px" fill-rule="nonzero"><defs><linearGradient x1="37.5" y1="10.5" x2="10.5" y2="37.5" gradientUnits="userSpaceOnUse" id="color-1"><stop offset="0.014" stop-color="#fe6d60"></stop><stop offset="0.046" stop-color="#fe766a"></stop><stop offset="0.208" stop-color="#fea097"></stop><stop offset="0.37" stop-color="#ffc2bd"></stop><stop offset="0.532" stop-color="#ffddda"></stop><stop offset="0.692" stop-color="#fff0ee"></stop><stop offset="0.849" stop-color="#fffbfb"></stop><stop offset="1" stop-color="#ffffff"></stop></linearGradient></defs><g fill-opacity="0" fill="#dddddd" fill-rule="nonzero" stroke="none" stroke-width="1" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="10" stroke-dasharray="" stroke-dashoffset="0" font-family="none" font-weight="none" font-size="none" text-anchor="none" style="mix-blend-mode: normal"><path d="M0,256v-256h256v256z" id="bgRectangle"></path></g><g fill="none" fill-rule="nonzero" stroke="none" stroke-width="none" stroke-linecap="none" stroke-linejoin="none" stroke-miterlimit="10" stroke-dasharray="" stroke-dashoffset="0" font-family="none" font-weight="none" font-size="none" text-anchor="none" style="mix-blend-mode: normal"><g transform="scale(5.33333,5.33333)"><path d="M39.975,12.975l-4.95,-4.95c-0.781,-0.781 -2.047,-0.781 -2.828,0l-8.197,8.197l-8.197,-8.197c-0.781,-0.781 -2.047,-0.781 -2.828,0l-4.95,4.95c-0.781,0.781 -0.781,2.047 0,2.828l8.197,8.197l-8.197,8.197c-0.781,0.781 -0.781,2.047 0,2.828l4.95,4.95c0.781,0.781 2.047,0.781 2.828,0l8.197,-8.197l8.197,8.197c0.781,0.781 2.047,0.781 2.828,0l4.95,-4.95c0.781,-0.781 0.781,-2.047 0,-2.828l-8.197,-8.197l8.197,-8.197c0.781,-0.781 0.781,-2.047 0,-2.828z" fill="url(#color-1)" stroke="none" stroke-width="1" stroke-linecap="butt" stroke-linejoin="miter"></path><path d="M11.728,28.494l-3.703,3.703c-0.781,0.781 -0.781,2.047 0,2.828l4.95,4.95c0.781,0.781 2.047,0.781 2.828,0l8.197,-8.197l8.197,8.197c0.781,0.781 2.047,0.781 2.828,0l4.95,-4.95c0.781,-0.781 0.781,-2.047 0,-2.828l-8.197,-8.197l8.197,-8.197c0.781,-0.781 0.781,-2.047 0,-2.828l-2.571,-2.571" fill="none" stroke="#e02f24" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"></path><path d="M33.079,7.143l-9.079,9.079l-8.197,-8.197c-0.781,-0.781 -2.047,-0.781 -2.828,0l-4.95,4.95c-0.781,0.781 -0.781,2.047 0,2.828l8.197,8.197" fill="none" stroke="#e02f24" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"></path></g></g></svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

@ -1,169 +1,13 @@
import { Avatar } from '@/components/avatar' import SiteNavigation from '@/components/SiteNavigation'
import {
Dropdown,
DropdownButton,
DropdownDivider,
DropdownItem,
DropdownLabel,
DropdownMenu,
} from '@/components/dropdown'
import {
Navbar,
NavbarDivider,
NavbarItem,
NavbarLabel,
NavbarSection,
NavbarSpacer,
} from '@/components/navbar'
import {
Sidebar,
SidebarBody,
SidebarHeader,
SidebarItem,
SidebarLabel,
SidebarSection,
} from '@/components/sidebar'
import { StackedLayout } from '@/components/stacked-layout'
import {
ArrowRightStartOnRectangleIcon,
ChevronDownIcon,
Cog8ToothIcon,
LightBulbIcon,
PlusIcon,
ShieldCheckIcon,
UserIcon,
} from '@heroicons/react/16/solid'
import { InboxIcon, MagnifyingGlassIcon } from '@heroicons/react/20/solid'
import { ThemeProvider } from 'next-themes'
import React from 'react' import React from 'react'
export const metadata = { export const metadata = {
description: "A Community's Collection", description: 'House of Study for Temple Beth El',
title: 'Midrashim', title: 'Midrashim',
} }
const navItems = [
{ label: 'Home', url: '/' },
{ label: 'Events', url: '/events' },
{ label: 'Explore', url: '/browse' },
{ label: 'Settings', url: '/settings' },
]
function TeamDropdownMenu() {
return (
<DropdownMenu className="min-w-80 lg:min-w-64" anchor="bottom start">
<DropdownItem href="/teams/1/settings">
<Cog8ToothIcon />
<DropdownLabel>Settings</DropdownLabel>
</DropdownItem>
<DropdownDivider />
<DropdownItem href="/teams/1">
<Avatar slot="icon" initials="BE" />
<DropdownLabel>Beth-El</DropdownLabel>
</DropdownItem>
<DropdownItem href="/teams/2">
<Avatar slot="icon" initials="EM" className="bg-purple-500 text-white" />
<DropdownLabel>Emanuel</DropdownLabel>
</DropdownItem>
<DropdownDivider />
<DropdownItem href="/repository/create">
<PlusIcon />
<DropdownLabel>New Repository&hellip;</DropdownLabel>
</DropdownItem>
</DropdownMenu>
)
}
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 ( return <SiteNavigation>{children}</SiteNavigation>
<ThemeProvider attribute="class" defaultTheme="system" enableSystem disableTransitionOnChange>
<StackedLayout
navbar={
<Navbar>
<Dropdown>
<DropdownButton as={NavbarItem} className="max-lg:hidden">
<Avatar src="/api/media/file/bethel-logo.jpg" />
<NavbarLabel>Midrashim</NavbarLabel>
<ChevronDownIcon />
</DropdownButton>
<TeamDropdownMenu />
</Dropdown>
<NavbarDivider className="max-lg:hidden" />
<NavbarSection className="max-lg:hidden">
{navItems.map(({ label, url }) => (
<NavbarItem key={label} href={url}>
{label}
</NavbarItem>
))}
</NavbarSection>
<NavbarSpacer />
<NavbarSection>
<NavbarItem href="/search" aria-label="Search">
<MagnifyingGlassIcon />
</NavbarItem>
<NavbarItem href="/inbox" aria-label="Inbox">
<InboxIcon />
</NavbarItem>
<Dropdown>
<DropdownButton as={NavbarItem}>
<Avatar src="/api/media/file/bethel-logo.jpg" square />
</DropdownButton>
<DropdownMenu className="min-w-64" anchor="bottom end">
<DropdownItem href="/my-profile">
<UserIcon />
<DropdownLabel>My profile</DropdownLabel>
</DropdownItem>
<DropdownItem href="/settings">
<Cog8ToothIcon />
<DropdownLabel>Settings</DropdownLabel>
</DropdownItem>
<DropdownDivider />
<DropdownItem href="/privacy-policy">
<ShieldCheckIcon />
<DropdownLabel>Privacy policy</DropdownLabel>
</DropdownItem>
<DropdownItem href="/share-feedback">
<LightBulbIcon />
<DropdownLabel>Share feedback</DropdownLabel>
</DropdownItem>
<DropdownDivider />
<DropdownItem href="/logout">
<ArrowRightStartOnRectangleIcon />
<DropdownLabel>Sign out</DropdownLabel>
</DropdownItem>
</DropdownMenu>
</Dropdown>
</NavbarSection>
</Navbar>
}
sidebar={
<Sidebar>
<SidebarHeader>
<Dropdown>
<DropdownButton as={SidebarItem} className="lg:mb-2.5">
<Avatar src="/api/media/file/bethel-logo.jpg" />
<SidebarLabel>Midrashim</SidebarLabel>
<ChevronDownIcon />
</DropdownButton>
<TeamDropdownMenu />
</Dropdown>
</SidebarHeader>
<SidebarBody>
<SidebarSection>
{navItems.map(({ label, url }) => (
<SidebarItem key={label} href={url}>
{label}
</SidebarItem>
))}
</SidebarSection>
</SidebarBody>
</Sidebar>
}
>
{children}
</StackedLayout>
</ThemeProvider>
)
} }

View File

@ -2,15 +2,14 @@ import { headers as getHeaders } from 'next/headers.js'
import { getPayload, PaginatedDocs } from 'payload' import { getPayload, PaginatedDocs } from 'payload'
import config from '@/payload.config' import config from '@/payload.config'
import React from 'react' import React from 'react'
import { fileURLToPath } from 'url'
import BookList from '@/components/BookList'
import UserFeed from '@/components/Feed/UserFeed' import UserFeed from '@/components/Feed/UserFeed'
import { Book } from '@/payload-types' import { Book, HoldRequest, 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'
import SearchBooks from '@/components/Search/SearchBooks' import SearchBooks from '@/components/Search/SearchBooks'
import Manage from '@/components/Manage/Manage'
export default async function HomePage() { export default async function HomePage() {
const headers = await getHeaders() const headers = await getHeaders()
@ -18,8 +17,6 @@ export default async function HomePage() {
const payload = await getPayload({ config: payloadConfig }) const payload = await getPayload({ config: payloadConfig })
const { user } = await payload.auth({ headers }) const { user } = await payload.auth({ headers })
const fileURL = `vscode://file/${fileURLToPath(import.meta.url)}`
const initBrowseBooks = (await payload.find({ const initBrowseBooks = (await payload.find({
collection: 'books', collection: 'books',
depth: 10, depth: 10,
@ -36,6 +33,32 @@ export default async function HomePage() {
}, },
})) as PaginatedDocs<Book> })) as PaginatedDocs<Book>
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>
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">
@ -103,7 +126,7 @@ export default async function HomePage() {
<TabsList className="grid w-full grid-cols-3"> <TabsList className="grid w-full grid-cols-3">
<TabsTrigger value="feed">Your Feed</TabsTrigger> <TabsTrigger value="feed">Your Feed</TabsTrigger>
<TabsTrigger value="search">Search</TabsTrigger> <TabsTrigger value="search">Search</TabsTrigger>
<TabsTrigger value="yourRepos">Your Repos</TabsTrigger> <TabsTrigger value="manage">Manage</TabsTrigger>
</TabsList> </TabsList>
<TabsContent value="feed">{user && <UserFeed user={user} />}</TabsContent> <TabsContent value="feed">{user && <UserFeed user={user} />}</TabsContent>
@ -112,7 +135,9 @@ export default async function HomePage() {
<SearchBooks initBrowseBooks={initBrowseBooks} /> <SearchBooks initBrowseBooks={initBrowseBooks} />
</TabsContent> </TabsContent>
<TabsContent value="yourRepos">Your Repos</TabsContent> <TabsContent value="manage">
<Manage repos={userRepos} />
</TabsContent>
</Tabs> </Tabs>
) : ( ) : (
<div className="flex w-full max-w-sm flex-col gap-6 mx-auto my-6"> <div className="flex w-full max-w-sm flex-col gap-6 mx-auto my-6">

View File

@ -1,4 +1,5 @@
import { ReactNode } from 'react' import { ReactNode } from 'react'
import { ThemeProvider } from '@/components/ThemeProvider'
type LayoutProps = { type LayoutProps = {
children: ReactNode children: ReactNode
@ -6,12 +7,19 @@ type LayoutProps = {
import './globals.css' import './globals.css'
import { Toaster } from '@/components/ui/sonner' import { Toaster } from '@/components/ui/sonner'
import { GlobalProvider } from '@/providers/GlobalProvider'
const Layout = ({ children }: LayoutProps) => { const Layout = ({ children }: LayoutProps) => {
return ( return (
<html> <html>
<body>{children}</body> <body>
<ThemeProvider attribute="class" defaultTheme="light">
<GlobalProvider globalProps={{}}>
{children}
<Toaster /> <Toaster />
</GlobalProvider>
</ThemeProvider>
</body>
</html> </html>
) )
} }

View File

@ -1,7 +1,8 @@
import { CollectionConfig } from "payload"; import { CollectionConfig } from "payload";
export const Repositories: CollectionConfig = { export const Repositories: CollectionConfig = {
slug: 'repositories', admin: { slug: 'repositories',
admin: {
useAsTitle: 'name' useAsTitle: 'name'
}, },
fields: [ fields: [
@ -20,6 +21,15 @@ export const Repositories: CollectionConfig = {
description: 'This is used to help identify which copies belong to this repo.' description: 'This is used to help identify which copies belong to this repo.'
} }
}, },
{
name: 'image',
type: 'relationship',
relationTo: 'media'
},
{
name: 'description',
type: 'textarea',
},
{ {
name: 'owner', name: 'owner',
type: 'relationship', type: 'relationship',
@ -37,6 +47,7 @@ export const Repositories: CollectionConfig = {
collection: 'holdRequests', collection: 'holdRequests',
on: 'repository', on: 'repository',
defaultSort: 'dateRequested', defaultSort: 'dateRequested',
maxDepth: 3,
where: { where: {
or: [ or: [
{ {

View File

@ -1,5 +1,5 @@
import { defaultAccess } from '@/lib/utils' import { defaultAccess } from '@/lib/utils'
import type { CollectionConfig, PayloadRequest } from 'payload' import type { CollectionConfig } from 'payload'
export const Users: CollectionConfig = { export const Users: CollectionConfig = {
slug: 'users', slug: 'users',
@ -35,6 +35,17 @@ export const Users: CollectionConfig = {
{ {
name: 'isOwnershipClaimed', name: 'isOwnershipClaimed',
type: 'checkbox', type: 'checkbox',
} },
{
name: 'repositories',
type: 'join',
collection: 'repositories',
on: 'owner',
},
{
name: 'profilePicture',
type: 'relationship',
relationTo: 'media',
},
], ],
} }

View File

@ -9,10 +9,8 @@ import {
PaginationPrevious, PaginationPrevious,
} from '@/components/ui/pagination' } from '@/components/ui/pagination'
import { Author, Book, Genre } from '@/payload-types' import { Author, Book, Genre } from '@/payload-types'
import { Avatar } from '../avatar'
import { PaginatedDocs } from 'payload' import { PaginatedDocs } from 'payload'
import Link from 'next/link' import Link from 'next/link'
import Image from 'next/image'
type Props = { type Props = {
books: PaginatedDocs<Book> books: PaginatedDocs<Book>
@ -57,11 +55,9 @@ export default function BookList(props: Props) {
{books?.map((b) => ( {books?.map((b) => (
<li key={b.lcc + (b.title || '')}> <li key={b.lcc + (b.title || '')}>
<Link href={`/books/${b.id}`} className="grid grid-cols-9 gap-x-4 py-5"> <Link href={`/books/${b.id}`} className="grid grid-cols-9 gap-x-4 py-5">
<Image <img
alt="" alt=""
className="w-18 h-22 flex-none bg-gray-800 col-span-2" className="h-24 sm:h-32 rounded inline-block bg-gray-800 col-span-2"
width={180}
height={220}
src={ src={
b.isbn b.isbn
? `https://covers.openlibrary.org/b/isbn/${b.isbn}-M.jpg` ? `https://covers.openlibrary.org/b/isbn/${b.isbn}-M.jpg`
@ -77,7 +73,7 @@ export default function BookList(props: Props) {
{makeGenreBadges(b)} {makeGenreBadges(b)}
</p> </p>
</div> </div>
<div className="flex flex-col mt-2 col-span-9 sm:items-end"> <div className="flex flex-col mt-2 col-span-9">
<p className="text-sm/6 text-foreground font-semibold">{makeAuthorsLabel(b)}</p> <p className="text-sm/6 text-foreground font-semibold">{makeAuthorsLabel(b)}</p>
{!b.copies?.docs?.length ? ( {!b.copies?.docs?.length ? (
<p className="mt-1 text-xs/5 text-gray-400">No copies found</p> <p className="mt-1 text-xs/5 text-gray-400">No copies found</p>

View File

@ -6,6 +6,7 @@ import Image from 'next/image'
import { BorderTrail } from 'components/motion-primitives/border-trail' import { BorderTrail } from 'components/motion-primitives/border-trail'
import clsx from 'clsx' import clsx from 'clsx'
import Link from 'next/link' import Link from 'next/link'
import { LoginForm } from '../login-form'
const stats = [ const stats = [
{ name: 'Outbound Loans', stat: '13' }, { name: 'Outbound Loans', stat: '13' },
@ -20,6 +21,13 @@ const UserFeed = async (props: Props) => {
const { user } = props const { user } = props
const isLoggedIn = !!user const isLoggedIn = !!user
if (!isLoggedIn)
return (
<div className="flex w-full max-w-sm flex-col gap-6 mx-auto my-6">
<LoginForm />
</div>
)
const payloadConfig = await config const payloadConfig = await config
const payload = await getPayload({ config: payloadConfig }) const payload = await getPayload({ config: payloadConfig })
@ -37,6 +45,9 @@ const UserFeed = async (props: Props) => {
userRequested: { userRequested: {
equals: user?.id, equals: user?.id,
}, },
isCheckedOut: {
not_equals: true,
},
}, },
})) as PaginatedDocs<HoldRequest> })) as PaginatedDocs<HoldRequest>

View File

@ -0,0 +1,91 @@
import { PaginatedDocs } from 'payload'
import { Author, Book, HoldRequest, Repository } from '@/payload-types'
import { Button } from '../ui/button'
import Image from 'next/image'
type Props = {
repos: PaginatedDocs<Repository> | null
}
const HoldRequestNotifications = (props: Props) => {
const { repos } = props
const totalHoldNotifications = repos?.docs.flatMap((r) => r.holdRequests?.docs).length || 0
const holdRequestsByRepoElements = repos?.docs.map((r) => {
return (
<ul role="list" className="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3">
{r.holdRequests?.docs?.map((h) => {
const hold = h as HoldRequest
const book = hold.book as Book
const authors = book.authors as Author[]
return (
<li key={hold.id} className="col-span-1 rounded-lg shadow-sm border border-accent">
<div className="flex w-full items-center justify-between space-x-6 p-6">
<div className="flex-1 truncate">
<div className="flex items-center space-x-3">
<h3 className="truncate text-sm font-medium text-foreground">{book.title}</h3>
</div>
<p className="mt-1 truncate text-sm text-gray-500">
{authors.map((a) => a.lf).join(' | ')}
</p>
{hold.isHolding ? (
<span>
<span className="mr-0.5 text-xs">Hold Until</span>
<time className="inline-flex shrink-0 items-center rounded-full bg-background/20 px-1.5 py-0.5 text-xs font-medium text-emerald-600 ring-1 ring-emerald-600/20 ring-inset">
{hold.holdingUntilDate}
</time>
</span>
) : (
<span>
<span className="mr-0.5 text-xs">Requested </span>
{!!hold.dateRequested && (
<time className="inline-flex shrink-0 items-center rounded-full bg-background/20 px-1.5 py-0.5 text-xs font-medium text-amber-500 ring-1 ring-amber-600/20 ring-inset">
{new Date(hold.dateRequested).toLocaleDateString()}
</time>
)}
</span>
)}
</div>
<img
alt=""
className="h-16 shrink-0 rounded bg-gray-300"
src={
book.isbn
? `https://covers.openlibrary.org/b/isbn/${book.isbn}-M.jpg`
: '/images/book-48.svg'
}
/>
</div>
<div>
<div className="flex gap-3 justify-around">
<Button className="inline-flex flex-1 items-center justify-center gap-3 rounded-bl-lg border border-transparent py-4 text-sm font-semibold text-foreground bg-foreground/5 hover:bg-red-400/10 cursor-pointer hover:scale-105">
<Image width={24} height={24} src="/images/reject.svg" alt="approve hold" />
<span>Decline</span>
</Button>
<Button className="inline-flex flex-1 items-center justify-center gap-3 rounded-br-lg border border-transparent py-4 text-sm font-semibold text-foreground bg-emerald-400/30 hover:bg-emerald-300/60 cursor-pointer hover:scale-105">
<Image width={24} height={24} src="/images/approve.svg" alt="approve hold" />
<span>Approve</span>
</Button>
</div>
</div>
</li>
)
})}
</ul>
)
})
return (
<section className="py-6">
<div className="mb-4 flex justify-between items-end">
<h3 className="px-4 text-lg font-semibold">Inbound Hold Requests</h3>
{!!totalHoldNotifications && (
<span className="font-bold">{totalHoldNotifications} Unaddressed</span>
)}
</div>
{holdRequestsByRepoElements}
</section>
)
}
export default HoldRequestNotifications

View File

@ -0,0 +1,27 @@
'use client'
import { Repository } from '@/payload-types'
import { PaginatedDocs } from 'payload'
import RepoList from './RepoList'
import HoldRequestNotifications from './HoldRequests'
type Props = {
repos: PaginatedDocs<Repository> | null
}
const Manage = (props: Props) => {
const { repos } = props
return (
<section>
<div className="my-6">
<RepoList repos={repos} />
</div>
<div>
<HoldRequestNotifications repos={repos} />
</div>
</section>
)
}
export default Manage

View File

@ -0,0 +1,79 @@
import { ArrowRight, Plus } from 'lucide-react'
import React from 'react'
import { Button } from '@/components/ui/button'
import { Separator } from '@/components/ui/separator'
import { PaginatedDocs } from 'payload'
import { Media, Repository } from '@/payload-types'
import Image from 'next/image'
import Link from 'next/link'
type Props = {
repos: PaginatedDocs<Repository> | null
}
const RepoList = (props: Props) => {
const { repos } = props
return (
<section className="py-6">
<div className="mb-4 flex justify-between items-end">
<h3 className="px-4 text-lg font-semibold">Your Repositories</h3>
<Link
href="/account/repos/add"
className="flex align-middle items-center justify-around transition-all hover:scale-105"
>
<Plus className="inline-block size-4" />
<span className="inline-block">Add New</span>
</Link>
</div>
<Separator />
{repos?.docs.map((item, index) => {
const image = (item.image as Media) || undefined
return (
<React.Fragment key={index}>
<div className="grid items-center px-4 py-3 sm:py-2 grid-cols-5">
<div className="flex sm:items-center gap-2 col-span-5 sm:col-span-4">
<span className="flex h-14 w-16 shrink-0 items-center justify-center sm:items-end">
<Image
height={26}
width={26}
alt=""
src={image?.url || '/images/library.svg'}
className="w-full items-center justify-center rounded-full bg-muted"
/>
</span>
<div className="flex flex-wrap items-center justify-between sm:items-stretch sm:justify-normal sm:flex-nowrap sm:flex-col">
<h3 className="font-semibold">{item.name}</h3>
<p className="text-sm text-muted-foreground">{item.abbreviation}</p>
<p className="text-xs font-thin italic w-full shrink-0 sm:w-auto sm:shrink">
{item.dateOpenToPublic ? (
<span>Opened: {item.dateOpenToPublic}</span>
) : (
<span>Not Opened to Public</span>
)}
</p>
<p className="pr-4 mt-1">{item.description}</p>
</div>
</div>
<Button
className="col-span-1 col-start-4 mt-2 sm:mt-0 sm:col-start-5"
variant="outline"
asChild
>
<a className="ml-auto gap-2" href={`/repo/${item.abbreviation}`}>
<span>View Repo</span>
<ArrowRight className="h-4 w-4" />
</a>
</Button>
</div>
<Separator />
</React.Fragment>
)
})}
</section>
)
}
export default RepoList

View File

@ -19,9 +19,9 @@ const SearchBooks = (props: Props) => {
} }
return ( return (
<section> <section className="my-8">
<SearchBooksInlineForm onSearchResult={onSearchResult} /> <SearchBooksInlineForm onSearchResult={onSearchResult} />
{bookList && <BookList books={bookList} />} <div className="my-4">{bookList && <BookList books={bookList} />}</div>
</section> </section>
) )
} }

View File

@ -60,7 +60,10 @@ const SearchBooksInlineForm = (props: Props) => {
return ( return (
<Form {...form}> <Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="flex flex-wrap items-end gap-2"> <form
onSubmit={form.handleSubmit(onSubmit)}
className="flex flex-wrap sm:flex-nowrap items-end gap-2"
>
<FormField <FormField
control={form.control} control={form.control}
name="title" name="title"
@ -87,7 +90,11 @@ const SearchBooksInlineForm = (props: Props) => {
</FormItem> </FormItem>
)} )}
/> />
<Button className="sm:w-auto w-full block" disabled={isSearching} type="submit"> <Button
className="sm:w-auto w-full block cursor-pointer"
disabled={isSearching}
type="submit"
>
{isSearching ? <Loader2 className="animate-spin mx-auto" /> : <span>Search</span>} {isSearching ? <Loader2 className="animate-spin mx-auto" /> : <span>Search</span>}
</Button> </Button>
</form> </form>

View File

@ -0,0 +1,164 @@
'use client'
import { Avatar } from '@/components/avatar'
import {
Dropdown,
DropdownButton,
DropdownDivider,
DropdownItem,
DropdownLabel,
DropdownMenu,
} from '@/components/dropdown'
import {
Navbar,
NavbarDivider,
NavbarItem,
NavbarLabel,
NavbarSection,
NavbarSpacer,
} from '@/components/navbar'
import {
Sidebar,
SidebarBody,
SidebarHeader,
SidebarItem,
SidebarLabel,
SidebarSection,
} from '@/components/sidebar'
import { StackedLayout } from '@/components/stacked-layout'
import { Media } from '@/payload-types'
import { useGlobal } from '@/providers/GlobalProvider'
import { ArrowRightStartOnRectangleIcon, LightBulbIcon, UserIcon } from '@heroicons/react/16/solid'
import { MagnifyingGlassIcon, SunIcon, MoonIcon } from '@heroicons/react/20/solid'
import { useTheme } from 'next-themes'
import React, { useMemo } from 'react'
const navItems = [
{ label: 'Home', url: '/' },
{ label: 'Events', url: '/events' },
{ label: 'Explore', url: '/browse' },
{ label: 'Settings', url: '/settings' },
]
export default function SiteNavigation(props: { children: React.ReactNode }) {
const { children } = props
const { theme, setTheme } = useTheme()
const { user, setUser } = useGlobal()
useMemo(() => {
if (user) return
fetch('/api/users/me', {
method: 'GET',
credentials: 'include',
headers: {
'Content-Type': 'application/json',
},
}).then(async (response) => {
const userRequest = await response.json()
setUser(userRequest.user)
console.log(userRequest.user)
})
}, [user?.id])
const profilePicture = user?.profilePicture as Media | undefined
const initials = user?.firstName
? `${user?.firstName.slice(0, 1) || ''}${user?.lastName?.slice(0, 1)}`
: 'U'
return (
<StackedLayout
navbar={
<Navbar>
<Dropdown>
<DropdownButton
as={NavbarItem}
onClick={() => (window.location.href = '/')}
className="max-lg:hidden"
>
<Avatar src="/api/media/file/bethel-logo.jpg" />
<NavbarLabel>Midrashim</NavbarLabel>
</DropdownButton>
</Dropdown>
<NavbarDivider className="max-lg:hidden" />
<NavbarSection className="max-lg:hidden">
{navItems.map(({ label, url }) => (
<NavbarItem key={label} href={url}>
{label}
</NavbarItem>
))}
</NavbarSection>
<NavbarSpacer />
<NavbarSection>
<NavbarItem href="/search" aria-label="Search">
<MagnifyingGlassIcon />
</NavbarItem>
<NavbarItem
aria-label="Inbox"
role={'button'}
onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}
>
<SunIcon className="dark:hidden" />
<MoonIcon className="hidden dark:block" />
</NavbarItem>
<Dropdown>
<DropdownButton as={NavbarItem}>
<Avatar src={profilePicture?.url || ''} initials={initials} square={false} />
</DropdownButton>
<DropdownMenu className="min-w-64" anchor="bottom end">
<DropdownItem href="/profile">
<UserIcon />
<DropdownLabel>My profile</DropdownLabel>
</DropdownItem>
{/*<DropdownItem href="/settings">
<Cog8ToothIcon />
<DropdownLabel>Settings</DropdownLabel>
</DropdownItem>
<DropdownDivider />
<DropdownItem href="/privacy-policy">
<ShieldCheckIcon />
<DropdownLabel>Privacy policy</DropdownLabel>
</DropdownItem>*/}
<DropdownItem href="/feedback">
<LightBulbIcon />
<DropdownLabel>Share feedback</DropdownLabel>
</DropdownItem>
<DropdownDivider />
<DropdownItem href="/logout">
<ArrowRightStartOnRectangleIcon />
<DropdownLabel>Sign out</DropdownLabel>
</DropdownItem>
</DropdownMenu>
</Dropdown>
</NavbarSection>
</Navbar>
}
sidebar={
<Sidebar>
<SidebarHeader>
<Dropdown>
<DropdownButton
as={SidebarItem}
className="lg:mb-2.5"
onClick={() => (window.location.href = '/')}
>
<Avatar src="/api/media/file/bethel-logo.jpg" />
<SidebarLabel>Midrashim</SidebarLabel>
</DropdownButton>
</Dropdown>
</SidebarHeader>
<SidebarBody>
<SidebarSection>
{navItems.map(({ label, url }) => (
<SidebarItem key={label} href={url}>
{label}
</SidebarItem>
))}
</SidebarSection>
</SidebarBody>
</Sidebar>
}
>
{children}
</StackedLayout>
)
}

View File

@ -0,0 +1,7 @@
'use client'
import { ThemeProvider as NextThemesProvider, ThemeProviderProps } from 'next-themes'
export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
return <NextThemesProvider {...props}>{children}</NextThemesProvider>
}

View File

@ -56,7 +56,7 @@ export function StackedLayout({
let [showSidebar, setShowSidebar] = useState(false) let [showSidebar, setShowSidebar] = useState(false)
return ( return (
<div className="relative isolate flex min-h-svh w-full flex-col bg-white lg:bg-zinc-100 dark:bg-zinc-900 dark:lg:bg-zinc-950"> <div className="relative isolate flex min-h-svh w-full flex-col lg:bg-zinc-100 dark:bg-zinc-900 dark:lg:bg-zinc-950">
{/* Sidebar on mobile */} {/* Sidebar on mobile */}
<MobileSidebar open={showSidebar} close={() => setShowSidebar(false)}> <MobileSidebar open={showSidebar} close={() => setShowSidebar(false)}>
{sidebar} {sidebar}

View File

@ -0,0 +1,28 @@
"use client"
import * as React from "react"
import * as SeparatorPrimitive from "@radix-ui/react-separator"
import { cn } from "@/lib/utils"
function Separator({
className,
orientation = "horizontal",
decorative = true,
...props
}: React.ComponentProps<typeof SeparatorPrimitive.Root>) {
return (
<SeparatorPrimitive.Root
data-slot="separator-root"
decorative={decorative}
orientation={orientation}
className={cn(
"bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px",
className
)}
{...props}
/>
)
}
export { Separator }

View File

@ -82,6 +82,9 @@ export interface Config {
'payload-migrations': PayloadMigration; 'payload-migrations': PayloadMigration;
}; };
collectionsJoins: { collectionsJoins: {
users: {
repositories: 'repositories';
};
books: { books: {
copies: 'copies'; copies: 'copies';
}; };
@ -159,6 +162,12 @@ export interface User {
firstName?: string | null; firstName?: string | null;
lastName?: string | null; lastName?: string | null;
isOwnershipClaimed?: boolean | null; isOwnershipClaimed?: boolean | null;
repositories?: {
docs?: (number | Repository)[];
hasNextPage?: boolean;
totalDocs?: number;
};
profilePicture?: (number | null) | Media;
updatedAt: string; updatedAt: string;
createdAt: string; createdAt: string;
email: string; email: string;
@ -170,6 +179,29 @@ export interface User {
lockUntil?: string | null; lockUntil?: string | null;
password?: string | null; password?: string | null;
} }
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "repositories".
*/
export interface Repository {
id: number;
name: string;
/**
* This is used to help identify which copies belong to this repo.
*/
abbreviation: string;
image?: (number | null) | Media;
description?: string | null;
owner: (number | User)[];
dateOpenToPublic?: string | null;
holdRequests?: {
docs?: (number | HoldRequest)[];
hasNextPage?: boolean;
totalDocs?: number;
};
updatedAt: string;
createdAt: string;
}
/** /**
* This interface was referenced by `Config`'s JSON-Schema * This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "media". * via the `definition` "media".
@ -189,6 +221,56 @@ export interface Media {
focalX?: number | null; focalX?: number | null;
focalY?: number | null; focalY?: number | null;
} }
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "holdRequests".
*/
export interface HoldRequest {
id: number;
copy?: (number | null) | Copy;
book: number | Book;
repository?: (number | null) | Repository;
userRequested: number | User;
dateRequested?: string | null;
isHolding?: boolean | null;
holdingUntilDate?: string | null;
isCheckedOut?: boolean | null;
updatedAt: string;
createdAt: string;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "copies".
*/
export interface Copy {
id: number;
label?: string | null;
condition?: number | null;
book?: (number | null) | Book;
repository?: (number | null) | Repository;
notes?: {
root: {
type: string;
children: {
type: string;
version: number;
[k: string]: unknown;
}[];
direction: ('ltr' | 'rtl') | null;
format: 'left' | 'start' | 'center' | 'right' | 'end' | 'justify' | '';
indent: number;
version: number;
};
[k: string]: unknown;
} | null;
holdRequests?: {
docs?: (number | HoldRequest)[];
hasNextPage?: boolean;
totalDocs?: number;
};
updatedAt: string;
createdAt: string;
}
/** /**
* This interface was referenced by `Config`'s JSON-Schema * This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "books". * via the `definition` "books".
@ -256,77 +338,6 @@ export interface Genre {
updatedAt: string; updatedAt: string;
createdAt: string; createdAt: string;
} }
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "copies".
*/
export interface Copy {
id: number;
label?: string | null;
condition?: number | null;
book?: (number | null) | Book;
repository?: (number | null) | Repository;
notes?: {
root: {
type: string;
children: {
type: string;
version: number;
[k: string]: unknown;
}[];
direction: ('ltr' | 'rtl') | null;
format: 'left' | 'start' | 'center' | 'right' | 'end' | 'justify' | '';
indent: number;
version: number;
};
[k: string]: unknown;
} | null;
holdRequests?: {
docs?: (number | HoldRequest)[];
hasNextPage?: boolean;
totalDocs?: number;
};
updatedAt: string;
createdAt: string;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "repositories".
*/
export interface Repository {
id: number;
name: string;
/**
* This is used to help identify which copies belong to this repo.
*/
abbreviation: string;
owner: (number | User)[];
dateOpenToPublic?: string | null;
holdRequests?: {
docs?: (number | HoldRequest)[];
hasNextPage?: boolean;
totalDocs?: number;
};
updatedAt: string;
createdAt: string;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "holdRequests".
*/
export interface HoldRequest {
id: number;
copy?: (number | null) | Copy;
book: number | Book;
repository?: (number | null) | Repository;
userRequested: number | User;
dateRequested?: string | null;
isHolding?: boolean | null;
holdingUntilDate?: string | null;
isCheckedOut?: boolean | null;
updatedAt: string;
createdAt: string;
}
/** /**
* This interface was referenced by `Config`'s JSON-Schema * This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "checkouts". * via the `definition` "checkouts".
@ -464,6 +475,8 @@ export interface UsersSelect<T extends boolean = true> {
firstName?: T; firstName?: T;
lastName?: T; lastName?: T;
isOwnershipClaimed?: T; isOwnershipClaimed?: T;
repositories?: T;
profilePicture?: T;
updatedAt?: T; updatedAt?: T;
createdAt?: T; createdAt?: T;
email?: T; email?: T;
@ -532,6 +545,8 @@ export interface AuthorsSelect<T extends boolean = true> {
export interface RepositoriesSelect<T extends boolean = true> { export interface RepositoriesSelect<T extends boolean = true> {
name?: T; name?: T;
abbreviation?: T; abbreviation?: T;
image?: T;
description?: T;
owner?: T; owner?: T;
dateOpenToPublic?: T; dateOpenToPublic?: T;
holdRequests?: T; holdRequests?: T;

View File

@ -0,0 +1,35 @@
'use client'
import { User } from '@/payload-types'
import { createContext, ReactNode, useContext, useState } from 'react'
type GlobalProps = {
user?: User
}
type GlobalState = {
setUser: (users?: User) => void
} & GlobalProps
const defaultState = {
user: undefined,
setUser: (user?: User) => {},
}
const GlobalContext = createContext<GlobalState>(defaultState)
type Props = { children: ReactNode; globalProps: GlobalProps }
export function GlobalProvider({ children, globalProps }: Props) {
const [user, setUser] = useState(globalProps.user)
const value = {
user,
setUser,
}
return <GlobalContext.Provider value={value}>{children}</GlobalContext.Provider>
}
export function useGlobal() {
return useContext(GlobalContext)
}

View File

@ -0,0 +1,35 @@
'use server'
import { getPayload, PaginatedDocs } from 'payload'
import config from '@/payload.config'
import { HoldRequest } from '@/payload-types'
type Props = {
userId: number
}
export const getHoldRequestOnUserRepo = async (props: Props): Promise<PaginatedDocs<HoldRequest> | null> => {
const { userId } = props
const payloadConfig = await config
const payload = await getPayload({ config: payloadConfig })
const findResponse = (await payload.find({
collection: 'holdRequests',
depth: 2,
limit: 25,
overrideAccess: false,
where: {
'repository.owner': {
equals: userId
},
},
select: {
id: true,
copy: true,
repository: true,
book: true,
},
})) as PaginatedDocs<HoldRequest>
return findResponse
}