11<script setup>
22const route = useRoute ();
3-
4- // optionally load `category` or `tag` query parameter when instantiating selection refs
5- //
6- // examples
7- //
8- // localhost:3000/blog?category=embedded
9- // localhost:3000/blog?tag=embedded
3+ const router = useRouter ();
104
115const selectedCategories = ref ([]);
12- if (route .query .category ) {
13- selectedCategories .value = [route .query .category ];
14- }
15-
166const selectedTags = ref ([]);
17- if (route .query .tag ) {
18- selectedTags .value = [route .query .tag ];
19- }
7+ const currentPage = ref (1 );
8+ const pageSize = 5 ;
209
2110const showTags = ref (true );
2211
@@ -66,12 +55,23 @@ const visibleArticles = computed(() => {
6655 });
6756});
6857
58+ const totalPages = computed (() => {
59+ return Math .max (1 , Math .ceil (visibleArticles .value .length / pageSize));
60+ });
61+
62+ const paginatedArticles = computed (() => {
63+ const start = (currentPage .value - 1 ) * pageSize;
64+ return visibleArticles .value .slice (start, start + pageSize);
65+ });
66+
6967function toggleTag (tag ) {
7068 if (selectedTags .value .includes (tag)) {
7169 selectedTags .value = selectedTags .value .filter ((el ) => el !== tag);
7270 } else {
7371 selectedTags .value .push (tag);
7472 }
73+
74+ currentPage .value = 1 ;
7575}
7676
7777function toggleCategory (cat ) {
@@ -82,65 +82,227 @@ function toggleCategory(cat) {
8282 } else {
8383 selectedCategories .value .push (cat);
8484 }
85+
86+ currentPage .value = 1 ;
87+ }
88+
89+ function normalizeFilterQuery (value ) {
90+ if (! value) {
91+ return [];
92+ }
93+
94+ const values = Array .isArray (value)
95+ ? value
96+ : value .split (" ," ).map ((val ) => val .trim ());
97+
98+ return [... new Set (values .filter (Boolean ))];
99+ }
100+
101+ function queryValueToString (value ) {
102+ if (! value) {
103+ return " " ;
104+ }
105+
106+ return Array .isArray (value) ? value .join (" ," ) : value;
107+ }
108+
109+ function normalizePageQuery (value ) {
110+ if (! value) {
111+ return 1 ;
112+ }
113+
114+ const raw = Array .isArray (value) ? value[0 ] : value;
115+ const parsed = parseInt (raw, 10 );
116+
117+ return Number .isFinite (parsed) && parsed > 0 ? parsed : 1 ;
85118}
119+
120+ watch (
121+ () => route .query .category ,
122+ (value ) => {
123+ selectedCategories .value = normalizeFilterQuery (value);
124+ },
125+ { immediate: true },
126+ );
127+
128+ watch (
129+ () => route .query .tag ,
130+ (value ) => {
131+ selectedTags .value = normalizeFilterQuery (value);
132+ },
133+ { immediate: true },
134+ );
135+
136+ watch (
137+ () => route .query .page ,
138+ (value ) => {
139+ currentPage .value = normalizePageQuery (value);
140+ },
141+ { immediate: true },
142+ );
143+
144+ watch (visibleArticles, () => {
145+ if (currentPage .value > totalPages .value ) {
146+ currentPage .value = totalPages .value ;
147+ }
148+ });
149+
150+ watch (
151+ [selectedCategories, selectedTags, currentPage],
152+ () => {
153+ const nextCategory = selectedCategories .value .join (" ," );
154+ const nextTag = selectedTags .value .join (" ," );
155+ const nextPage = currentPage .value > 1 ? String (currentPage .value ) : " " ;
156+
157+ const currentCategory = queryValueToString (route .query .category );
158+ const currentTag = queryValueToString (route .query .tag );
159+ const currentPageQuery = Array .isArray (route .query .page )
160+ ? route .query .page [0 ]
161+ : route .query .page || " " ;
162+
163+ if (
164+ nextCategory === currentCategory &&
165+ nextTag === currentTag &&
166+ nextPage === currentPageQuery
167+ ) {
168+ return ;
169+ }
170+
171+ const query = { ... route .query };
172+
173+ if (nextCategory) {
174+ query .category = nextCategory;
175+ } else {
176+ delete query .category ;
177+ }
178+
179+ if (nextTag) {
180+ query .tag = nextTag;
181+ } else {
182+ delete query .tag ;
183+ }
184+
185+ if (nextPage) {
186+ query .page = nextPage;
187+ } else {
188+ delete query .page ;
189+ }
190+
191+ router .replace ({ query });
192+ },
193+ { deep: true },
194+ );
86195 </script >
87196
88197<template >
89- <section class =" container mx-auto font-mono text-white" >
90- <div class =" grid grid-cols-4 gap-4" >
91- <div class =" col-span-4 lg:col-span-3" >
92- <div class =" grid grid-cols-10 gap-y-4 lg:gap-y-6" >
93- <template v-for =" article in visibleArticles " :key =" article ._id " >
94- <NuxtTime
95- :datetime =" article.date"
96- class =" col-span-10 lg:col-span-2"
97- year =" numeric"
98- month =" short"
99- day =" 2-digit"
100- />
101- <div class =" col-span-10 lg:col-span-8" >
102- <div class =" flex-col space-y-2" >
103- <NuxtLink class =" text-orange-500" :to =" article.path" >{{
104- article.title
105- }}</NuxtLink >
106- <ContentRenderer
107- v-if =" article.meta?.excerpt"
108- :value =" article.meta.excerpt"
109- class =" line-clamp-5 text-xs text-gray-400"
110- />
198+ <section
199+ class =" min-h-screen bg-emerald-950 bg-[url('/images/noise.svg')] py-10 text-white"
200+ >
201+ <div class =" container mx-auto space-y-8" >
202+ <div class =" space-y-4" >
203+ <div class =" grid gap-4" >
204+ <NuxtLink
205+ v-for =" article in paginatedArticles"
206+ :key =" article._id"
207+ :to =" article.path"
208+ class =" h-full"
209+ >
210+ <div
211+ class =" flex h-full flex-col justify-between space-y-3 bg-black/50 p-4 text-white drop-shadow-lg hover:ring-1 hover:ring-white"
212+ >
213+ <div class =" space-y-1" >
214+ <div class =" flex items-center gap-3" >
215+ <p class =" flex-1 text-xl font-semibold text-orange-400" >
216+ {{ article.title }}
217+ </p >
218+ <NuxtTime
219+ :datetime =" article.date"
220+ class =" text-sm text-gray-300"
221+ year =" numeric"
222+ month =" short"
223+ day =" 2-digit"
224+ />
225+ </div >
111226 </div >
227+ <p
228+ v-if =" article.description"
229+ class =" line-clamp-4 text-sm text-gray-100"
230+ >
231+ {{ article.description }}
232+ </p >
233+ <ContentRenderer
234+ v-else-if =" article.meta?.excerpt"
235+ :value =" article.meta.excerpt"
236+ class =" line-clamp-4 text-sm text-gray-100"
237+ />
112238 </div >
113- </template >
239+ </NuxtLink >
114240 </div >
115- </div >
116- <div class =" col-span-4 border-l border-gray-700 pl-4 lg:col-span-1" >
117- <div class =" my-2 space-y-2" >
118- <p class =" text-xl font-bold" >Categories</p >
119- <div
120- v-for =" category in categories"
121- :key =" category"
122- :class =" {
123- 'bg-orange-500 text-white': selectedCategories.includes(category),
124- }"
125- class =" cursor-pointer p-1 text-sm text-gray-400 hover:bg-orange-500 hover:text-white"
126- @click =" toggleCategory(category)"
241+
242+ <div class =" flex items-center justify-between text-sm text-gray-300" >
243+ <button
244+ type =" button"
245+ class =" border border-white/20 px-3 py-1 text-xs uppercase tracking-wide transition hover:border-orange-400 hover:text-white disabled:cursor-not-allowed disabled:border-white/5 disabled:text-gray-500"
246+ :disabled =" currentPage === 1"
247+ @click =" currentPage > 1 && currentPage--"
127248 >
128- {{ category }}
129- </div >
249+ Previous
250+ </button >
251+ <p >Page {{ currentPage }} of {{ totalPages }}</p >
252+ <button
253+ type =" button"
254+ class =" border border-white/20 px-3 py-1 text-xs uppercase tracking-wide transition hover:border-orange-400 hover:text-white disabled:cursor-not-allowed disabled:border-white/5 disabled:text-gray-500"
255+ :disabled =" currentPage === totalPages"
256+ @click =" currentPage < totalPages && currentPage++"
257+ >
258+ Next
259+ </button >
130260 </div >
261+ </div >
131262
132- <div v-if =" showTags" class =" my-2 space-y-2" >
133- <p class =" text-xl font-bold" >Tags</p >
134- <div
135- v-for =" (tag, ix) in tags"
136- :key =" ix"
137- :class =" {
138- 'bg-orange-500 text-white': selectedTags.includes(tag),
139- }"
140- class =" cursor-pointer select-none p-1 text-sm text-gray-400 hover:bg-orange-500 hover:text-white"
141- @click =" toggleTag(tag)"
142- >
143- {{ tag }}
263+ <div class =" space-y-6 border-t border-white/20 pt-6" >
264+ <div class =" grid gap-8 lg:grid-cols-2" >
265+ <div >
266+ <p class =" text-xs uppercase tracking-widest text-gray-300" >
267+ Categories
268+ </p >
269+ <div class =" mt-3 flex flex-wrap gap-2" >
270+ <button
271+ v-for =" category in categories"
272+ :key =" category"
273+ type =" button"
274+ :class =" [
275+ 'border border-white/20 px-4 py-1 text-sm transition hover:border-orange-400 hover:text-white',
276+ selectedCategories.includes(category)
277+ ? 'bg-orange-500 text-white'
278+ : 'text-gray-200',
279+ ]"
280+ @click =" toggleCategory(category)"
281+ >
282+ {{ category }}
283+ </button >
284+ </div >
285+ </div >
286+ <div v-if =" showTags" >
287+ <p class =" text-xs uppercase tracking-widest text-gray-300" >
288+ Tags
289+ </p >
290+ <div class =" mt-3 flex flex-wrap gap-2" >
291+ <button
292+ v-for =" (tag, ix) in tags"
293+ :key =" ix"
294+ type =" button"
295+ :class =" [
296+ 'border border-white/20 px-4 py-1 text-sm transition hover:border-orange-400 hover:text-white',
297+ selectedTags.includes(tag)
298+ ? 'bg-orange-500 text-white'
299+ : 'text-gray-200',
300+ ]"
301+ @click =" toggleTag(tag)"
302+ >
303+ {{ tag }}
304+ </button >
305+ </div >
144306 </div >
145307 </div >
146308 </div >
0 commit comments