11import type { Metadata } from "next"
2- import Link from "next/link"
32
4- import { getPublishedPosts , getAllSeries } from "@/lib/posts"
5- import { computeReadingStats } from "@/lib/reading-time"
6- import { WaxSeal } from "@/components/wax-seal"
3+ import { Frontispiece } from "@/components/frontispiece"
74import { site } from "@/lib/site"
85
96export const metadata : Metadata = {
@@ -12,284 +9,10 @@ export const metadata: Metadata = {
129 alternates : { canonical : "/frontispiece" } ,
1310}
1411
15- // Roman numerals up to 3999 — sufficient for years and post counts.
16- function toRoman ( n : number ) : string {
17- if ( n <= 0 ) return ""
18- const map : [ number , string ] [ ] = [
19- [ 1000 , "M" ] , [ 900 , "CM" ] , [ 500 , "D" ] , [ 400 , "CD" ] ,
20- [ 100 , "C" ] , [ 90 , "XC" ] , [ 50 , "L" ] , [ 40 , "XL" ] ,
21- [ 10 , "X" ] , [ 9 , "IX" ] , [ 5 , "V" ] , [ 4 , "IV" ] , [ 1 , "I" ] ,
22- ]
23- let result = ""
24- let rem = n
25- for ( const [ v , s ] of map ) {
26- while ( rem >= v ) {
27- result += s
28- rem -= v
29- }
30- }
31- return result
32- }
33-
34- const MONTH_LATIN = [
35- "Ianuarius" , "Februarius" , "Martius" , "Aprilis" , "Maius" , "Iunius" ,
36- "Iulius" , "Augustus" , "September" , "October" , "November" , "December" ,
37- ] as const
38-
3912export default function FrontispiecePage ( ) {
40- const posts = getPublishedPosts ( )
41- const series = getAllSeries ( )
42-
43- // CJK-aware total word count — sum across the whole corpus.
44- let totalWords = 0
45- for ( const p of posts ) totalWords += computeReadingStats ( p . content ) . totalWords
46-
47- // Earliest post date drives the "since" line. Posts are date-DESC.
48- const earliest = posts . length ? posts [ posts . length - 1 ] . date : ""
49- const earliestYear = earliest ? parseInt ( earliest . slice ( 0 , 4 ) , 10 ) : 0
50- const earliestMonth = earliest ? parseInt ( earliest . slice ( 5 , 7 ) , 10 ) : 0
51- const earliestLabel = earliest
52- ? `${ MONTH_LATIN [ earliestMonth - 1 ] } ${ toRoman ( earliestYear ) } `
53- : ""
54-
55- const currentYear = new Date ( ) . getFullYear ( )
56- const yearsSpan = earliest
57- ? Math . max ( 1 , currentYear - earliestYear + 1 )
58- : 0
59-
6013 return (
61- < main className = "relative min-h-screen pt-24 pb-20 px-5 sm:px-6 overflow-hidden" >
62- { /* Soft parchment vignettes — same vocabulary as the home hero */ }
63- < div
64- aria-hidden
65- className = "absolute inset-x-0 top-0 h-[65vh] pointer-events-none"
66- style = { {
67- backgroundImage :
68- "radial-gradient(ellipse 50% 40% at 100% 0%, rgba(194, 65, 12, 0.05), transparent 60%), " +
69- "radial-gradient(ellipse 45% 35% at 0% 100%, rgba(180, 83, 9, 0.04), transparent 65%)" ,
70- } }
71- />
72-
73- < article className = "relative max-w-3xl mx-auto" >
74- { /* ============ Outer page frame ============
75- A double rule with the upper-left flourish drawn in.
76- The flourish is a single SVG path that strokes-in over a
77- second, giving the impression of a hand sketching the
78- border the moment the page is opened. */ }
79- < div
80- className = "relative border hairline-strong rounded-sm bg-card/40 px-8 sm:px-14 py-14 sm:py-20"
81- style = { { outline : "1px solid var(--border)" , outlineOffset : "6px" } }
82- >
83- { /* Corner flourish — top-left. Path is drawn with stroke-dash. */ }
84- < svg
85- aria-hidden
86- viewBox = "0 0 80 80"
87- className = "absolute -top-1 -left-1 w-12 sm:w-16 text-primary/65"
88- fill = "none"
89- >
90- < path
91- d = "M 4 30 C 4 14, 14 4, 30 4 M 8 14 Q 14 8, 22 6 M 4 24 L 4 18 L 10 18"
92- stroke = "currentColor"
93- strokeWidth = "1.4"
94- strokeLinecap = "round"
95- style = { {
96- strokeDasharray : 120 ,
97- strokeDashoffset : 120 ,
98- animation : "swash-draw 1.4s 0.3s var(--ease-out) forwards" ,
99- } }
100- />
101- </ svg >
102- { /* Mirrored flourish — bottom-right */ }
103- < svg
104- aria-hidden
105- viewBox = "0 0 80 80"
106- className = "absolute -bottom-1 -right-1 w-12 sm:w-16 text-primary/65 rotate-180"
107- fill = "none"
108- >
109- < path
110- d = "M 4 30 C 4 14, 14 4, 30 4 M 8 14 Q 14 8, 22 6 M 4 24 L 4 18 L 10 18"
111- stroke = "currentColor"
112- strokeWidth = "1.4"
113- strokeLinecap = "round"
114- style = { {
115- strokeDasharray : 120 ,
116- strokeDashoffset : 120 ,
117- animation : "swash-draw 1.4s 0.6s var(--ease-out) forwards" ,
118- } }
119- />
120- </ svg >
121-
122- { /* ============ Press imprint ============ */ }
123- < div className = "text-center animate-fade-in-up" >
124- < p className = "font-mono text-[11px] sm:text-xs uppercase tracking-[0.42em] text-muted" >
125- Kairos · Press
126- </ p >
127- < div className = "flex items-center justify-center gap-3 mt-2" >
128- < span aria-hidden className = "block w-8 h-px bg-border-strong/60" />
129- < span className = "font-mono text-[10px] uppercase tracking-[0.32em] text-muted-light" >
130- Open Access · { toRoman ( currentYear ) }
131- </ span >
132- < span aria-hidden className = "block w-8 h-px bg-border-strong/60" />
133- </ div >
134- </ div >
135-
136- { /* ============ The mark ============ */ }
137- < div
138- className = "flex justify-center my-12 sm:my-16 animate-fade-in-up"
139- style = { { animationDelay : "0.18s" } }
140- >
141- < WaxSeal />
142- </ div >
143-
144- { /* ============ Bilingual epigraph ============ */ }
145- < div
146- className = "text-center animate-fade-in-up"
147- style = { { animationDelay : "0.34s" } }
148- >
149- < p className = "serif italic text-2xl sm:text-3xl text-foreground tracking-tight leading-snug" >
150- Verba volant,
151- < br className = "sm:hidden" />
152- < span className = "sm:ml-2" > scripta manent.</ span >
153- </ p >
154- < p className = "serif text-base sm:text-lg text-muted mt-4 leading-relaxed" >
155- 口舌之言易散,
156- < br className = "sm:hidden" />
157- < span className = "sm:ml-1" > 笔墨之迹长存。</ span >
158- </ p >
159- < p className = "font-mono text-[10px] uppercase tracking-[0.3em] text-muted-light mt-5" >
160- — proverbium romanum
161- </ p >
162- </ div >
163-
164- { /* ============ Volume colophon ============
165- The "this volume contains" line — uses real corpus data.
166- Numbers in serif tabular nums, labels in mono small-caps.
167- Reads like the verso of a real book's title page. */ }
168- < div
169- className = "mt-14 sm:mt-16 grid grid-cols-3 gap-6 sm:gap-10 animate-fade-in-up"
170- style = { { animationDelay : "0.5s" } }
171- >
172- < Stat
173- label = "Volumina"
174- sub = "篇"
175- value = { posts . length . toString ( ) }
176- roman = { toRoman ( posts . length ) }
177- />
178- < Stat
179- label = "Verba"
180- sub = "字"
181- value = { totalWords . toLocaleString ( "en-US" ) }
182- roman = { null }
183- />
184- < Stat
185- label = "Anno"
186- sub = "年"
187- value = { yearsSpan . toString ( ) }
188- roman = { toRoman ( yearsSpan ) }
189- />
190- </ div >
191-
192- { /* ============ Provenance line ============ */ }
193- < div
194- className = "mt-14 sm:mt-16 flex flex-col items-center text-center gap-2 animate-fade-in-up"
195- style = { { animationDelay : "0.66s" } }
196- >
197- < span aria-hidden className = "block w-10 h-px bg-border-strong/60" />
198- < p className = "font-mono text-[10px] sm:text-[11px] uppercase tracking-[0.28em] text-muted-light leading-relaxed" >
199- Imprinted by { site . author } · Kaifeng, Henan
200- </ p >
201- < p className = "font-mono text-[10px] uppercase tracking-[0.28em] text-muted-light leading-relaxed" >
202- { earliestLabel
203- ? < > Since { earliestLabel } · { series . length } { series . length === 1 ? "Series" : "Series" } </ >
204- : < > Sub Astris Apertis</ > }
205- </ p >
206- </ div >
207-
208- { /* ============ "Turn the page" CTA ============ */ }
209- < div
210- className = "mt-14 sm:mt-16 flex flex-col items-center gap-4 animate-fade-in-up"
211- style = { { animationDelay : "0.82s" } }
212- >
213- < Link
214- href = "/blog"
215- className = "group relative inline-flex items-center gap-3 serif italic text-lg text-foreground hover:text-primary transition-colors"
216- >
217- < span className = "text-primary/70 group-hover:text-primary transition-colors" >
218- 〔
219- </ span >
220- < span > 翻开书页</ span >
221- < span aria-hidden className = "font-mono not-italic text-xs uppercase tracking-[0.28em] text-muted-light group-hover:text-primary/80 transition-colors" >
222- Turn →
223- </ span >
224- < span className = "text-primary/70 group-hover:text-primary transition-colors" >
225- 〕
226- </ span >
227- </ Link >
228- < p className = "font-mono text-[10px] uppercase tracking-[0.24em] text-muted-light" >
229- or wander ·
230- < Link
231- href = "/archive"
232- className = "hover:text-foreground transition-colors"
233- >
234- /archive
235- </ Link >
236- ·
237- < Link
238- href = "/series"
239- className = "hover:text-foreground transition-colors"
240- >
241- /series
242- </ Link >
243- ·
244- < Link
245- href = "/curriculum"
246- className = "hover:text-foreground transition-colors"
247- >
248- /curriculum
249- </ Link >
250- </ p >
251- </ div >
252- </ div >
253-
254- { /* Folio number — like the bottom of every right-hand page in
255- an old book. Always reads "I" because this is page one. */ }
256- < p className = "mt-6 text-center font-mono text-[10px] uppercase tracking-[0.32em] text-muted-light tabular-nums" >
257- — folio I —
258- </ p >
259- </ article >
14+ < main >
15+ < Frontispiece mode = "page" />
26016 </ main >
26117 )
26218}
263-
264- function Stat ( {
265- label,
266- sub,
267- value,
268- roman,
269- } : {
270- label : string
271- sub : string
272- value : string
273- roman : string | null
274- } ) {
275- return (
276- < div className = "text-center" >
277- < p className = "font-mono text-[10px] uppercase tracking-[0.3em] text-muted-light mb-2" >
278- { label }
279- </ p >
280- < p className = "serif text-3xl sm:text-4xl font-semibold text-foreground tabular-nums leading-none" >
281- { value }
282- </ p >
283- { roman ? (
284- < p className = "font-mono text-[10px] uppercase tracking-[0.3em] text-primary/65 mt-2 tabular-nums" >
285- { roman }
286- </ p >
287- ) : (
288- < p className = "font-mono text-[10px] uppercase tracking-[0.3em] text-muted-light/60 mt-2" >
289- ·
290- </ p >
291- ) }
292- < p className = "serif italic text-xs text-muted mt-1" > { sub } </ p >
293- </ div >
294- )
295- }
0 commit comments