Skip to content

Commit 7c10419

Browse files
committed
feat(blog): redesign
1 parent 4d94451 commit 7c10419

13 files changed

Lines changed: 874 additions & 331 deletions

File tree

frontend/app/[locale]/blog/[slug]/PostDetails.tsx

Lines changed: 117 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { notFound } from 'next/navigation';
33
import groq from 'groq';
44
import { getTranslations } from 'next-intl/server';
55
import { client } from '@/client';
6+
import { Link } from '@/i18n/routing';
67

78
type SocialLink = {
89
_key?: string;
@@ -21,6 +22,7 @@ type Author = {
2122
};
2223

2324
type Post = {
25+
_id?: string;
2426
title?: string;
2527
publishedAt?: string;
2628
mainImage?: string;
@@ -29,6 +31,7 @@ type Post = {
2931
resourceLink?: string;
3032
author?: Author;
3133
body?: any[];
34+
slug?: { current?: string };
3235
};
3336

3437
function plainTextFromPortableText(value: any): string {
@@ -42,24 +45,25 @@ function plainTextFromPortableText(value: any): string {
4245

4346
const query = groq`
4447
*[_type=="post" && slug.current==$slug][0]{
45-
"title": coalesce(title[$locale], title.en, title),
48+
_id,
49+
"title": coalesce(title[$locale], title[lower($locale)], title.uk, title.en, title.pl, title),
4650
publishedAt,
4751
"mainImage": mainImage.asset->url,
4852
"categories": categories[]->title,
4953
tags,
5054
resourceLink,
5155
5256
"author": author->{
53-
"name": coalesce(name[$locale], name.en, name),
54-
"company": coalesce(company[$locale], company.en, company),
55-
"jobTitle": coalesce(jobTitle[$locale], jobTitle.en, jobTitle),
56-
"city": coalesce(city[$locale], city.en, city),
57-
"bio": coalesce(bio[$locale], bio.en, bio),
57+
"name": coalesce(name[$locale], name[lower($locale)], name.uk, name.en, name.pl, name),
58+
"company": coalesce(company[$locale], company[lower($locale)], company.uk, company.en, company.pl, company),
59+
"jobTitle": coalesce(jobTitle[$locale], jobTitle[lower($locale)], jobTitle.uk, jobTitle.en, jobTitle.pl, jobTitle),
60+
"city": coalesce(city[$locale], city[lower($locale)], city.uk, city.en, city.pl, city),
61+
"bio": coalesce(bio[$locale], bio[lower($locale)], bio.uk, bio.en, bio.pl, bio),
5862
"image": image.asset->url,
5963
socialMedia[]{ _key, platform, url }
6064
},
6165
62-
"body": coalesce(body[$locale], body.en, body)[]{
66+
"body": coalesce(body[$locale], body[lower($locale)], body.uk, body.en, body.pl, body)[]{
6367
...,
6468
_type == "image" => {
6569
...,
@@ -68,6 +72,19 @@ const query = groq`
6872
}
6973
}
7074
`;
75+
const recommendedQuery = groq`
76+
*[_type=="post" && defined(slug.current) && slug.current != $slug]{
77+
_id,
78+
"title": coalesce(title[$locale], title[lower($locale)], title.uk, title.en, title.pl, title),
79+
publishedAt,
80+
"mainImage": mainImage.asset->url,
81+
slug,
82+
"author": author->{
83+
"name": coalesce(name[$locale], name[lower($locale)], name.uk, name.en, name.pl, name),
84+
"image": image.asset->url
85+
}
86+
}
87+
`;
7188

7289
export default async function PostDetails({
7390
slug,
@@ -84,6 +101,13 @@ export default async function PostDetails({
84101
slug: slugParam,
85102
locale,
86103
});
104+
const recommendedAll: Post[] = await client.fetch(recommendedQuery, {
105+
slug: slugParam,
106+
locale,
107+
});
108+
const recommendedPosts = recommendedAll
109+
.sort(() => Math.random() - 0.5)
110+
.slice(0, 3);
87111

88112
if (!post?.title) return notFound();
89113

@@ -98,38 +122,40 @@ export default async function PostDetails({
98122

99123
return (
100124
<main className="max-w-3xl mx-auto px-6 py-12">
101-
<h1 className="text-4xl font-bold text-gray-900">{post.title}</h1>
102-
103-
<div className="mt-4 text-sm text-gray-500">
104-
{post.publishedAt && new Date(post.publishedAt).toLocaleDateString()}
105-
</div>
106-
107-
{(post.categories?.length || 0) > 0 && (
108-
<div className="mt-4 flex flex-wrap gap-2">
109-
{post.categories!.map((cat, i) => (
110-
<span
111-
key={`${cat}-${i}`}
112-
className="text-xs bg-blue-50 text-blue-700 px-2 py-1 rounded-md"
113-
>
114-
{cat}
115-
</span>
116-
))}
125+
<Link
126+
href="/blog"
127+
className="inline-flex items-center gap-2 text-sm text-gray-600 border-b border-current transition hover:text-[#ff00ff] hover:bg-sky-50 hover:shadow-[0_6px_18px_rgba(56,189,248,0.18)] dark:text-gray-300 dark:hover:bg-sky-900/20"
128+
>
129+
<span>&larr;</span>
130+
<span>{t('goBack')}</span>
131+
</Link>
132+
133+
{post.categories?.[0] && (
134+
<div className="text-sm font-medium text-gray-500 dark:text-gray-400 text-center">
135+
<Link
136+
href={`/blog?category=${encodeURIComponent(post.categories[0])}`}
137+
className="inline-flex items-center gap-1 hover:text-[#ff00ff] transition"
138+
>
139+
{post.categories[0]}
140+
</Link>
117141
</div>
118142
)}
143+
<h1 className="mt-3 text-4xl font-bold text-gray-900 dark:text-gray-100 text-center">
144+
{post.title}
145+
</h1>
119146

120-
{(post.tags?.length || 0) > 0 && (
121-
<div className="mt-3 flex flex-wrap gap-2">
122-
{post.tags!.map((tag, i) => (
123-
<span
124-
key={`${tag}-${i}`}
125-
className="text-xs bg-purple-50 text-purple-700 px-2 py-1 rounded-md"
126-
>
127-
#{tag}
128-
</span>
129-
))}
147+
{(authorName || post.publishedAt) && (
148+
<div className="mt-4 flex justify-center gap-2 text-sm text-gray-500 dark:text-gray-400">
149+
{authorName && <span>{authorName}</span>}
150+
{authorName && post.publishedAt && <span>·</span>}
151+
{post.publishedAt && (
152+
<span>{new Date(post.publishedAt).toLocaleDateString()}</span>
153+
)}
130154
</div>
131155
)}
132156

157+
{(post.tags?.length || 0) > 0 && null}
158+
133159
{post.mainImage && (
134160
<div className="relative w-full h-[420px] rounded-2xl overflow-hidden border border-gray-200 my-8">
135161
<Image
@@ -165,52 +191,67 @@ export default async function PostDetails({
165191
})}
166192
</article>
167193

168-
{post.resourceLink && (
169-
<div className="mt-10">
170-
<a
171-
href={post.resourceLink}
172-
target="_blank"
173-
rel="noopener noreferrer"
174-
className="inline-flex bg-green-600 text-white px-5 py-3 rounded-lg hover:bg-green-700 transition"
175-
>
176-
Visit Resource →
177-
</a>
178-
</div>
179-
)}
194+
{recommendedPosts.length > 0 && (
195+
<>
196+
<div className="mt-16 flex justify-center">
197+
<div className="h-10 w-px bg-gray-200 dark:bg-gray-800" />
198+
</div>
180199

181-
{(authorBio || authorName || authorMeta) && (
182-
<section className="mt-12 p-6 rounded-2xl border border-gray-200 bg-white">
183-
<h2 className="text-lg font-semibold">{t('aboutAuthor')}</h2>
184-
<div className="mt-4 flex items-start gap-4">
185-
{post.author?.image && (
186-
<div className="relative w-14 h-14 shrink-0">
187-
<Image
188-
src={post.author.image}
189-
alt={authorName || 'Author'}
190-
fill
191-
className="rounded-full object-cover border border-gray-200"
192-
/>
193-
</div>
194-
)}
195-
196-
<div className="min-w-0">
197-
{authorName && (
198-
<p className="text-sm font-semibold text-gray-900">
199-
{authorName}
200-
</p>
201-
)}
202-
{authorMeta && (
203-
<p className="mt-1 text-sm text-gray-600">{authorMeta}</p>
204-
)}
205-
{authorBio && (
206-
<p className="mt-3 text-sm text-gray-700 whitespace-pre-line leading-relaxed">
207-
{authorBio}
208-
</p>
209-
)}
200+
<section className="mt-10">
201+
<h2 className="text-2xl font-semibold text-gray-900 dark:text-gray-100">
202+
{t('recommendedPosts')}
203+
</h2>
204+
<div className="mt-6 grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
205+
{recommendedPosts.map(item => (
206+
<Link
207+
key={item._id}
208+
href={`/blog/${item.slug?.current}`}
209+
className="group block"
210+
>
211+
{item.mainImage && (
212+
<div className="relative h-44 w-full overflow-hidden rounded-2xl">
213+
<Image
214+
src={item.mainImage}
215+
alt={item.title || 'Post image'}
216+
fill
217+
className="object-cover transition-transform duration-300 group-hover:scale-[1.03]"
218+
/>
219+
</div>
220+
)}
221+
<h3 className="mt-4 text-lg font-semibold text-gray-900 transition group-hover:text-[#ff00ff] dark:text-gray-100">
222+
{item.title}
223+
</h3>
224+
{(item.author?.name || item.publishedAt) && (
225+
<div className="mt-2 flex items-center gap-2 text-sm text-gray-500 dark:text-gray-400">
226+
{item.author?.image && (
227+
<span className="relative h-5 w-5 overflow-hidden rounded-full">
228+
<Image
229+
src={item.author.image}
230+
alt={item.author.name || 'Author'}
231+
fill
232+
className="object-cover"
233+
/>
234+
</span>
235+
)}
236+
{item.author?.name && <span>{item.author.name}</span>}
237+
{item.author?.name && item.publishedAt && <span>·</span>}
238+
{item.publishedAt && (
239+
<span>
240+
{new Date(item.publishedAt).toLocaleDateString()}
241+
</span>
242+
)}
243+
</div>
244+
)}
245+
</Link>
246+
))}
210247
</div>
211-
</div>
212-
</section>
248+
</section>
249+
</>
213250
)}
251+
252+
{post.resourceLink && null}
253+
254+
{(authorBio || authorName || authorMeta) && null}
214255
</main>
215256
);
216257
}

frontend/app/[locale]/blog/page.tsx

Lines changed: 25 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -30,27 +30,27 @@ export default async function BlogPage({
3030
*[_type == "post" && defined(slug.current)]
3131
| order(publishedAt desc) {
3232
_id,
33-
"title": coalesce(title[$locale], title.en, title),
33+
"title": coalesce(title[$locale], title[lower($locale)], title.uk, title.en, title.pl, title),
3434
slug,
3535
publishedAt,
3636
tags,
3737
resourceLink,
3838
3939
"categories": categories[]->title,
4040
41-
"body": coalesce(body[$locale], body.en, body)[]{
41+
"body": coalesce(body[$locale], body[lower($locale)], body.uk, body.en, body.pl, body)[]{
4242
...,
4343
children[]{
4444
text
4545
}
4646
},
4747
"mainImage": mainImage.asset->url,
4848
"author": author->{
49-
"name": coalesce(name[$locale], name.en, name),
50-
"company": coalesce(company[$locale], company.en, company),
51-
"jobTitle": coalesce(jobTitle[$locale], jobTitle.en, jobTitle),
52-
"city": coalesce(city[$locale], city.en, city),
53-
"bio": coalesce(bio[$locale], bio.en, bio),
49+
"name": coalesce(name[$locale], name[lower($locale)], name.uk, name.en, name.pl, name),
50+
"company": coalesce(company[$locale], company[lower($locale)], company.uk, company.en, company.pl, company),
51+
"jobTitle": coalesce(jobTitle[$locale], jobTitle[lower($locale)], jobTitle.uk, jobTitle.en, jobTitle.pl, jobTitle),
52+
"city": coalesce(city[$locale], city[lower($locale)], city.uk, city.en, city.pl, city),
53+
"bio": coalesce(bio[$locale], bio[lower($locale)], bio.uk, bio.en, bio.pl, bio),
5454
"image": image.asset->url,
5555
socialMedia[]{
5656
_key,
@@ -62,11 +62,27 @@ export default async function BlogPage({
6262
`,
6363
{ locale }
6464
);
65+
const categories = await client.fetch(
66+
groq`
67+
*[_type == "category"] | order(orderRank asc) {
68+
_id,
69+
title
70+
}
71+
`
72+
);
73+
const featuredPost = posts?.[0];
6574

6675
return (
6776
<main className="max-w-6xl mx-auto px-6 py-12">
68-
<h1 className="text-4xl font-bold mb-10 text-center">{t('title')}</h1>
69-
<BlogFilters posts={posts} />
77+
<h1 className="text-4xl font-bold mb-4 text-center">{t('title')}</h1>
78+
<p className="mx-auto max-w-2xl text-center text-base text-gray-500 dark:text-gray-400">
79+
{t('subtitle')}
80+
</p>
81+
<BlogFilters
82+
posts={posts}
83+
categories={categories}
84+
featuredPost={featuredPost}
85+
/>
7086
</main>
7187
);
7288
}

0 commit comments

Comments
 (0)