feat: added book list preview for home page

This commit is contained in:
Yehoshua Sandler 2025-04-16 10:35:47 -05:00
parent cad467d8f4
commit d02a07541b
10 changed files with 145 additions and 168 deletions

View File

@ -1,5 +1,4 @@
import React from 'react' import React from 'react'
import './styles.css'
export const metadata = { export const metadata = {
description: 'A blank template using Payload in a Next.js app.', description: 'A blank template using Payload in a Next.js app.',
@ -12,7 +11,7 @@ export default async function RootLayout(props: { children: React.ReactNode }) {
return ( return (
<html lang="en"> <html lang="en">
<body> <body>
<main>{children}</main> <main className="bg-white dark:bg-gray-800">{children}</main>
</body> </body>
</html> </html>
) )

View File

@ -5,7 +5,8 @@ import React from 'react'
import { fileURLToPath } from 'url' import { fileURLToPath } from 'url'
import config from '@/payload.config' import config from '@/payload.config'
import './styles.css' import BookList from '@/components/BookList'
import { Book } from '@/payload-types'
export default async function HomePage() { export default async function HomePage() {
const headers = await getHeaders() const headers = await getHeaders()
@ -15,6 +16,21 @@ export default async function HomePage() {
const fileURL = `vscode://file/${fileURLToPath(import.meta.url)}` const fileURL = `vscode://file/${fileURLToPath(import.meta.url)}`
const books = await payload.find({
collection: 'books',
depth: 2,
limit: 12,
overrideAccess: false,
select: {
title: true,
authors: true,
publication: true,
lcc: true,
genre: true,
isbn: true,
},
})
return ( return (
<div className="home"> <div className="home">
<div className="content"> <div className="content">
@ -48,6 +64,7 @@ export default async function HomePage() {
</a> </a>
</div> </div>
</div> </div>
<BookList books={(books.docs || []) as Book[]} />
<div className="footer"> <div className="footer">
<p>Update this page by editing</p> <p>Update this page by editing</p>
<a className="codeLink" href={fileURL}> <a className="codeLink" href={fileURL}>

View File

@ -1,164 +0,0 @@
:root {
--font-mono: 'Roboto Mono', monospace;
}
* {
box-sizing: border-box;
}
html {
font-size: 18px;
line-height: 32px;
background: rgb(0, 0, 0);
-webkit-font-smoothing: antialiased;
}
html,
body,
#app {
height: 100%;
}
body {
font-family: system-ui;
font-size: 18px;
line-height: 32px;
margin: 0;
color: rgb(1000, 1000, 1000);
@media (max-width: 1024px) {
font-size: 15px;
line-height: 24px;
}
}
img {
max-width: 100%;
height: auto;
display: block;
}
h1 {
margin: 40px 0;
font-size: 64px;
line-height: 70px;
font-weight: bold;
@media (max-width: 1024px) {
margin: 24px 0;
font-size: 42px;
line-height: 42px;
}
@media (max-width: 768px) {
font-size: 38px;
line-height: 38px;
}
@media (max-width: 400px) {
font-size: 32px;
line-height: 32px;
}
}
p {
margin: 24px 0;
@media (max-width: 1024px) {
margin: calc(var(--base) * 0.75) 0;
}
}
a {
color: currentColor;
&:focus {
opacity: 0.8;
outline: none;
}
&:active {
opacity: 0.7;
outline: none;
}
}
svg {
vertical-align: middle;
}
.home {
display: flex;
flex-direction: column;
justify-content: space-between;
align-items: center;
height: 100vh;
padding: 45px;
max-width: 1024px;
margin: 0 auto;
overflow: hidden;
@media (max-width: 400px) {
padding: 24px;
}
.content {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
flex-grow: 1;
h1 {
text-align: center;
}
}
.links {
display: flex;
align-items: center;
gap: 12px;
a {
text-decoration: none;
padding: 0.25rem 0.5rem;
border-radius: 4px;
}
.admin {
color: rgb(0, 0, 0);
background: rgb(1000, 1000, 1000);
border: 1px solid rgb(0, 0, 0);
}
.docs {
color: rgb(1000, 1000, 1000);
background: rgb(0, 0, 0);
border: 1px solid rgb(1000, 1000, 1000);
}
}
.footer {
display: flex;
align-items: center;
gap: 8px;
@media (max-width: 1024px) {
flex-direction: column;
gap: 6px;
}
p {
margin: 0;
}
.codeLink {
text-decoration: none;
padding: 0 0.5rem;
background: rgb(60, 60, 60);
border-radius: 4px;
}
}
}

5
src/app/globals.css Normal file
View File

@ -0,0 +1,5 @@
@import "tailwindcss";
@tailwind base;
@tailwind components;
@tailwind utilities;

17
src/app/layout.tsx Normal file
View File

@ -0,0 +1,17 @@
import { ReactNode } from 'react'
type LayoutProps = {
children: ReactNode
}
import './globals.css'
const Layout = ({ children }: LayoutProps) => {
return (
<html>
<body>{children}</body>
</html>
)
}
export default Layout

View File

@ -1,4 +1,3 @@
import { access } from 'node:fs/promises'
import type { CollectionConfig } from 'payload' import type { CollectionConfig } from 'payload'
export const Authors: CollectionConfig = { export const Authors: CollectionConfig = {

View File

@ -40,6 +40,14 @@ export const Books: CollectionConfig = {
name: 'lcc', name: 'lcc',
type: 'text', type: 'text',
}, },
{
name: 'isbn',
type: 'text',
},
{
name: 'asin',
type: 'text',
},
{ {
name: 'publication', name: 'publication',
type: 'text', type: 'text',
@ -54,6 +62,7 @@ export const Books: CollectionConfig = {
type: 'relationship', type: 'relationship',
relationTo: 'genre', relationTo: 'genre',
hasMany: true, hasMany: true,
maxDepth: 3,
admin: { admin: {
allowEdit: true, allowEdit: true,
allowCreate: true allowCreate: true

View File

@ -0,0 +1,84 @@
import { Badge } from '@/components/badge'
import { Author, Book, Genre } from '@/payload-types'
import { Avatar } from '../avatar'
type Props = {
books: Book[]
}
const makeAuthorsLabel = (book: Book) => {
const authors = book.authors as Author[] // TODO: endure this type safety
const translators = authors?.filter((a) => a.role === 'Translator').map((t) => t.lf)
const editors = authors?.filter((a) => a.role === 'Editor').map((e) => e.lf)
const actualAuthors = authors
?.filter((a) => !editors.includes(a.lf) && !translators.includes(a.lf))
.map((a) => a.lf)
return (
<ul>
<li>{actualAuthors.join(', ')}</li>
{!!translators?.length && <li>Translators: {translators.join(', ')}</li>}
{!!editors?.length && <li>Editors: {editors.join(', ')}</li>}
</ul>
)
}
const makeGenreBadges = (book: Book) => {
return (book.genre as Genre[])?.map((g) => (
<Badge key={g.name + book.title + book.id} className="ml-0.5">
{g.name}
</Badge>
))
}
export default function BookList(props: Props) {
const { books } = props
return (
<section id="user-repositories">
<ul role="list" className="divide-y divide-gray-800">
{books.map((b) => (
<li key={b.lcc + (b.title || '')} className="flex justify-between gap-x-6 py-5">
<div className="flex max-w-9/12 min-w-0 gap-x-4">
<Avatar
square
className="size-12 flex-none bg-gray-800"
src={
b.isbn
? `https://covers.openlibrary.org/b/isbn/${b.isbn}-S.jpg`
: '/images/book-48.svg'
}
/>
<div className="min-w-0 flex-auto">
<p className="text-sm/6 font-semibold text-white">
<span>{b.title}</span>
<small className="ml-1 italic font-thin">{b.lcc}</small>
</p>
<p className="mt-1 truncate text-xs/5 text-gray-400">{makeGenreBadges(b)}</p>
</div>
</div>
<div className="flex flex-col items-end">
<p className="text-sm/6 text-white text-right">{makeAuthorsLabel(b)}</p>
{
/*b.lastBorrowed*/ false ? (
<p className="mt-1 text-xs/5 text-gray-400">
Last Borrowed{' '}
<time dateTime={new Date().toUTCString()}>{new Date().toUTCString()}</time>
</p>
) : (
<div className="mt-1 flex items-center gap-x-1.5">
<div className="flex-none rounded-full bg-emerald-500/20 p-1">
<div className="size-1.5 rounded-full bg-emerald-500" />
</div>
<p className="text-xs/5 text-gray-400">Available</p>
</div>
)
}
</div>
</li>
))}
</ul>
</section>
)
}

View File

@ -5,6 +5,11 @@ export const Header: GlobalConfig = {
slug: 'header', slug: 'header',
label: 'Header Nav', label: 'Header Nav',
fields: [ fields: [
{
name: 'logo',
type: 'relationship',
relationTo: 'media',
},
{ {
name: 'headerLinks', name: 'headerLinks',
type: 'array', type: 'array',

View File

@ -181,6 +181,8 @@ export interface Book {
authors?: (number | Author)[] | null; authors?: (number | Author)[] | null;
pages?: number | null; pages?: number | null;
lcc?: string | null; lcc?: string | null;
isbn?: string | null;
asin?: string | null;
publication?: string | null; publication?: string | null;
date?: string | null; date?: string | null;
genre?: (number | Genre)[] | null; genre?: (number | Genre)[] | null;
@ -420,6 +422,8 @@ export interface BooksSelect<T extends boolean = true> {
authors?: T; authors?: T;
pages?: T; pages?: T;
lcc?: T; lcc?: T;
isbn?: T;
asin?: T;
publication?: T; publication?: T;
date?: T; date?: T;
genre?: T; genre?: T;
@ -535,6 +539,7 @@ export interface PayloadMigrationsSelect<T extends boolean = true> {
*/ */
export interface Header { export interface Header {
id: number; id: number;
logo?: (number | null) | Media;
headerLinks?: headerLinks?:
| { | {
label?: string | null; label?: string | null;
@ -551,6 +556,7 @@ export interface Header {
* via the `definition` "header_select". * via the `definition` "header_select".
*/ */
export interface HeaderSelect<T extends boolean = true> { export interface HeaderSelect<T extends boolean = true> {
logo?: T;
headerLinks?: headerLinks?:
| T | T
| { | {