Skip to content

Commit

Permalink
Merge pull request #1 from jonaschlegel/add-illustration-resources-page
Browse files Browse the repository at this point in the history
Add literature page and resources for illustration literature
  • Loading branch information
jonaschlegel authored Nov 11, 2024
2 parents fea109c + d6edb1c commit d0f638a
Show file tree
Hide file tree
Showing 106 changed files with 5,451 additions and 834 deletions.
59 changes: 59 additions & 0 deletions app/literature/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import LiteratureListLayout from '@/components/LiteratureListLayout'
import { genPageMetadata } from 'app/seo'
import fs from 'fs'
import matter from 'gray-matter'
import path from 'path'

export const metadata = genPageMetadata({ title: 'Literature Resources' })

const getLiteratureData = () => {
const dirPath = path.join(process.cwd(), 'data/resources/illustrations')
const files = fs.readdirSync(dirPath)
return files.map((filename) => {
const markdownWithMeta = fs.readFileSync(path.join(dirPath, filename), 'utf-8')
const { data, content } = matter(markdownWithMeta)

return {
title: data.title,
authors: data.authors,
year: data.year,
publisher: data.publisher,
externalLink: data.externalLink || '',
reviewsLink: data.reviewsLink || '',
literatureType: data.type,
category: data.category,
tags: data.tags,
isbn: data.isbn || '',
doi: data.doi || '',
coverImage: data.coverImage,
abstract: content.match(/## Abstract\s([\s\S]*?)##/)?.[1].trim() || 'No abstract available.',
tableOfContents:
content.match(/## Table of Contents\s([\s\S]*?)##/)?.[1].trim() ||
'No table of contents available.',
hidden: data.hidden || false,
purposeAndAudience:
content.match(/## Purpose and Audience\s([\s\S]*?)##/)?.[1].trim() ||
'No information available.',
reviews: content.match(/## Reviews\s([\s\S]*?)##/)?.[1].trim() || 'No reviews available.',
keyExcerpt:
content.match(/## Key Excerpt\s([\s\S]*?)##/)?.[1].trim() || 'No key excerpt available.',
}
})
}

export default function Literature() {
const literatureData = getLiteratureData()

return (
<div className="pb-8 pt-6">
<h1 className="mb-6 text-4xl font-bold">Archaeological Illustration Resources</h1>
<div className="mb-6">
A collection of literature resources related to archaeological illustration, including
books, articles, and other publications. This list is a work in progress and will be updated
as new resources are discovered. If you have a resource you would like to add to this list,
please contact me.
</div>
<LiteratureListLayout initialLiteratureData={literatureData} />
</div>
)
}
2 changes: 1 addition & 1 deletion app/tag-data.json
Original file line number Diff line number Diff line change
@@ -1 +1 @@
{"archink":2,"inktober":2,"archaeological-illustration":1,"creative-challenge":1,"learn-in-public":1,"learning":1,"public":1,"illustration":2,"archaeology":2,"archaeological-illustratioon":2,"ipad":1,"digital":1,"digital-drawing":1,"definition":1,"scientific-communication":1,"scicomm":1,"scicom":2,"science-communication":2,"archaeoink":1}
{"visual-storytelling":1,"conceptual-illustration":2,"women-archaeologists":1,"ann-axtell-morris":1,"archaeological-illustration":8,"public-archaeology":1,"visual-communication":2,"science-communication":5,"social-media-engagement":1,"archink":2,"inktober":3,"creative-challenge":1,"archaeological-communication":1,"stippling-in-archaeology":1,"drawing-techniques":1,"material-culture-documentation":1,"visual-archaeology":1,"100daysofdrawing":1,"skill-development":1,"observational-practice":1,"artefact-engagement":1,"polychromy":1,"uncertainty":1,"reconstruction":1,"ancient-greek-art":1,"illustration":3,"skin-colour":1,"eurocentric-bias":1,"learn-in-public":1,"learning":1,"public":1,"archaeology":2,"archaeological-illustratioon":2,"ipad":1,"digital":1,"digital-drawing":1,"definition":1,"scientific-communication":1,"scicomm":1,"scicom":2,"archaeoink":1}
2 changes: 1 addition & 1 deletion components/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ const Header = () => {
<Link
key={link.title}
href={link.href}
className="hidden font-medium text-gray-900 dark:text-gray-100 sm:block"
className="hidden font-medium text-gray-900 dark:text-gray-100 lg:block"
>
{link.title}
</Link>
Expand Down
7 changes: 5 additions & 2 deletions components/Image.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import NextImage, { ImageProps } from 'next/image'
import NextImage from 'next/image'
import { ComponentProps } from 'react'

const Image = ({ ...rest }: ImageProps) => <NextImage {...rest} />
type NextImageProps = ComponentProps<typeof NextImage>

const Image = ({ ...rest }: NextImageProps) => <NextImage {...rest} />

export default Image
28 changes: 22 additions & 6 deletions components/Link.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,37 @@
import type { LinkProps } from 'next/link'
/* eslint-disable jsx-a11y/anchor-has-content */
import Link from 'next/link'
import { AnchorHTMLAttributes } from 'react'

const CustomLink = ({ href, ...rest }: LinkProps & AnchorHTMLAttributes<HTMLAnchorElement>) => {
type LinkProps = Parameters<typeof Link>[0]

const CustomLink = ({
href,
children,
...rest
}: LinkProps & AnchorHTMLAttributes<HTMLAnchorElement>) => {
const isInternalLink = href && href.startsWith('/')
const isAnchorLink = href && href.startsWith('#')

if (isInternalLink) {
return <Link href={href} {...rest} />
return (
<Link href={href} {...rest}>
{children}
</Link>
)
}

if (isAnchorLink) {
return <a href={href} {...rest} />
return (
<a href={href} {...rest}>
{children}
</a>
)
}

return <a target="_blank" rel="noopener noreferrer" href={href} {...rest} />
return (
<a target="_blank" rel="noopener noreferrer" href={href} {...rest}>
{children}
</a>
)
}

export default CustomLink
163 changes: 163 additions & 0 deletions components/LiteratureCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
import { startTransition, useState } from 'react'
import Image from './Image'
import LiteratureTag from './LiteratureTag'

interface Author {
lastName: string
firstName: string
}

interface LiteratureProps {
title: string
authors?: Author[]
year: string
publisher: string
externalLink?: string
reviewsLink?: string
literatureType: string
category: string
tags: string[]
isbn?: string
doi?: string
abstract: string
tableOfContents: string
coverImage?: string
hidden?: boolean
purposeAndAudience: string
reviews: string
keyExcerpt: string
onTagClick: (tag: string) => void
}

const LiteratureCard = ({
title,
authors = [],
year,
publisher,
externalLink,
reviewsLink,
literatureType,
category,
tags,
isbn,
doi,
abstract,
tableOfContents,
coverImage,
hidden,
purposeAndAudience,
reviews,
keyExcerpt,
onTagClick,
}: LiteratureProps) => {
const [isExpanded, setIsExpanded] = useState(false)

if (hidden) return null

const formattedAuthors = authors.length
? authors.map((author) => `${author.firstName} ${author.lastName}`).join(', ')
: 'No authors listed'

const toggleExpand = () => setIsExpanded(!isExpanded)

return (
<div
role="button"
tabIndex={0}
className={`flex w-full cursor-pointer rounded-md border-2 border-gray-200 border-opacity-60 p-4 shadow-lg transition-all duration-300 hover:shadow-xl dark:border-gray-700 ${
isExpanded ? 'max-h-[600px]' : 'max-h-[180px]'
}`}
onClick={toggleExpand}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
toggleExpand()
}
}}
>
{/* Cover Image Section */}
{coverImage ? (
<Image
src={coverImage}
alt={`Cover of ${title}`}
width={96}
height={128}
className="mr-4 rounded-md object-cover"
/>
) : (
<div className="mr-4 h-32 w-24 rounded-md bg-gray-200"></div>
)}

{/* Content Section */}
<div className="flex-1 space-y-2 overflow-hidden transition-all duration-300">
<div className="flex items-start justify-between">
<div>
<h3 className="mb-2 text-2xl font-bold leading-8 tracking-tight dark:text-white">
{title}
</h3>
<p className="text-md text-gray-500 dark:text-gray-400">
By {formattedAuthors}{year}
</p>
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400">{publisher}</p>
</div>
</div>

{/* Tags Section */}
<div className="mt-2 flex flex-wrap">
{tags.map((tag) => (
<LiteratureTag key={tag} text={tag} onClick={onTagClick} />
))}
</div>

{/* Expandable section */}
{isExpanded && (
<div className="mt-4 max-h-[300px] space-y-4 overflow-y-auto text-gray-700 dark:text-gray-300">
<div>
<h4 className="font-semibold">Abstract</h4>
<p className="text-sm">{abstract}</p>
</div>
<div>
<h4 className="font-semibold">Purpose and Audience</h4>
<p className="text-sm">{purposeAndAudience}</p>
</div>
{/* <div>
<h4 className="font-semibold">Table of Contents</h4>
<p className="text-sm">{tableOfContents}</p>
</div> */}
<div>
<h4 className="font-semibold">Key Excerpt</h4>
<p className="text-sm">{keyExcerpt}</p>
</div>
<div>
<h4 className="font-semibold">Reviews</h4>
<p className="text-sm">{reviews}</p>
{reviewsLink && (
<a
href={reviewsLink}
className="text-sm hover:text-primary-600 dark:hover:text-primary-400"
target="_blank"
rel="noopener noreferrer"
>
—Read futher Reviews
</a>
)}
</div>
<div className="mt-2 flex space-x-4 text-sm">
{externalLink && (
<a
href={externalLink}
className="hover:text-primary-600 dark:hover:text-primary-400"
target="_blank"
rel="noopener noreferrer"
>
Get it here
</a>
)}
</div>
</div>
)}
</div>
</div>
)
}

export default LiteratureCard
78 changes: 78 additions & 0 deletions components/LiteratureFilter.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
'use client'

import { useState, useMemo } from 'react'
import LiteratureList from './LiteratureList'
import SearchBar from './SearchBar'
import TagFilter from './TagFilter'
import { LiteratureData } from '../types/types'

interface LiteratureFilterProps {
initialLiteratureData: LiteratureData[]
}

const LiteratureFilter = ({ initialLiteratureData }: LiteratureFilterProps) => {
const [searchQuery, setSearchQuery] = useState('')
const [selectedTags, setSelectedTags] = useState<string[]>([])
const [displayLimit, setDisplayLimit] = useState(10)

const handleSearch = (query: string) => setSearchQuery(query)
const handleTagChange = (tags: string[]) => setSelectedTags(tags)

const onTagClick = (tag: string) => {
setSelectedTags((prevTags) =>
prevTags.includes(tag) ? prevTags.filter((t) => t !== tag) : [...prevTags, tag]
)
}

const filteredLiteratureData = useMemo(() => {
return initialLiteratureData.filter((item) => {
const matchesSearch =
item.title.toLowerCase().includes(searchQuery.toLowerCase()) ||
item.authors.some((author) =>
`${author.firstName} ${author.lastName}`.toLowerCase().includes(searchQuery.toLowerCase())
)
const matchesTags =
selectedTags.length === 0 || selectedTags.every((tag) => item.tags.includes(tag))
return matchesSearch && matchesTags
})
}, [initialLiteratureData, searchQuery, selectedTags])

const allTags = Array.from(new Set(initialLiteratureData.flatMap((item) => item.tags)))

const displayedLiteratureData = filteredLiteratureData.slice(0, displayLimit)

const handleLoadMore = () => {
setDisplayLimit((prevLimit) => prevLimit + 10)
}

return (
<div className="flex space-x-6">
<div className="w-1/4 rounded-lg bg-gray-100 p-4 shadow-md">
<h2 className="mb-4 text-lg font-semibold">Filter by Keywords</h2>
<TagFilter
availableTags={allTags}
selectedTags={selectedTags}
onTagChange={handleTagChange}
/>
</div>

<div className="w-3/4 space-y-5">
<SearchBar onSearch={handleSearch} />
<LiteratureList literatureData={displayedLiteratureData} onTagClick={onTagClick} />

{displayLimit < filteredLiteratureData.length && (
<div className="mt-4 flex">
<button
onClick={handleLoadMore}
className="rounded-lg bg-primary-500 px-4 py-2 text-white hover:bg-primary-600"
>
Load More
</button>
</div>
)}
</div>
</div>
)
}

export default LiteratureFilter
Loading

0 comments on commit d0f638a

Please sign in to comment.