1+ ---
2+ import type { CollectionEntry } from ' astro:content'
3+ import type { AstroComponentFactory } from ' astro/runtime/server/index.js'
4+ import { Picture } from ' astro:assets'
5+ import BaseLayout from ' @layouts/BaseLayout.astro'
6+ import Icon from ' @components/Icon/index.astro'
7+ import {
8+ buildTagPagePath ,
9+ getSortedTagContent ,
10+ getTagPageSlice ,
11+ getTagTotalPages ,
12+ } from ' @lib/tags/pagination'
13+
14+ export interface Props {
15+ tagEntry: CollectionEntry <' tags' >
16+ content: Array <CollectionEntry <' articles' >>
17+ currentPage: number
18+ TagContent: AstroComponentFactory
19+ }
20+
21+ const { tagEntry, content : allTagContent, currentPage, TagContent } = Astro .props as Props
22+ const tag = tagEntry .data .slug
23+ const totalItems = allTagContent .length
24+ const totalPages = getTagTotalPages (totalItems )
25+ const sortedContent = getSortedTagContent (allTagContent )
26+ const paginatedContent = getTagPageSlice (sortedContent , currentPage )
27+ const path = buildTagPagePath (tag , currentPage )
28+ ---
29+
30+ <BaseLayout
31+ pageTitle ={ ` ${tagEntry .data .displayName } - Tag ` }
32+ pageDescription ={ tagEntry .data .description }
33+ path ={ path }
34+ breadcrumbTitle ={ tagEntry .data .displayName }
35+ section =" Tags"
36+ >
37+ <div class =" max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12" >
38+ <header class =" mb-6 sm:mb-8" >
39+ <div class =" grid grid-cols-[auto_1fr] items-start gap-4 sm:flex sm:gap-6 sm:items-start" >
40+ <div class =" shrink-0" >
41+ <div
42+ class =" relative w-28 sm:w-32 md:w-60 aspect-square overflow-hidden border border-trim rounded-2xl bg-page-offset p-4"
43+ >
44+ <Picture
45+ src ={ tagEntry .data .cover }
46+ alt ={ tagEntry .data .coverAlt }
47+ widths ={ [112 , 128 , 240 ]}
48+ sizes =" (min-width: 768px) 240px, (min-width: 640px) 128px, 112px"
49+ formats ={ [' avif' , ' webp' , ' png' ]}
50+ layout =" constrained"
51+ fit =" contain"
52+ position =" center"
53+ class =" absolute inset-0 h-full w-full object-contain"
54+ loading =" eager"
55+ />
56+ </div >
57+ </div >
58+
59+ <div class =" min-w-0 sm:flex-1 sm:space-y-4 sm:ml-4 sm:mt-6" >
60+ <div
61+ class =" flex flex-col gap-2 sm:flex-row sm:flex-wrap sm:items-center sm:gap-x-4 sm:gap-y-2"
62+ >
63+ <h1 class =" text-page-inverse" >
64+ { tagEntry .data .displayName }
65+ </h1 >
66+ <span
67+ class =" inline-flex w-fit rounded-full bg-page-inverse px-3 py-1 text-sm font-semibold text-page-base whitespace-nowrap ml-2 sm:mt-2"
68+ >
69+ { totalItems }
70+ { totalItems === 1 ? ' article' : ' articles' }
71+ </span >
72+ </div >
73+
74+ {
75+ tagEntry .data .intro && (
76+ <p class = " hidden sm:block text-lg text-content leading-relaxed max-w-2xl" >
77+ { tagEntry .data .intro }
78+ </p >
79+ )
80+ }
81+
82+ <div
83+ class =" flex flex-wrap items-center gap-x-4 gap-y-1 text-sm text-content-offset mt-2 sm:mt-0 ml-3 sm:ml-0"
84+ >
85+ {
86+ sortedContent .length > 0 && (
87+ <span class = " text-content-active" >
88+ Latest:{ ' ' }
89+ <time class = " font-medium text-content" >
90+ { sortedContent [0 ]! .data .publishDate .toLocaleDateString (' en-US' , {
91+ month: ' short' ,
92+ day: ' numeric' ,
93+ year: ' numeric' ,
94+ })}
95+ </time >
96+ </span >
97+ )
98+ }
99+ </div >
100+ </div >
101+ </div >
102+ </header >
103+
104+ <section
105+ class =" border border-trim rounded-xl px-6 sm:px-8 pt-4 sm:pt-6 max-w-none mb-12"
106+ aria-label =" Tag description"
107+ >
108+ <TagContent />
109+ </section >
110+
111+ {
112+ paginatedContent .length > 0 ? (
113+ <>
114+ <section
115+ class = " grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8 lg:gap-12 mb-12"
116+ aria-labelledby = " tagged-content-title"
117+ >
118+ <h2 class = " sr-only" id = " tagged-content-title" >
119+ Tagged content
120+ </h2 >
121+ { paginatedContent .map (item => {
122+ const href = ` /articles/${item .id } `
123+
124+ return (
125+ <article class = " group h-full relative after:pointer-events-none after:absolute after:content-[''] after:inset-0 after:rounded-none after:border-2 after:border-transparent after:opacity-0 after:transition-opacity after:duration-150 after:ease-out focus-within:after:opacity-100 focus-within:after:-inset-1.5 focus-within:after:border-spotlight" >
126+ <a
127+ href = { href }
128+ class = " block h-full bg-content-inverse rounded-xl shadow-lg transition-all duration-300 overflow-hidden border border-trim transform hover:-translate-y-2 no-underline hover:no-underline focus-visible:no-underline focus-visible:outline-none focus-visible:shadow-none"
129+ >
130+ { item .data .cover && (
131+ <div class = " relative aspect-video overflow-hidden" >
132+ <Picture
133+ src = { item .data .cover }
134+ alt = { item .data .coverAlt }
135+ widths = { [320 , 640 , 960 , 1280 ]}
136+ sizes = " (min-width: 1280px) 33vw, (min-width: 768px) 50vw, 100vw"
137+ formats = { [' avif' , ' webp' , ' jpeg' ]}
138+ layout = " constrained"
139+ fit = " cover"
140+ position = " center"
141+ class = " absolute inset-0 h-full w-full"
142+ loading = " lazy"
143+ />
144+ </div >
145+ )}
146+
147+ <div class = " p-6 space-y-4" >
148+ <div class = " flex items-center gap-3 text-sm" >
149+ <span class = " px-3 py-1 rounded-full text-white font-medium bg-primary" >
150+ Article
151+ </span >
152+ <time datetime = { item .data .publishDate .toISOString ()} >
153+ { item .data .publishDate .toLocaleDateString (' en-US' , {
154+ year: ' numeric' ,
155+ month: ' long' ,
156+ day: ' numeric' ,
157+ })}
158+ </time >
159+ </div >
160+
161+ <h2 class = " text-xl font-semibold mb-3 group-hover:text-primary transition-colors" >
162+ { item .data .title }
163+ </h2 >
164+
165+ { item .data .description && (
166+ <p class = " text-content-offset leading-relaxed text-sm" >
167+ { item .data .description }
168+ </p >
169+ )}
170+ <div class = " mt-4 flex items-center text-primary text-sm font-medium opacity-0 group-hover:opacity-100 transition-opacity" >
171+ <span >Learn more</span >
172+ <Icon
173+ icon = " exit-right-thin"
174+ size = { 4 }
175+ classes = " ml-0.5 mb-0.5 transform group-hover:translate-x-1 transition-transform"
176+ />
177+ </div >
178+ </div >
179+ </a >
180+ </article >
181+ )
182+ })}
183+ </section >
184+
185+ { totalPages > 1 && (
186+ <nav class = " flex flex-wrap justify-center items-center gap-2" aria-label = " Pagination" >
187+ { currentPage > 1 && (
188+ <a
189+ href = { buildTagPagePath (tag , currentPage - 1 )}
190+ class = " px-4 py-2 text-sm font-medium bg-content-inverse border border-trim rounded-lg hover:bg-page-offset transition-colors duration-200"
191+ >
192+ ← Previous
193+ </a >
194+ )}
195+
196+ { currentPage > 3 && (
197+ <>
198+ <a
199+ href = { buildTagPagePath (tag , 1 )}
200+ class = " px-3 py-2 text-sm font-medium bg-content-inverse border border-trim rounded-lg hover:bg-page-offset transition-colors duration-200"
201+ >
202+ 1
203+ </a >
204+ { currentPage > 4 && <span class = " px-3 py-2 text-sm" >…</span >}
205+ </>
206+ )}
207+
208+ { Array .from ({ length: Math .min (5 , totalPages ) }, (_ , i ) => {
209+ const startPage = Math .max (1 , Math .min (currentPage - 2 , totalPages - 4 ))
210+ const page = startPage + i
211+ if (page > totalPages ) return null
212+
213+ const isCurrentPage = page === currentPage
214+ return (
215+ <a
216+ href = { buildTagPagePath (tag , page )}
217+ class = { ` px-3 py-2 text-sm font-medium rounded-lg transition-colors duration-200 ${
218+ isCurrentPage
219+ ? ' bg-spotlight text-white'
220+ : ' bg-content-inverse border border-trim hover:bg-page-offset'
221+ } ` }
222+ aria-current = { isCurrentPage ? ' page' : undefined }
223+ >
224+ { page }
225+ </a >
226+ )
227+ })}
228+
229+ { currentPage < totalPages - 2 && (
230+ <>
231+ { currentPage < totalPages - 3 && <span class = " px-3 py-2 text-sm" >…</span >}
232+ <a
233+ href = { buildTagPagePath (tag , totalPages )}
234+ class = " px-3 py-2 text-sm font-medium bg-content-inverse border border-trim rounded-lg hover:bg-page-offset transition-colors duration-200"
235+ >
236+ { totalPages }
237+ </a >
238+ </>
239+ )}
240+
241+ { currentPage < totalPages && (
242+ <a
243+ href = { buildTagPagePath (tag , currentPage + 1 )}
244+ class = " px-4 py-2 text-sm font-medium bg-content-inverse border border-trim rounded-lg hover:bg-page-offset transition-colors duration-200"
245+ >
246+ Next →
247+ </a >
248+ )}
249+ </nav >
250+ )}
251+ </>
252+ ) : (
253+ <div class = " text-center py-12" >
254+ <p class = " text-xl mb-4" >No content found for this tag.</p >
255+ <a href = " /tags" class = " text-primary hover:underline" >
256+ Browse other tags
257+ </a >
258+ </div >
259+ )
260+ }
261+ </div >
262+ </BaseLayout >
0 commit comments