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} */
|
||||
const nextConfig = {
|
||||
// Your Next.js config here
|
||||
images: {
|
||||
domains: ['covers.openlibrary.org'],
|
||||
},
|
||||
}
|
||||
|
||||
export default withPayload(nextConfig, { devBundleServerPackages: false })
|
||||
|
@ -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
|
||||
</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.
|
||||
</p>
|
||||
<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>
|
||||
)
|
||||
}
|
||||
|
@ -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(),
|
||||
},
|
||||
})
|
||||
|
||||
|
@ -14,6 +14,14 @@ export const Users: CollectionConfig = {
|
||||
type: 'select',
|
||||
options: ['admin', 'user'],
|
||||
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 {
|
||||
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;
|
||||
|
Loading…
x
Reference in New Issue
Block a user