feat: theme control, user manage feed, globl provider
This commit is contained in:
parent
6d32990cb6
commit
f291efcc18
47
package-lock.json
generated
47
package-lock.json
generated
@ -17,6 +17,7 @@
|
||||
"@payloadcms/payload-cloud": "3.31.0",
|
||||
"@payloadcms/richtext-lexical": "3.31.0",
|
||||
"@radix-ui/react-label": "^2.1.4",
|
||||
"@radix-ui/react-separator": "^1.1.4",
|
||||
"@radix-ui/react-slot": "^1.2.0",
|
||||
"@radix-ui/react-tabs": "^1.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": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.0.tgz",
|
||||
|
||||
@ -24,6 +24,7 @@
|
||||
"@payloadcms/payload-cloud": "3.31.0",
|
||||
"@payloadcms/richtext-lexical": "3.31.0",
|
||||
"@radix-ui/react-label": "^2.1.4",
|
||||
"@radix-ui/react-separator": "^1.1.4",
|
||||
"@radix-ui/react-slot": "^1.2.0",
|
||||
"@radix-ui/react-tabs": "^1.1.4",
|
||||
"@tailwindcss/cli": "^4.1.4",
|
||||
|
||||
1
public/images/approve.svg
Normal file
1
public/images/approve.svg
Normal 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 |
1
public/images/library.svg
Normal file
1
public/images/library.svg
Normal 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
1
public/images/reject.svg
Normal 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 |
@ -1,169 +1,13 @@
|
||||
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 {
|
||||
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 SiteNavigation from '@/components/SiteNavigation'
|
||||
import React from 'react'
|
||||
|
||||
export const metadata = {
|
||||
description: "A Community's Collection",
|
||||
description: 'House of Study for Temple Beth El',
|
||||
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…</DropdownLabel>
|
||||
</DropdownItem>
|
||||
</DropdownMenu>
|
||||
)
|
||||
}
|
||||
|
||||
export default async function RootLayout(props: { children: React.ReactNode }) {
|
||||
const { children } = props
|
||||
|
||||
return (
|
||||
<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>
|
||||
)
|
||||
return <SiteNavigation>{children}</SiteNavigation>
|
||||
}
|
||||
|
||||
@ -2,15 +2,14 @@ import { headers as getHeaders } from 'next/headers.js'
|
||||
import { getPayload, PaginatedDocs } from 'payload'
|
||||
import config from '@/payload.config'
|
||||
import React from 'react'
|
||||
import { fileURLToPath } from 'url'
|
||||
|
||||
import BookList from '@/components/BookList'
|
||||
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 { TextShimmer } from '@/components/ui/text-shimmer'
|
||||
import { LoginForm } from '@/components/login-form'
|
||||
import SearchBooks from '@/components/Search/SearchBooks'
|
||||
import Manage from '@/components/Manage/Manage'
|
||||
|
||||
export default async function HomePage() {
|
||||
const headers = await getHeaders()
|
||||
@ -18,8 +17,6 @@ export default async function HomePage() {
|
||||
const payload = await getPayload({ config: payloadConfig })
|
||||
const { user } = await payload.auth({ headers })
|
||||
|
||||
const fileURL = `vscode://file/${fileURLToPath(import.meta.url)}`
|
||||
|
||||
const initBrowseBooks = (await payload.find({
|
||||
collection: 'books',
|
||||
depth: 10,
|
||||
@ -36,6 +33,32 @@ export default async function HomePage() {
|
||||
},
|
||||
})) 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 (
|
||||
<div className="home">
|
||||
<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">
|
||||
<TabsTrigger value="feed">Your Feed</TabsTrigger>
|
||||
<TabsTrigger value="search">Search</TabsTrigger>
|
||||
<TabsTrigger value="yourRepos">Your Repos</TabsTrigger>
|
||||
<TabsTrigger value="manage">Manage</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="feed">{user && <UserFeed user={user} />}</TabsContent>
|
||||
@ -112,7 +135,9 @@ export default async function HomePage() {
|
||||
<SearchBooks initBrowseBooks={initBrowseBooks} />
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="yourRepos">Your Repos</TabsContent>
|
||||
<TabsContent value="manage">
|
||||
<Manage repos={userRepos} />
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
) : (
|
||||
<div className="flex w-full max-w-sm flex-col gap-6 mx-auto my-6">
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { ReactNode } from 'react'
|
||||
import { ThemeProvider } from '@/components/ThemeProvider'
|
||||
|
||||
type LayoutProps = {
|
||||
children: ReactNode
|
||||
@ -6,12 +7,19 @@ type LayoutProps = {
|
||||
|
||||
import './globals.css'
|
||||
import { Toaster } from '@/components/ui/sonner'
|
||||
import { GlobalProvider } from '@/providers/GlobalProvider'
|
||||
|
||||
const Layout = ({ children }: LayoutProps) => {
|
||||
return (
|
||||
<html>
|
||||
<body>{children}</body>
|
||||
<body>
|
||||
<ThemeProvider attribute="class" defaultTheme="light">
|
||||
<GlobalProvider globalProps={{}}>
|
||||
{children}
|
||||
<Toaster />
|
||||
</GlobalProvider>
|
||||
</ThemeProvider>
|
||||
</body>
|
||||
</html>
|
||||
)
|
||||
}
|
||||
|
||||
@ -1,7 +1,8 @@
|
||||
import { CollectionConfig } from "payload";
|
||||
|
||||
export const Repositories: CollectionConfig = {
|
||||
slug: 'repositories', admin: {
|
||||
slug: 'repositories',
|
||||
admin: {
|
||||
useAsTitle: 'name'
|
||||
},
|
||||
fields: [
|
||||
@ -20,6 +21,15 @@ export const Repositories: CollectionConfig = {
|
||||
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',
|
||||
type: 'relationship',
|
||||
@ -37,6 +47,7 @@ export const Repositories: CollectionConfig = {
|
||||
collection: 'holdRequests',
|
||||
on: 'repository',
|
||||
defaultSort: 'dateRequested',
|
||||
maxDepth: 3,
|
||||
where: {
|
||||
or: [
|
||||
{
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { defaultAccess } from '@/lib/utils'
|
||||
import type { CollectionConfig, PayloadRequest } from 'payload'
|
||||
import type { CollectionConfig } from 'payload'
|
||||
|
||||
export const Users: CollectionConfig = {
|
||||
slug: 'users',
|
||||
@ -35,6 +35,17 @@ export const Users: CollectionConfig = {
|
||||
{
|
||||
name: 'isOwnershipClaimed',
|
||||
type: 'checkbox',
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'repositories',
|
||||
type: 'join',
|
||||
collection: 'repositories',
|
||||
on: 'owner',
|
||||
},
|
||||
{
|
||||
name: 'profilePicture',
|
||||
type: 'relationship',
|
||||
relationTo: 'media',
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
@ -9,10 +9,8 @@ import {
|
||||
PaginationPrevious,
|
||||
} from '@/components/ui/pagination'
|
||||
import { Author, Book, Genre } from '@/payload-types'
|
||||
import { Avatar } from '../avatar'
|
||||
import { PaginatedDocs } from 'payload'
|
||||
import Link from 'next/link'
|
||||
import Image from 'next/image'
|
||||
|
||||
type Props = {
|
||||
books: PaginatedDocs<Book>
|
||||
@ -57,11 +55,9 @@ export default function BookList(props: Props) {
|
||||
{books?.map((b) => (
|
||||
<li key={b.lcc + (b.title || '')}>
|
||||
<Link href={`/books/${b.id}`} className="grid grid-cols-9 gap-x-4 py-5">
|
||||
<Image
|
||||
<img
|
||||
alt=""
|
||||
className="w-18 h-22 flex-none bg-gray-800 col-span-2"
|
||||
width={180}
|
||||
height={220}
|
||||
className="h-24 sm:h-32 rounded inline-block bg-gray-800 col-span-2"
|
||||
src={
|
||||
b.isbn
|
||||
? `https://covers.openlibrary.org/b/isbn/${b.isbn}-M.jpg`
|
||||
@ -77,7 +73,7 @@ export default function BookList(props: Props) {
|
||||
{makeGenreBadges(b)}
|
||||
</p>
|
||||
</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>
|
||||
{!b.copies?.docs?.length ? (
|
||||
<p className="mt-1 text-xs/5 text-gray-400">No copies found</p>
|
||||
|
||||
@ -6,6 +6,7 @@ import Image from 'next/image'
|
||||
import { BorderTrail } from 'components/motion-primitives/border-trail'
|
||||
import clsx from 'clsx'
|
||||
import Link from 'next/link'
|
||||
import { LoginForm } from '../login-form'
|
||||
|
||||
const stats = [
|
||||
{ name: 'Outbound Loans', stat: '13' },
|
||||
@ -20,6 +21,13 @@ const UserFeed = async (props: Props) => {
|
||||
const { user } = props
|
||||
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 payload = await getPayload({ config: payloadConfig })
|
||||
|
||||
@ -37,6 +45,9 @@ const UserFeed = async (props: Props) => {
|
||||
userRequested: {
|
||||
equals: user?.id,
|
||||
},
|
||||
isCheckedOut: {
|
||||
not_equals: true,
|
||||
},
|
||||
},
|
||||
})) as PaginatedDocs<HoldRequest>
|
||||
|
||||
|
||||
91
src/components/Manage/HoldRequests.tsx
Normal file
91
src/components/Manage/HoldRequests.tsx
Normal 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
|
||||
27
src/components/Manage/Manage.tsx
Normal file
27
src/components/Manage/Manage.tsx
Normal 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
|
||||
79
src/components/Manage/RepoList.tsx
Normal file
79
src/components/Manage/RepoList.tsx
Normal 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
|
||||
@ -19,9 +19,9 @@ const SearchBooks = (props: Props) => {
|
||||
}
|
||||
|
||||
return (
|
||||
<section>
|
||||
<section className="my-8">
|
||||
<SearchBooksInlineForm onSearchResult={onSearchResult} />
|
||||
{bookList && <BookList books={bookList} />}
|
||||
<div className="my-4">{bookList && <BookList books={bookList} />}</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
@ -60,7 +60,10 @@ const SearchBooksInlineForm = (props: Props) => {
|
||||
|
||||
return (
|
||||
<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
|
||||
control={form.control}
|
||||
name="title"
|
||||
@ -87,7 +90,11 @@ const SearchBooksInlineForm = (props: Props) => {
|
||||
</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>}
|
||||
</Button>
|
||||
</form>
|
||||
|
||||
164
src/components/SiteNavigation.tsx
Normal file
164
src/components/SiteNavigation.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
7
src/components/ThemeProvider.tsx
Normal file
7
src/components/ThemeProvider.tsx
Normal 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>
|
||||
}
|
||||
@ -56,7 +56,7 @@ export function StackedLayout({
|
||||
let [showSidebar, setShowSidebar] = useState(false)
|
||||
|
||||
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 */}
|
||||
<MobileSidebar open={showSidebar} close={() => setShowSidebar(false)}>
|
||||
{sidebar}
|
||||
|
||||
28
src/components/ui/separator.tsx
Normal file
28
src/components/ui/separator.tsx
Normal 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 }
|
||||
@ -82,6 +82,9 @@ export interface Config {
|
||||
'payload-migrations': PayloadMigration;
|
||||
};
|
||||
collectionsJoins: {
|
||||
users: {
|
||||
repositories: 'repositories';
|
||||
};
|
||||
books: {
|
||||
copies: 'copies';
|
||||
};
|
||||
@ -159,6 +162,12 @@ export interface User {
|
||||
firstName?: string | null;
|
||||
lastName?: string | null;
|
||||
isOwnershipClaimed?: boolean | null;
|
||||
repositories?: {
|
||||
docs?: (number | Repository)[];
|
||||
hasNextPage?: boolean;
|
||||
totalDocs?: number;
|
||||
};
|
||||
profilePicture?: (number | null) | Media;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
email: string;
|
||||
@ -170,6 +179,29 @@ export interface User {
|
||||
lockUntil?: 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
|
||||
* via the `definition` "media".
|
||||
@ -189,6 +221,56 @@ export interface Media {
|
||||
focalX?: 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
|
||||
* via the `definition` "books".
|
||||
@ -256,77 +338,6 @@ export interface Genre {
|
||||
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
|
||||
* 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
|
||||
* via the `definition` "checkouts".
|
||||
@ -464,6 +475,8 @@ export interface UsersSelect<T extends boolean = true> {
|
||||
firstName?: T;
|
||||
lastName?: T;
|
||||
isOwnershipClaimed?: T;
|
||||
repositories?: T;
|
||||
profilePicture?: T;
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
email?: T;
|
||||
@ -532,6 +545,8 @@ export interface AuthorsSelect<T extends boolean = true> {
|
||||
export interface RepositoriesSelect<T extends boolean = true> {
|
||||
name?: T;
|
||||
abbreviation?: T;
|
||||
image?: T;
|
||||
description?: T;
|
||||
owner?: T;
|
||||
dateOpenToPublic?: T;
|
||||
holdRequests?: T;
|
||||
|
||||
35
src/providers/GlobalProvider.tsx
Normal file
35
src/providers/GlobalProvider.tsx
Normal 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)
|
||||
}
|
||||
35
src/serverActions/GetHoldRequestsOnUserRepo.ts
Normal file
35
src/serverActions/GetHoldRequestsOnUserRepo.ts
Normal 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
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user