Skip to content

Commit 0109871

Browse files
Merge pull request #145 from DevLoversTeam/sanity
Sanity
2 parents a63a90a + edf62b2 commit 0109871

14 files changed

Lines changed: 9499 additions & 8180 deletions

File tree

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

Lines changed: 140 additions & 79 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 {
@@ -40,26 +43,46 @@ function plainTextFromPortableText(value: any): string {
4043
.trim();
4144
}
4245

46+
function seededShuffle<T>(items: T[], seed: number) {
47+
const result = [...items];
48+
let value = seed;
49+
for (let i = result.length - 1; i > 0; i -= 1) {
50+
value = (value * 1664525 + 1013904223) % 4294967296;
51+
const j = value % (i + 1);
52+
[result[i], result[j]] = [result[j], result[i]];
53+
}
54+
return result;
55+
}
56+
57+
function hashString(input: string) {
58+
let hash = 0;
59+
for (let i = 0; i < input.length; i += 1) {
60+
hash = (hash * 31 + input.charCodeAt(i)) >>> 0;
61+
}
62+
return hash;
63+
}
64+
4365
const query = groq`
4466
*[_type=="post" && slug.current==$slug][0]{
45-
"title": coalesce(title[$locale], title.en, title),
67+
_id,
68+
"title": coalesce(title[$locale], title[lower($locale)], title.uk, title.en, title.pl, title),
4669
publishedAt,
4770
"mainImage": mainImage.asset->url,
4871
"categories": categories[]->title,
4972
tags,
5073
resourceLink,
5174
5275
"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),
76+
"name": coalesce(name[$locale], name[lower($locale)], name.uk, name.en, name.pl, name),
77+
"company": coalesce(company[$locale], company[lower($locale)], company.uk, company.en, company.pl, company),
78+
"jobTitle": coalesce(jobTitle[$locale], jobTitle[lower($locale)], jobTitle.uk, jobTitle.en, jobTitle.pl, jobTitle),
79+
"city": coalesce(city[$locale], city[lower($locale)], city.uk, city.en, city.pl, city),
80+
"bio": coalesce(bio[$locale], bio[lower($locale)], bio.uk, bio.en, bio.pl, bio),
5881
"image": image.asset->url,
5982
socialMedia[]{ _key, platform, url }
6083
},
6184
62-
"body": coalesce(body[$locale], body.en, body)[]{
85+
"body": coalesce(body[$locale], body[lower($locale)], body.uk, body.en, body.pl, body)[]{
6386
...,
6487
_type == "image" => {
6588
...,
@@ -68,6 +91,19 @@ const query = groq`
6891
}
6992
}
7093
`;
94+
const recommendedQuery = groq`
95+
*[_type=="post" && defined(slug.current) && slug.current != $slug]{
96+
_id,
97+
"title": coalesce(title[$locale], title[lower($locale)], title.uk, title.en, title.pl, title),
98+
publishedAt,
99+
"mainImage": mainImage.asset->url,
100+
slug,
101+
"author": author->{
102+
"name": coalesce(name[$locale], name[lower($locale)], name.uk, name.en, name.pl, name),
103+
"image": image.asset->url
104+
}
105+
}
106+
`;
71107

72108
export default async function PostDetails({
73109
slug,
@@ -84,6 +120,14 @@ export default async function PostDetails({
84120
slug: slugParam,
85121
locale,
86122
});
123+
const recommendedAll: Post[] = await client.fetch(recommendedQuery, {
124+
slug: slugParam,
125+
locale,
126+
});
127+
const recommendedPosts = seededShuffle(
128+
recommendedAll,
129+
hashString(slugParam)
130+
).slice(0, 3);
87131

88132
if (!post?.title) return notFound();
89133

@@ -98,38 +142,40 @@ export default async function PostDetails({
98142

99143
return (
100144
<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-
))}
145+
<Link
146+
href="/blog"
147+
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"
148+
>
149+
<span>&larr;</span>
150+
<span>{t('goBack')}</span>
151+
</Link>
152+
153+
{post.categories?.[0] && (
154+
<div className="text-sm font-medium text-gray-500 dark:text-gray-400 text-center">
155+
<Link
156+
href={`/blog?category=${encodeURIComponent(post.categories[0])}`}
157+
className="inline-flex items-center gap-1 hover:text-[#ff00ff] transition"
158+
>
159+
{post.categories[0]}
160+
</Link>
117161
</div>
118162
)}
163+
<h1 className="mt-3 text-4xl font-bold text-gray-900 dark:text-gray-100 text-center">
164+
{post.title}
165+
</h1>
119166

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-
))}
167+
{(authorName || post.publishedAt) && (
168+
<div className="mt-4 flex justify-center gap-2 text-sm text-gray-500 dark:text-gray-400">
169+
{authorName && <span>{authorName}</span>}
170+
{authorName && post.publishedAt && <span>·</span>}
171+
{post.publishedAt && (
172+
<span>{new Date(post.publishedAt).toLocaleDateString()}</span>
173+
)}
130174
</div>
131175
)}
132176

177+
{(post.tags?.length || 0) > 0 && null}
178+
133179
{post.mainImage && (
134180
<div className="relative w-full h-[420px] rounded-2xl overflow-hidden border border-gray-200 my-8">
135181
<Image
@@ -142,18 +188,18 @@ export default async function PostDetails({
142188
)}
143189

144190
<article className="prose prose-gray max-w-none">
145-
{post.body?.map((block: any) => {
191+
{post.body?.map((block: any, index: number) => {
146192
if (block?._type === 'block') {
147193
const text = (block.children || [])
148194
.map((c: any) => c.text || '')
149195
.join('');
150-
return <p key={block._key || Math.random()}>{text}</p>;
196+
return <p key={block._key || `block-${index}`}>{text}</p>;
151197
}
152198

153199
if (block?._type === 'image' && block?.url) {
154200
return (
155201
<img
156-
key={block._key || block.url}
202+
key={block._key || `image-${index}`}
157203
src={block.url}
158204
alt={post.title || 'Post image'}
159205
className="rounded-xl border border-gray-200 my-6"
@@ -165,52 +211,67 @@ export default async function PostDetails({
165211
})}
166212
</article>
167213

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-
)}
214+
{recommendedPosts.length > 0 && (
215+
<>
216+
<div className="mt-16 flex justify-center">
217+
<div className="h-10 w-px bg-gray-200 dark:bg-gray-800" />
218+
</div>
180219

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-
)}
220+
<section className="mt-10">
221+
<h2 className="text-2xl font-semibold text-gray-900 dark:text-gray-100">
222+
{t('recommendedPosts')}
223+
</h2>
224+
<div className="mt-6 grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
225+
{recommendedPosts.map(item => (
226+
<Link
227+
key={item._id}
228+
href={`/blog/${item.slug?.current}`}
229+
className="group block"
230+
>
231+
{item.mainImage && (
232+
<div className="relative h-44 w-full overflow-hidden rounded-2xl">
233+
<Image
234+
src={item.mainImage}
235+
alt={item.title || 'Post image'}
236+
fill
237+
className="object-cover transition-transform duration-300 group-hover:scale-[1.03]"
238+
/>
239+
</div>
240+
)}
241+
<h3 className="mt-4 text-lg font-semibold text-gray-900 transition group-hover:text-[#ff00ff] dark:text-gray-100">
242+
{item.title}
243+
</h3>
244+
{(item.author?.name || item.publishedAt) && (
245+
<div className="mt-2 flex items-center gap-2 text-sm text-gray-500 dark:text-gray-400">
246+
{item.author?.image && (
247+
<span className="relative h-5 w-5 overflow-hidden rounded-full">
248+
<Image
249+
src={item.author.image}
250+
alt={item.author.name || 'Author'}
251+
fill
252+
className="object-cover"
253+
/>
254+
</span>
255+
)}
256+
{item.author?.name && <span>{item.author.name}</span>}
257+
{item.author?.name && item.publishedAt && <span>·</span>}
258+
{item.publishedAt && (
259+
<span>
260+
{new Date(item.publishedAt).toLocaleDateString()}
261+
</span>
262+
)}
263+
</div>
264+
)}
265+
</Link>
266+
))}
210267
</div>
211-
</div>
212-
</section>
268+
</section>
269+
</>
213270
)}
271+
272+
{post.resourceLink && null}
273+
274+
{(authorBio || authorName || authorMeta) && null}
214275
</main>
215276
);
216277
}

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)