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} */
const nextConfig = {
// Your Next.js config here
images: {
domains: ['covers.openlibrary.org'],
},
}
export default withPayload(nextConfig, { devBundleServerPackages: false })

View File

@ -5,6 +5,7 @@ import { fileURLToPath } from 'url'
import config from '@/payload.config'
import BookList from '@/components/BookList'
import UserFeed from '@/components/Feed/UserFeed'
import { Book } from '@/payload-types'
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
import { TextShimmer } from '@/components/ui/text-shimmer'
@ -35,26 +36,60 @@ export default async function HomePage() {
return (
<div className="home">
<div className="py-24 sm:py-32">
<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">
Engage In Our Community Resources
</p>
<div className="relative isolate overflow-hidden py-24 sm:py-32">
<img
alt=""
src="/api/media/file/geniza1.jpg"
className="absolute inset-0 -z-10 size-full object-cover"
/>
<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
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)]"
>
Welcome
Welcome&nbsp;
</TextShimmer>
{user && <small>{`user.firstName`}</small>}
{user && <small>{user.firstName}</small>}
</h2>
<p className="mt-8 text-lg font-medium text-pretty text-muted-foreground sm:text-xl/8">
Anim aute id magna aliqua ad ad non deserunt sunt. Qui irure qui lorem cupidatat
commodo. Elit sunt amet fugiat veniam occaecat fugiat.
<div className="mt-8 text-lg font-light text-pretty text-foreground text-shadow-lg shadow-background sm:text-xl/8">
<p className="indent-5 italic">
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>
<small className="block text-right">- ibn Tibbon</small>
</div>
</div>
</div>
</div>
@ -66,9 +101,7 @@ export default async function HomePage() {
<TabsTrigger value="search">Search</TabsTrigger>
</TabsList>
<TabsContent value="feed">
{initBrowseBooks && <BookList books={initBrowseBooks} />}
</TabsContent>
<TabsContent value="feed">{user && <UserFeed user={user} />}</TabsContent>
<TabsContent value="browse">
{initBrowseBooks && <BookList books={initBrowseBooks} />}
@ -76,13 +109,6 @@ export default async function HomePage() {
<TabsContent value="search">Search</TabsContent>
</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>
)
}

View File

@ -1,5 +1,5 @@
'use server'
import { headers as nextHeaders } from 'next/headers'
import { getPayload } from "payload"
import configPromise from '@payload-config'
@ -10,11 +10,19 @@ type Props = {
const requestHold = async (props: Props) => {
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({
collection: 'holdRequests',
data: {
repository: props.repositoryId,
book: props.bookId,
userRequested: authResponse.user.id,
dateRequested: new Date().toUTCString(),
},
})

View File

@ -14,6 +14,14 @@ export const Users: CollectionConfig = {
type: 'select',
options: ['admin', 'user'],
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 {
id: number;
role?: ('admin' | 'user') | null;
firstName?: string | null;
lastName?: string | null;
updatedAt: string;
createdAt: string;
email: string;
@ -458,6 +460,8 @@ export interface PayloadMigration {
*/
export interface UsersSelect<T extends boolean = true> {
role?: T;
firstName?: T;
lastName?: T;
updatedAt?: T;
createdAt?: T;
email?: T;