feat: initial user feed, and some page ui
This commit is contained in:
parent
06573d9044
commit
f23bc1976f
43
components/motion-primitives/border-trail.tsx
Normal file
43
components/motion-primitives/border-trail.tsx
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
'use client';
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
import { motion, Transition } from 'motion/react';
|
||||||
|
|
||||||
|
export type BorderTrailProps = {
|
||||||
|
className?: string;
|
||||||
|
size?: number;
|
||||||
|
transition?: Transition;
|
||||||
|
onAnimationComplete?: () => void;
|
||||||
|
style?: React.CSSProperties;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function BorderTrail({
|
||||||
|
className,
|
||||||
|
size = 60,
|
||||||
|
transition,
|
||||||
|
onAnimationComplete,
|
||||||
|
style,
|
||||||
|
}: BorderTrailProps) {
|
||||||
|
const defaultTransition: Transition = {
|
||||||
|
repeat: Infinity,
|
||||||
|
duration: 5,
|
||||||
|
ease: 'linear',
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='pointer-events-none absolute inset-0 rounded-[inherit] border border-transparent [mask-clip:padding-box,border-box] [mask-composite:intersect] [mask-image:linear-gradient(transparent,transparent),linear-gradient(#000,#000)]'>
|
||||||
|
<motion.div
|
||||||
|
className={cn('absolute aspect-square bg-zinc-500', className)}
|
||||||
|
style={{
|
||||||
|
width: size,
|
||||||
|
offsetPath: `rect(0 auto auto 0 round ${size}px)`,
|
||||||
|
...style,
|
||||||
|
}}
|
||||||
|
animate={{
|
||||||
|
offsetDistance: ['0%', '100%'],
|
||||||
|
}}
|
||||||
|
transition={transition || defaultTransition}
|
||||||
|
onAnimationComplete={onAnimationComplete}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -2,7 +2,9 @@ import { withPayload } from '@payloadcms/next/withPayload'
|
|||||||
|
|
||||||
/** @type {import('next').NextConfig} */
|
/** @type {import('next').NextConfig} */
|
||||||
const nextConfig = {
|
const nextConfig = {
|
||||||
// Your Next.js config here
|
images: {
|
||||||
|
domains: ['covers.openlibrary.org'],
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withPayload(nextConfig, { devBundleServerPackages: false })
|
export default withPayload(nextConfig, { devBundleServerPackages: false })
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import { fileURLToPath } from 'url'
|
|||||||
|
|
||||||
import config from '@/payload.config'
|
import config from '@/payload.config'
|
||||||
import BookList from '@/components/BookList'
|
import BookList from '@/components/BookList'
|
||||||
|
import UserFeed from '@/components/Feed/UserFeed'
|
||||||
import { Book } from '@/payload-types'
|
import { Book } 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'
|
||||||
@ -35,26 +36,60 @@ export default async function HomePage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="home">
|
<div className="home">
|
||||||
<div className="py-24 sm:py-32">
|
<div className="relative isolate overflow-hidden py-24 sm:py-32">
|
||||||
<div className="mx-auto max-w-7xl px-6 lg:px-8">
|
<img
|
||||||
<div className="mx-auto max-w-2xl lg:mx-0">
|
alt=""
|
||||||
<p className="text-base/7 font-semibold text-foreground">
|
src="/api/media/file/geniza1.jpg"
|
||||||
Engage In Our Community Resources
|
className="absolute inset-0 -z-10 size-full object-cover"
|
||||||
</p>
|
/>
|
||||||
|
|
||||||
<h2 className="mt-2 text-5xl font-semibold tracking-tight text-foreground sm:text-7xl">
|
<div
|
||||||
|
aria-hidden="true"
|
||||||
|
className="hidden sm:absolute sm:-top-10 sm:right-1/2 sm:-z-10 sm:mr-10 sm:block sm:transform-gpu sm:blur-3xl"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
clipPath:
|
||||||
|
'polygon(74.1% 44.1%, 100% 61.6%, 97.5% 26.9%, 85.5% 0.1%, 80.7% 2%, 72.5% 32.5%, 60.2% 62.4%, 52.4% 68.1%, 47.5% 58.3%, 45.2% 34.5%, 27.5% 76.7%, 0.1% 64.9%, 17.9% 100%, 27.6% 76.8%, 76.1% 97.7%, 74.1% 44.1%)',
|
||||||
|
}}
|
||||||
|
className="aspect-1097/845 w-[78.5625rem] bg-linear-to-tr from-accent-background to-background opacity-100"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
aria-hidden="true"
|
||||||
|
className="absolute -top-52 left-1/2 -z-10 -translate-x-1/2 transform-gpu blur-3xl sm:top-[-28rem] sm:ml-16 sm:translate-x-0 sm:transform-gpu"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
clipPath:
|
||||||
|
'polygon(74.1% 44.1%, 100% 61.6%, 97.5% 26.9%, 85.5% 0.1%, 80.7% 2%, 72.5% 32.5%, 60.2% 62.4%, 52.4% 68.1%, 47.5% 58.3%, 45.2% 34.5%, 27.5% 76.7%, 0.1% 64.9%, 17.9% 100%, 27.6% 76.8%, 76.1% 97.7%, 74.1% 44.1%)',
|
||||||
|
}}
|
||||||
|
className="aspect-1097/845 w-[78.5625rem] bg-linear-to-tr from-background bg-background"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mx-auto max-w-7xl px-6 lg:px-8 ">
|
||||||
|
<div className="mx-auto max-w-2xl lg:mx-0">
|
||||||
|
<p className="text-base/7 font-semibold text-foreground">Temple Beth-El Beit Midrash</p>
|
||||||
|
|
||||||
|
<h2 className="mt-2 text-5xl font-semibold tracking-tight text-foreground sm:text-7xl text-shadow-lg">
|
||||||
<TextShimmer
|
<TextShimmer
|
||||||
duration={2.2}
|
duration={2.2}
|
||||||
className="[--base-color:var(--color-emerald-700)] [--base-gradient-color:var(--color-white)] dark:[--base-color:var(--color-emerald-600)] dark:[--base-gradient-color:var(--color-white)]"
|
className="[--base-color:var(--color-emerald-700)] [--base-gradient-color:var(--color-white)] dark:[--base-color:var(--color-emerald-600)] dark:[--base-gradient-color:var(--color-white)]"
|
||||||
>
|
>
|
||||||
Welcome
|
Welcome
|
||||||
</TextShimmer>
|
</TextShimmer>
|
||||||
{user && <small>{`user.firstName`}</small>}
|
{user && <small>{user.firstName}</small>}
|
||||||
</h2>
|
</h2>
|
||||||
<p className="mt-8 text-lg font-medium text-pretty text-muted-foreground sm:text-xl/8">
|
<div className="mt-8 text-lg font-light text-pretty text-foreground text-shadow-lg shadow-background sm:text-xl/8">
|
||||||
Anim aute id magna aliqua ad ad non deserunt sunt. Qui irure qui lorem cupidatat
|
<p className="indent-5 italic">
|
||||||
commodo. Elit sunt amet fugiat veniam occaecat fugiat.
|
Never refuse to lend books to anyone who cannot afford to purchase them, but lend
|
||||||
|
books only to those who can be trusted to return them.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
<small className="block text-right">- ibn Tibbon</small>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -66,9 +101,7 @@ export default async function HomePage() {
|
|||||||
<TabsTrigger value="search">Search</TabsTrigger>
|
<TabsTrigger value="search">Search</TabsTrigger>
|
||||||
</TabsList>
|
</TabsList>
|
||||||
|
|
||||||
<TabsContent value="feed">
|
<TabsContent value="feed">{user && <UserFeed user={user} />}</TabsContent>
|
||||||
{initBrowseBooks && <BookList books={initBrowseBooks} />}
|
|
||||||
</TabsContent>
|
|
||||||
|
|
||||||
<TabsContent value="browse">
|
<TabsContent value="browse">
|
||||||
{initBrowseBooks && <BookList books={initBrowseBooks} />}
|
{initBrowseBooks && <BookList books={initBrowseBooks} />}
|
||||||
@ -76,13 +109,6 @@ export default async function HomePage() {
|
|||||||
|
|
||||||
<TabsContent value="search">Search</TabsContent>
|
<TabsContent value="search">Search</TabsContent>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
|
||||||
<div className="footer">
|
|
||||||
<p>Update this page by editing</p>
|
|
||||||
<a className="codeLink" href={fileURL}>
|
|
||||||
<code>app/(frontend)/page.tsx</code>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
'use server'
|
'use server'
|
||||||
|
import { headers as nextHeaders } from 'next/headers'
|
||||||
import { getPayload } from "payload"
|
import { getPayload } from "payload"
|
||||||
import configPromise from '@payload-config'
|
import configPromise from '@payload-config'
|
||||||
|
|
||||||
@ -10,11 +10,19 @@ type Props = {
|
|||||||
const requestHold = async (props: Props) => {
|
const requestHold = async (props: Props) => {
|
||||||
const payload = await getPayload({ config: configPromise })
|
const payload = await getPayload({ config: configPromise })
|
||||||
|
|
||||||
|
const headers = await nextHeaders()
|
||||||
|
const authResponse = await payload.auth({ headers })
|
||||||
|
console.log(authResponse.user);
|
||||||
|
|
||||||
|
if (!authResponse.user?.id) return
|
||||||
|
|
||||||
const requestHoldResponse = await payload.create({
|
const requestHoldResponse = await payload.create({
|
||||||
collection: 'holdRequests',
|
collection: 'holdRequests',
|
||||||
data: {
|
data: {
|
||||||
repository: props.repositoryId,
|
repository: props.repositoryId,
|
||||||
book: props.bookId,
|
book: props.bookId,
|
||||||
|
userRequested: authResponse.user.id,
|
||||||
|
dateRequested: new Date().toUTCString(),
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@ -14,6 +14,14 @@ export const Users: CollectionConfig = {
|
|||||||
type: 'select',
|
type: 'select',
|
||||||
options: ['admin', 'user'],
|
options: ['admin', 'user'],
|
||||||
saveToJWT: true
|
saveToJWT: true
|
||||||
}
|
},
|
||||||
|
{
|
||||||
|
name: 'firstName',
|
||||||
|
type: 'text',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'lastName',
|
||||||
|
type: 'text',
|
||||||
|
},
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|||||||
121
src/components/Feed/UserFeed.tsx
Normal file
121
src/components/Feed/UserFeed.tsx
Normal file
@ -0,0 +1,121 @@
|
|||||||
|
import type { Book, Copy, HoldRequest, Repository, User } from '@/payload-types'
|
||||||
|
import { getPayload, PaginatedDocs } from 'payload'
|
||||||
|
import config from '@/payload.config'
|
||||||
|
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '../ui/card'
|
||||||
|
import Image from 'next/image'
|
||||||
|
import { BorderTrail } from 'components/motion-primitives/border-trail'
|
||||||
|
import clsx from 'clsx'
|
||||||
|
|
||||||
|
const stats = [
|
||||||
|
{ name: 'Outbound Loans', stat: '13' },
|
||||||
|
{ name: 'Active Holds', stat: '6' },
|
||||||
|
{ name: 'Hold Requests', stat: '3', shouldHighlight: true },
|
||||||
|
]
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
user?: User
|
||||||
|
}
|
||||||
|
const UserFeed = async (props: Props) => {
|
||||||
|
const { user } = props
|
||||||
|
const isLoggedIn = !!user
|
||||||
|
|
||||||
|
const payloadConfig = await config
|
||||||
|
const payload = await getPayload({ config: payloadConfig })
|
||||||
|
|
||||||
|
const holdRequests = (await payload.find({
|
||||||
|
collection: 'holdRequests',
|
||||||
|
limit: 6,
|
||||||
|
depth: 3,
|
||||||
|
select: {
|
||||||
|
copy: true,
|
||||||
|
dateRequested: true,
|
||||||
|
repository: true,
|
||||||
|
book: true,
|
||||||
|
},
|
||||||
|
where: {
|
||||||
|
userRequested: {
|
||||||
|
equals: user?.id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})) as PaginatedDocs<HoldRequest>
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section>
|
||||||
|
<div className="my-6">
|
||||||
|
<h3 className="text-lg font-semibold text-foreground">Outbound Activity</h3>
|
||||||
|
<dl className="mt-5 grid grid-cols-1 gap-5 sm:grid-cols-3">
|
||||||
|
{stats.map((item) => (
|
||||||
|
<div
|
||||||
|
key={item.name}
|
||||||
|
className={clsx(
|
||||||
|
'relative overflow-hidden rounded-lg accent-background px-4 py-5 shadow-sm sm:p-6',
|
||||||
|
!item.shouldHighlight ? 'border-1 border-muted-foreground' : '',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{!!item.shouldHighlight && (
|
||||||
|
<BorderTrail
|
||||||
|
style={{
|
||||||
|
boxShadow:
|
||||||
|
'0px 0px 60px 30px rgb(200 255 200 / 50%), 0 0 100px 60px rgb(0 0 0 / 50%), 0 0 140px 90px rgb(0 0 0 / 50%)',
|
||||||
|
}}
|
||||||
|
size={100}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<dt className="truncate text-sm font-medium text-muted-foreground">{item.name}</dt>
|
||||||
|
<dd className="mt-1 text-3xl font-semibold tracking-tight text-foreground">
|
||||||
|
{item.stat}
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</dl>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="my-6">
|
||||||
|
<h2 className="text-lg font-semibold text-foreground">Inbound Activity</h2>
|
||||||
|
<div className="my-3">
|
||||||
|
<h3 className="text-base font-semibold text-muted-foreground">Your Holds</h3>
|
||||||
|
<ul className="flex gap-2">
|
||||||
|
{holdRequests.docs?.map((h) => {
|
||||||
|
const book = h.book as Book
|
||||||
|
const repository = h.repository as Repository
|
||||||
|
|
||||||
|
const formatedDateRequested = h.dateRequested
|
||||||
|
? new Date(h.dateRequested).toDateString()
|
||||||
|
: ''
|
||||||
|
|
||||||
|
return (
|
||||||
|
<li className="inline-block" key={book.isbn}>
|
||||||
|
<Card className="w-48">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>{book.title}</CardTitle>
|
||||||
|
<CardDescription>{repository.abbreviation}</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<Image
|
||||||
|
alt="book cover"
|
||||||
|
className="mx-auto w-full"
|
||||||
|
width={120}
|
||||||
|
height={180}
|
||||||
|
src={
|
||||||
|
book.isbn
|
||||||
|
? `https://covers.openlibrary.org/b/isbn/${book.isbn}-M.jpg`
|
||||||
|
: '/images/book-48.svg'
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</CardContent>
|
||||||
|
<CardFooter className="text-muted-foreground text-sm block">
|
||||||
|
<span className="block w-full">Requested:</span>
|
||||||
|
<span className="block w-full">{formatedDateRequested}</span>
|
||||||
|
</CardFooter>
|
||||||
|
</Card>
|
||||||
|
</li>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default UserFeed
|
||||||
78
src/components/ui/card.tsx
Normal file
78
src/components/ui/card.tsx
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
import * as React from 'react'
|
||||||
|
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
|
function Card({ className, ...props }: React.ComponentProps<'div'>) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="card"
|
||||||
|
className={cn(
|
||||||
|
'bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm',
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CardHeader({ className, ...props }: React.ComponentProps<'div'>) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="card-header"
|
||||||
|
className={cn(
|
||||||
|
'@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6',
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CardTitle({ className, ...props }: React.ComponentProps<'div'>) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="card-title"
|
||||||
|
className={cn(
|
||||||
|
'leading-none font-semibold text-ellipsis overflow-hidden line-clamp-1',
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CardDescription({ className, ...props }: React.ComponentProps<'div'>) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="card-description"
|
||||||
|
className={cn('text-muted-foreground text-sm', className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CardAction({ className, ...props }: React.ComponentProps<'div'>) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="card-action"
|
||||||
|
className={cn('col-start-2 row-span-2 row-start-1 self-start justify-self-end', className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CardContent({ className, ...props }: React.ComponentProps<'div'>) {
|
||||||
|
return <div data-slot="card-content" className={cn('px-6', className)} {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
function CardFooter({ className, ...props }: React.ComponentProps<'div'>) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="card-footer"
|
||||||
|
className={cn('flex items-center px-6 [.border-t]:pt-6', className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Card, CardHeader, CardFooter, CardTitle, CardAction, CardDescription, CardContent }
|
||||||
@ -156,6 +156,8 @@ export interface UserAuthOperations {
|
|||||||
export interface User {
|
export interface User {
|
||||||
id: number;
|
id: number;
|
||||||
role?: ('admin' | 'user') | null;
|
role?: ('admin' | 'user') | null;
|
||||||
|
firstName?: string | null;
|
||||||
|
lastName?: string | null;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
email: string;
|
email: string;
|
||||||
@ -458,6 +460,8 @@ export interface PayloadMigration {
|
|||||||
*/
|
*/
|
||||||
export interface UsersSelect<T extends boolean = true> {
|
export interface UsersSelect<T extends boolean = true> {
|
||||||
role?: T;
|
role?: T;
|
||||||
|
firstName?: T;
|
||||||
|
lastName?: T;
|
||||||
updatedAt?: T;
|
updatedAt?: T;
|
||||||
createdAt?: T;
|
createdAt?: T;
|
||||||
email?: T;
|
email?: T;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user