feat: initial user feed, and some page ui

This commit is contained in:
Yehoshua Sandler 2025-04-22 13:14:04 -05:00
parent 06573d9044
commit f23bc1976f
8 changed files with 316 additions and 26 deletions

View 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>
);
}

View File

@ -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 })

View File

@ -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&nbsp;
</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>
) )
} }

View File

@ -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(),
}, },
}) })

View File

@ -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',
},
], ],
} }

View 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

View 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 }

View File

@ -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;