Skip to content

Commit 487403e

Browse files
author
Oleksandr Ratushnyi
committed
Add stats API and integrate post statistics retrieval from Redis
1 parent 6776170 commit 487403e

File tree

20 files changed

+145
-37
lines changed

20 files changed

+145
-37
lines changed

src/app/api/stats/route.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { redis } from '@/lib/redis';
2+
import { NextRequest, NextResponse } from 'next/server';
3+
4+
export async function GET(request: NextRequest) {
5+
const slugsParam = request.nextUrl.searchParams.get('slugs');
6+
7+
if (!slugsParam) {
8+
return NextResponse.json({ error: 'Slugs are required' }, { status: 400 });
9+
}
10+
11+
const slugs = slugsParam.split(',').filter(Boolean);
12+
13+
if (slugs.length === 0) {
14+
return NextResponse.json({ stats: {} });
15+
}
16+
17+
const pipeline = redis.pipeline();
18+
19+
for (const slug of slugs) {
20+
pipeline.get(`views:${slug}`);
21+
pipeline.get(`likes:${slug}`);
22+
}
23+
24+
const results = await pipeline.exec();
25+
26+
const stats: Record<string, { views: number; likes: number }> = {};
27+
28+
slugs.forEach((slug, index) => {
29+
const views = (results[index * 2] as number) ?? 0;
30+
const likes = (results[index * 2 + 1] as number) ?? 0;
31+
stats[slug] = { views, likes };
32+
});
33+
34+
return NextResponse.json({ stats });
35+
}

src/app/blog/[slug]/page.module.css

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@
5858
.tag {
5959
padding: 0.4rem 1rem;
6060
background-color: var(--N50);
61-
border-radius: 4px;
61+
border-radius: var(--radius-sm);
6262
font-size: 1.3rem;
6363
color: var(--N600);
6464
}
@@ -67,7 +67,7 @@
6767
position: relative;
6868
width: 100%;
6969
padding-bottom: 56.25%;
70-
border-radius: 12px;
70+
border-radius: var(--radius-lg);
7171
overflow: hidden;
7272
background-color: var(--N50);
7373
margin-top: 2rem;
@@ -121,7 +121,7 @@
121121
}
122122

123123
.coverImage {
124-
border-radius: 16px;
124+
border-radius: var(--radius-xl);
125125
margin-top: 3rem;
126126
}
127127

src/app/blog/page.module.css

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@
4141
display: flex;
4242
flex-direction: column;
4343
border: 1px solid var(--N100);
44-
border-radius: 12px;
44+
border-radius: var(--radius-lg);
4545
overflow: hidden;
4646
text-decoration: none;
4747
color: inherit;
@@ -94,9 +94,29 @@
9494
gap: 0.8rem;
9595
font-size: 1.3rem;
9696
color: var(--N400);
97+
margin-bottom: 0.8rem;
98+
}
99+
100+
.stats {
101+
display: flex;
102+
align-items: center;
103+
gap: 1.2rem;
104+
font-size: 1.3rem;
105+
color: var(--N400);
97106
margin-bottom: 1.2rem;
98107
}
99108

109+
.stat {
110+
display: flex;
111+
align-items: center;
112+
gap: 0.4rem;
113+
}
114+
115+
.statIcon {
116+
width: 1.4rem;
117+
height: 1.4rem;
118+
}
119+
100120
.separator {
101121
color: var(--N200);
102122
}
@@ -110,7 +130,7 @@
110130
.tag {
111131
padding: 0.3rem 0.8rem;
112132
background-color: var(--N50);
113-
border-radius: 4px;
133+
border-radius: var(--radius-sm);
114134
font-size: 1.2rem;
115135
color: var(--N600);
116136
}

src/app/blog/page.tsx

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,19 @@
11
import { Metadata } from "next";
22
import Link from "next/link";
33
import Image from "next/image";
4-
import { getAllPosts, formatDate } from "@/lib/blog";
4+
import { FiEye, FiHeart } from "react-icons/fi";
5+
import { getAllPosts, formatDate, getPostsStats } from "@/lib/blog";
56
import styles from "./page.module.css";
67

78
export const metadata: Metadata = {
89
title: "Blog | Oleksandr Ratushnyi",
910
description: "Articles about web development, JavaScript, React, and more.",
1011
};
1112

12-
export default function BlogPage() {
13+
export default async function BlogPage() {
1314
const posts = getAllPosts();
15+
const slugs = posts.map((post) => post.slug);
16+
const stats = await getPostsStats(slugs);
1417

1518
return (
1619
<main className={styles.main}>
@@ -52,6 +55,16 @@ export default function BlogPage() {
5255
<span className={styles.separator}>·</span>
5356
<span className={styles.readingTime}>{post.readingTime}</span>
5457
</div>
58+
<div className={styles.stats}>
59+
<span className={styles.stat}>
60+
<FiEye className={styles.statIcon} />
61+
{stats[post.slug]?.views ?? 0}
62+
</span>
63+
<span className={styles.stat}>
64+
<FiHeart className={styles.statIcon} />
65+
{stats[post.slug]?.likes ?? 0}
66+
</span>
67+
</div>
5568
{post.tags.length > 0 && (
5669
<div className={styles.tags}>
5770
{post.tags.slice(0, 3).map((tag) => (

src/app/globals.css

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,15 @@
8282
--SECTION_OFFSET: 8rem;
8383

8484
--S500: #78ff1c;
85+
86+
/* Border Radius Scale */
87+
--radius-xs: 2px; /* inline code, tiny elements */
88+
--radius-sm: 4px; /* tags, small buttons, badges */
89+
--radius-md: 8px; /* cards, inputs, code blocks */
90+
--radius-lg: 12px; /* large cards, containers */
91+
--radius-xl: 16px; /* hero images, feature sections */
92+
--radius-full: 9999px; /* pills, fully rounded */
93+
--radius-round: 50%; /* circles */
8594
}
8695

8796
* {
@@ -139,7 +148,7 @@ a, button {
139148
/* Syntax highlighting (rehype-pretty-code) */
140149
[data-rehype-pretty-code-figure] {
141150
margin: 2.5rem 0;
142-
border-radius: 12px;
151+
border-radius: var(--radius-lg);
143152
overflow: hidden;
144153
background-color: var(--N900);
145154
}
@@ -182,7 +191,7 @@ a, button {
182191
:not(pre) > code {
183192
background-color: var(--N30);
184193
padding: 0.2rem 0.5rem;
185-
border-radius: 4px;
194+
border-radius: var(--radius-sm);
186195
font-size: 0.9em;
187196
font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace;
188197
}
@@ -194,6 +203,6 @@ a, button {
194203
}
195204

196205
[data-rehype-pretty-code-figure] {
197-
border-radius: 8px;
206+
border-radius: var(--radius-md);
198207
}
199208
}

src/app/page.module.css

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
height: 8px;
2525
bottom: 4px;
2626
left: 7px;
27-
border-radius: 2px;
27+
border-radius: var(--radius-xs);
2828
position: absolute;
2929
}
3030

src/components/Article/Article.module.css

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@
9797
.article code:not(pre code) {
9898
background-color: var(--N50);
9999
padding: 0.2rem 0.5rem;
100-
border-radius: 4px;
100+
border-radius: var(--radius-sm);
101101
font-size: 0.9em;
102102
font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace;
103103
}

src/components/ArticleImage/ArticleImage.module.css

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
position: relative;
77
width: 100%;
88
padding-bottom: 66.67%;
9-
border-radius: 8px;
9+
border-radius: var(--radius-md);
1010
overflow: hidden;
1111
background-color: var(--N50);
1212
}
@@ -46,7 +46,7 @@
4646

4747
@media screen and (min-width: 768px) {
4848
.imageWrapper {
49-
border-radius: 12px;
49+
border-radius: var(--radius-lg);
5050
}
5151

5252
.fullWidth .imageWrapper {

src/components/ArticleSlider/ArticleSlider.module.css

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828
position: relative;
2929
width: 100%;
3030
padding-bottom: 66.67%;
31-
border-radius: 8px;
31+
border-radius: var(--radius-md);
3232
overflow: hidden;
3333
background-color: var(--N50);
3434
}
@@ -55,7 +55,7 @@
5555
.dot {
5656
width: 8px;
5757
height: 8px;
58-
border-radius: 50%;
58+
border-radius: var(--radius-round);
5959
border: none;
6060
background-color: var(--N200);
6161
cursor: pointer;
@@ -79,13 +79,13 @@
7979
background-color: rgba(0, 0, 0, 0.6);
8080
color: white;
8181
padding: 0.4rem 0.8rem;
82-
border-radius: 4px;
82+
border-radius: var(--radius-sm);
8383
font-size: 1.2rem;
8484
font-weight: 500;
8585
}
8686

8787
@media screen and (min-width: 768px) {
8888
.imageWrapper {
89-
border-radius: 12px;
89+
border-radius: var(--radius-lg);
9090
}
9191
}

src/components/Callout/Callout.module.css

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
gap: 1.2rem;
55
margin: 2rem 0;
66
padding: 1.2rem 1.4rem;
7-
border-radius: 4px;
7+
border-radius: var(--radius-md);
88
background-color: var(--N20);
99
}
1010

0 commit comments

Comments
 (0)