diff --git a/messages/de.json b/messages/de.json
index 10194b2..8ebd4bc 100644
--- a/messages/de.json
+++ b/messages/de.json
@@ -50,7 +50,11 @@
"back": "Alle Einträge von {project}",
"details": "Angaben",
"readingTime": "{minutes} Min. Lesezeit",
- "emptyTitle": "Noch kein Inhalt"
+ "emptyTitle": "Noch kein Inhalt",
+ "previous": "Vorheriger Eintrag",
+ "next": "Nächster Eintrag",
+ "nearby": "Weiter im Projekt",
+ "more": "Mehr aus {project}"
},
"block": {
"before": "Vorher",
@@ -675,7 +679,9 @@
},
"publishTitle": "Veröffentlichen",
"publishHint": "Kein Zugang darf veröffentlichen. Das bleibt an Konten gebunden."
- }
+ },
+ "recentEntries": "Zuletzt bearbeitet",
+ "allEntries": "Alle Beiträge"
},
"filter": {
"type": "Art",
diff --git a/messages/en.json b/messages/en.json
index 68b9836..daf65ec 100644
--- a/messages/en.json
+++ b/messages/en.json
@@ -50,7 +50,11 @@
"back": "All entries of {project}",
"details": "Details",
"readingTime": "{minutes} min read",
- "emptyTitle": "No content yet"
+ "emptyTitle": "No content yet",
+ "previous": "Previous entry",
+ "next": "Next entry",
+ "nearby": "More in this project",
+ "more": "More from {project}"
},
"block": {
"before": "Before",
@@ -675,7 +679,9 @@
},
"publishTitle": "Publishing",
"publishHint": "No access may publish. That stays bound to accounts."
- }
+ },
+ "recentEntries": "Recently edited",
+ "allEntries": "All entries"
},
"filter": {
"type": "Type",
diff --git a/src/app/(site)/[project]/[entry]/page.tsx b/src/app/(site)/[project]/[entry]/page.tsx
index 8591a9f..85c716c 100644
--- a/src/app/(site)/[project]/[entry]/page.tsx
+++ b/src/app/(site)/[project]/[entry]/page.tsx
@@ -6,6 +6,7 @@ import { ProjectArchive } from '~/components/archive/ProjectArchive'
import {
findPostWithBlocks,
listArchiveMonths,
+ loadPostNeighbours,
listPostTypeCounts,
listPostsForArchive,
loadArchiveStats,
@@ -60,7 +61,17 @@ export default async function EntryPage({ params, searchParams }: Props) {
notFound()
}
- return
+ const neighbours = await loadPostNeighbours({
+ scope,
+ projectSlug: detail.project.slug,
+ postId: detail.post.id,
+ publishAt: detail.post.publishAt,
+ limit: coverCount,
+ })
+
+ const nearbyCovers = await loadPostCovers(neighbours.more.map(item => item.id))
+
+ return
}
const query = await searchParams
diff --git a/src/app/(site)/page.tsx b/src/app/(site)/page.tsx
index 43b0b01..1e7ea91 100644
--- a/src/app/(site)/page.tsx
+++ b/src/app/(site)/page.tsx
@@ -24,7 +24,7 @@ import type { ViewerScope } from '~/domain/types'
const scope: ViewerScope = 'internal'
-const sideCount = 3
+const sideCount = 2
const sideThreshold = 4
@@ -101,7 +101,7 @@ export default async function OverviewPage({ searchParams }: { searchParams: Pro
{lead ? (
-
+
) : null}
diff --git a/src/app/admin/layout.tsx b/src/app/admin/layout.tsx
index b5d19e5..e916f88 100644
--- a/src/app/admin/layout.tsx
+++ b/src/app/admin/layout.tsx
@@ -29,7 +29,7 @@ export default async function AdminLayout({ children }: { children: ReactNode })
mark={}
>
- {children}
+ {children}
)
diff --git a/src/app/admin/page.tsx b/src/app/admin/page.tsx
index e181a5c..59539bf 100644
--- a/src/app/admin/page.tsx
+++ b/src/app/admin/page.tsx
@@ -2,11 +2,13 @@ import Link from 'next/link'
import { getTranslations } from 'next-intl/server'
import { TbArrowNarrowRight, TbPhoto, TbPlus } from 'react-icons/tb'
import { AdminHeading } from '~/components/admin/AdminHeading'
+import { EntryStatusChip } from '~/components/admin/EntryStatusChip'
import { AdminNotice } from '~/components/admin/AdminNotice'
import { ghostButtonClass, quietLinkClass } from '~/components/admin/styles'
-import { projectStyle, projectSurface } from '~/components/ui/Plate'
+import { Plate, projectStyle, projectSurface } from '~/components/ui/Plate'
import { SectionLabel } from '~/components/ui/SectionLabel'
import { listClients } from '~/data/repositories/clients'
+import { listEntries } from '~/data/repositories/entries'
import { listAllProjects, listBrands } from '~/data/repositories/projects'
import { listUsers } from '~/data/repositories/users'
import { listProjectsForUser } from '~/data/repositories/user-roles'
@@ -15,6 +17,8 @@ import {
adminBrandsPath,
adminClientsPath,
adminNewEntryPath,
+ adminEntriesPath,
+ adminEntryPath,
adminNewProjectPath,
adminProjectEntriesPath,
adminProjectPath,
@@ -25,6 +29,8 @@ import { isAdmin } from '~/lib/auth-access'
import { requireAdminArea } from '~/lib/auth-guards'
import { adminMediaPath } from '~/lib/auth-routes'
+const recentCount = 5
+
export default async function AdminPage() {
const viewer = await requireAdminArea()
const t = await getTranslations('admin')
@@ -35,9 +41,24 @@ export default async function AdminPage() {
const brands = manages ? await listBrands() : []
const users = manages ? await listUsers() : []
const clients = manages ? await listClients() : []
+ const projectIds = manages ? undefined : projects.map(project => project.id)
+
+ const [drafts, scheduled, published, recent] = await Promise.all([
+ listEntries({ projectIds, status: 'draft', page: 1, perPage: 1 }),
+ listEntries({ projectIds, status: 'scheduled', page: 1, perPage: 1 }),
+ listEntries({ projectIds, status: 'published', page: 1, perPage: 1 }),
+ listEntries({ projectIds, page: 1, perPage: recentCount }),
+ ])
+
+ const entryTiles = [
+ { href: adminEntriesPath, label: e('status.draft'), value: drafts.total },
+ { href: adminEntriesPath, label: e('status.scheduled'), value: scheduled.total },
+ { href: adminEntriesPath, label: e('status.published'), value: published.total },
+ ]
const tiles = manages
? [
+ ...entryTiles,
{ href: adminProjectsPath, label: t('nav.projects'), value: projects.length },
{ href: adminBrandsPath, label: t('nav.brands'), value: brands.length },
{ href: adminUsersPath, label: t('nav.users'), value: users.length },
@@ -47,7 +68,7 @@ export default async function AdminPage() {
value: clients.filter(client => client.revokedAt === null).length,
},
]
- : []
+ : entryTiles
return (
@@ -72,9 +93,9 @@ export default async function AdminPage() {
/>
{tiles.length > 0 ? (
-
+
{tiles.map(tile => (
- -
+
-
{tile.label}
@@ -86,6 +107,36 @@ export default async function AdminPage() {
) : null}
+ {recent.items.length > 0 ? (
+
+ {t('allEntries')}}>
+ {t('recentEntries')}
+
+
+ {recent.items.map(item => (
+ -
+
+
+ {item.title}
+
+
+
+ ))}
+
+
+ ) : null}
+
{t('projectsMeta', { count: projects.length })}}>
{t('yourProjects')}
diff --git a/src/app/globals.css b/src/app/globals.css
index 1610f09..87664ca 100644
--- a/src/app/globals.css
+++ b/src/app/globals.css
@@ -17,6 +17,7 @@
--color-shot-1: var(--color-rule);
--color-shot-2: var(--color-ink-3);
--container-page: 63rem;
+ --container-wide: 80rem;
--container-read: 38rem;
--font-display: var(--font-archivo), system-ui, sans-serif;
--font-body: var(--font-source-serif), Georgia, serif;
diff --git a/src/components/archive/ArchiveMonths.tsx b/src/components/archive/ArchiveMonths.tsx
index 05d1a03..4cebe45 100644
--- a/src/components/archive/ArchiveMonths.tsx
+++ b/src/components/archive/ArchiveMonths.tsx
@@ -37,7 +37,7 @@ function group(months: ArchiveMonth[]): YearGroup[] {
export function ArchiveMonths({ months, hrefFor, activeYear, activeMonth }: ArchiveMonthsProps) {
const t = useTranslations('archive')
- if (months.length <= 1) {
+ if (months.length === 0) {
return null
}
diff --git a/src/components/archive/EntryRow.tsx b/src/components/archive/EntryRow.tsx
index 3989a80..5932fef 100644
--- a/src/components/archive/EntryRow.tsx
+++ b/src/components/archive/EntryRow.tsx
@@ -1,11 +1,10 @@
import Link from 'next/link'
-import type { CSSProperties } from 'react'
import { useTranslations } from 'next-intl'
import { AuthorMark } from '~/components/ui/AuthorMark'
import { Badge } from '~/components/ui/Badge'
import { EntryDate } from '~/components/ui/EntryDate'
import { Highlight } from '~/components/ui/Highlight'
-import { entryMark, entryNumber, projectStyle, projectSurface } from '~/components/ui/Plate'
+import { Plate } from '~/components/ui/Plate'
import { postPath } from '~/lib/routes'
import type { PostListItem } from '~/data/repositories/posts'
@@ -17,49 +16,42 @@ export type EntryRowProps = {
className?: string
}
-const stagger = 40
-
-const maxStagger = 12
-
-export function EntryRow({ item, index = 0, showCode = true, highlight, className }: EntryRowProps) {
+export function EntryRow({ item, showCode = true, highlight, className }: EntryRowProps) {
const t = useTranslations('entry')
- const delay = `${Math.min(index, maxStagger) * stagger}ms`
- const style: CSSProperties = { ...projectStyle(item.projectColor), animationDelay: delay }
const source = { code: item.projectCode, slug: item.projectSlug }
return (
-
-
-
-
- {showCode ? entryMark(source, item.number) : entryNumber(item.number)}
-
+
+
+
{t('number', { number: item.number })}
-
+
{item.teaser ? (
-
+
{item.teaser}
) : (
-
+
)}
-
+
{item.publishAt ? : null}
{item.authorName ? : null}
diff --git a/src/components/archive/FeatureEntry.tsx b/src/components/archive/FeatureEntry.tsx
index 970d94f..485a765 100644
--- a/src/components/archive/FeatureEntry.tsx
+++ b/src/components/archive/FeatureEntry.tsx
@@ -4,7 +4,7 @@ import { AuthorMark } from '~/components/ui/AuthorMark'
import { Badge } from '~/components/ui/Badge'
import { Cover, type CoverMedia } from '~/components/ui/Cover'
import { EntryDate } from '~/components/ui/EntryDate'
-import { Plate, entryMark, projectStyle, projectSurface } from '~/components/ui/Plate'
+import { Plate, projectStyle } from '~/components/ui/Plate'
import { Stamp } from '~/components/ui/Stamp'
import { postPath, projectPath } from '~/lib/routes'
import type { PostListItem } from '~/data/repositories/posts'
@@ -34,25 +34,30 @@ export function FeatureEntry({
return (
- }
- />
+
+
}
+ />
+ {cover ? (
+
+ ) : null}
+
-
-
-
- {entryMark(source, item.number)}
-
-
{item.publishAt ? : null}
{newest ? : null}
diff --git a/src/components/archive/PostDetail.tsx b/src/components/archive/PostDetail.tsx
index b9d3869..e1e879a 100644
--- a/src/components/archive/PostDetail.tsx
+++ b/src/components/archive/PostDetail.tsx
@@ -1,6 +1,6 @@
import Link from 'next/link'
import { useFormatter, useLocale, useTranslations } from 'next-intl'
-import { TbArrowNarrowLeft } from 'react-icons/tb'
+import { TbArrowNarrowLeft, TbArrowNarrowRight } from 'react-icons/tb'
import { EmptyNotice } from './EmptyNotice'
import { BlockRenderer } from '~/components/blocks/BlockRenderer'
import { Wrap } from '~/components/layout/Wrap'
@@ -17,18 +17,22 @@ import { toCover } from '~/lib/cover'
import { longDateTimeFormat } from '~/lib/dates'
import { contentBlocks, readingMinutes } from '~/lib/post-content'
import { projectPath } from '~/lib/routes'
-import type { ArchivePostDetail } from '~/data/repositories/archive'
+import { SideEntries } from './SideEntries'
+import { postPath } from '~/lib/routes'
+import type { ArchivePostDetail, PostCover, PostNeighbours } from '~/data/repositories/archive'
import type { Locale } from '~/i18n/config'
export type PostDetailProps = {
detail: ArchivePostDetail
+ neighbours?: PostNeighbours
+ covers?: Map
}
const column = 'mx-auto w-full max-w-read'
const coverSizes = '(min-width: 63rem) 63rem, 100vw'
-export function PostDetail({ detail }: PostDetailProps) {
+export function PostDetail({ detail, neighbours, covers }: PostDetailProps) {
const t = useTranslations('post')
const entry = useTranslations('entry')
const locale = useLocale() as Locale
@@ -146,6 +150,46 @@ export function PostDetail({ detail }: PostDetailProps) {
+
+ {neighbours && (neighbours.previous || neighbours.next) ? (
+
+ ) : null}
+
+ {neighbours && neighbours.more.length > 0 ? (
+
+
+
+ ) : null}
diff --git a/src/components/archive/ProjectArchive.tsx b/src/components/archive/ProjectArchive.tsx
index 5944469..ab1bc11 100644
--- a/src/components/archive/ProjectArchive.tsx
+++ b/src/components/archive/ProjectArchive.tsx
@@ -105,7 +105,7 @@ export function ProjectArchive({
color={project.color}
number={highest ?? project.postCount}
showNumber={project.postCount > 0}
- size="row"
+ size="mark"
/>
{archive('entries', { count: project.postCount })}
diff --git a/src/components/archive/SideEntries.tsx b/src/components/archive/SideEntries.tsx
index acda5c7..609ff03 100644
--- a/src/components/archive/SideEntries.tsx
+++ b/src/components/archive/SideEntries.tsx
@@ -3,7 +3,7 @@ import { useTranslations } from 'next-intl'
import { Badge } from '~/components/ui/Badge'
import { Cover, type CoverMedia } from '~/components/ui/Cover'
import { EntryDate } from '~/components/ui/EntryDate'
-import { Plate, entryMark, projectStyle } from '~/components/ui/Plate'
+import { Plate, projectStyle } from '~/components/ui/Plate'
import { SectionLabel } from '~/components/ui/SectionLabel'
import { postPath } from '~/lib/routes'
import type { PostListItem } from '~/data/repositories/posts'
@@ -11,12 +11,14 @@ import type { PostListItem } from '~/data/repositories/posts'
export type SideEntriesProps = {
items: PostListItem[]
covers?: Map
+ label?: string
+ layout?: 'column' | 'grid'
className?: string
}
-const coverSizes = '(min-width: 64rem) 6rem, 20vw'
+const coverSizes = '(min-width: 64rem) 22rem, 40vw'
-export function SideEntries({ items, covers, className }: SideEntriesProps) {
+export function SideEntries({ items, covers, label, layout = 'column', className }: SideEntriesProps) {
const t = useTranslations('entry')
if (items.length === 0) {
@@ -25,8 +27,8 @@ export function SideEntries({ items, covers, className }: SideEntriesProps) {
return (