Add Logbuch: project update blog with admin, media and API

Public archive with sidebar navigation, project pages, month archive,
search and entry pages with image blocks. Admin area for brands,
projects, post types, users, api clients, media and the entry editor
with drag and drop images, preview per audience, scheduling and
publish checks. Read and write API with bearer tokens, audience
scoping, idempotent creation, OpenAPI document and editorial guide.
Magic link login with configurable allowed domains, whole app behind
the session gate. 456 tests including design rule checks.
This commit is contained in:
Matthias Giesselmann
2026-07-31 21:33:42 +02:00
commit b90ff252d1
291 changed files with 43671 additions and 0 deletions
+78
View File
@@ -0,0 +1,78 @@
import Link from 'next/link'
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 { SectionLabel } from '~/components/ui/SectionLabel'
import { postPath } from '~/lib/routes'
import type { PostListItem } from '~/data/repositories/posts'
export type SideEntriesProps = {
items: PostListItem[]
covers?: Map<string, CoverMedia>
className?: string
}
const coverSizes = '(min-width: 64rem) 6rem, 20vw'
export function SideEntries({ items, covers, className }: SideEntriesProps) {
const t = useTranslations('entry')
if (items.length === 0) {
return null
}
return (
<aside className={`flex flex-col gap-3 ${className ?? ''}`}>
<SectionLabel as="h3">{t('next')}</SectionLabel>
<ul className="m-0 flex list-none flex-col gap-3 p-0">
{items.map(item => {
const source = { code: item.projectCode, slug: item.projectSlug }
return (
<li key={item.id}>
<Link
href={postPath(item.projectSlug, item.slug)}
style={projectStyle(item.projectColor)}
className="group flex animate-rise gap-3 border border-rule bg-surface p-2.5 no-underline transition-colors duration-120 hover:border-ink-3 motion-reduce:animate-none motion-reduce:transition-none"
>
<Cover
media={covers?.get(item.id)}
sizes={coverSizes}
className="aspect-video w-24 shrink-0"
fallback={
<Plate
project={source}
color={item.projectColor}
number={item.number}
size="card"
className="w-full"
/>
}
/>
<span className="flex min-w-0 flex-1 flex-col gap-1">
<span className="flex flex-wrap items-center gap-x-2 gap-y-1 font-mono text-micro text-ink-3">
<span className="font-semibold uppercase tracking-mark text-ink-2 tabular-nums">
{entryMark(source, item.number)}
</span>
<Badge type={item.type} />
</span>
<span className="font-display text-small font-semibold leading-snug text-balance text-ink">
{item.title}
</span>
{item.teaser ? (
<span className="line-clamp-2 text-micro leading-snug text-ink-2">{item.teaser}</span>
) : null}
{item.publishAt ? (
<EntryDate date={item.publishAt} className="font-mono text-micro text-ink-3" />
) : null}
</span>
</Link>
</li>
)
})}
</ul>
</aside>
)
}