Skip to content

Commit cb4a066

Browse files
committed
Refactor blog index; add pagination
1 parent d61589a commit cb4a066

File tree

1 file changed

+225
-63
lines changed

1 file changed

+225
-63
lines changed

pages/blog/index.vue

Lines changed: 225 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,11 @@
11
<script setup>
22
const 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
115
const selectedCategories = ref([]);
12-
if (route.query.category) {
13-
selectedCategories.value = [route.query.category];
14-
}
15-
166
const selectedTags = ref([]);
17-
if (route.query.tag) {
18-
selectedTags.value = [route.query.tag];
19-
}
7+
const currentPage = ref(1);
8+
const pageSize = 5;
209
2110
const 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+
6967
function 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
7777
function 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

Comments
 (0)