Skip to content

Commit fed94e4

Browse files
Merge pull request #176 from DevLoversTeam/sanity
2 parents aa5654b + 0cf755c commit fed94e4

14 files changed

Lines changed: 583 additions & 348 deletions

File tree

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

Lines changed: 114 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,28 @@ function plainTextFromPortableText(value: any): string {
4545
.trim();
4646
}
4747

48+
function linkifyText(text: string) {
49+
const urlRegex = /(https?:\/\/[^\s]+)/g;
50+
const parts = text.split(urlRegex);
51+
return parts.map((part, index) => {
52+
if (!part) return null;
53+
if (urlRegex.test(part)) {
54+
return (
55+
<a
56+
key={`link-${index}`}
57+
href={part}
58+
target="_blank"
59+
rel="noopener noreferrer"
60+
className="text-[var(--accent-primary)] underline underline-offset-4"
61+
>
62+
{part}
63+
</a>
64+
);
65+
}
66+
return <span key={`text-${index}`}>{part}</span>;
67+
});
68+
}
69+
4870
function seededShuffle<T>(items: T[], seed: number) {
4971
const result = [...items];
5072
let value = seed;
@@ -103,6 +125,13 @@ const recommendedQuery = groq`
103125
"author": author->{
104126
"name": coalesce(name[$locale], name[lower($locale)], name.uk, name.en, name.pl, name),
105127
"image": image.asset->url
128+
},
129+
"body": coalesce(body[$locale], body[lower($locale)], body.uk, body.en, body.pl, body)[]{
130+
...,
131+
_type == "image" => {
132+
...,
133+
"url": asset->url
134+
}
106135
}
107136
}
108137
`;
@@ -115,6 +144,7 @@ export default async function PostDetails({
115144
locale: string;
116145
}) {
117146
const t = await getTranslations({ locale, namespace: 'blog' });
147+
const tNav = await getTranslations({ locale, namespace: 'navigation' });
118148
const slugParam = String(slug || '').trim();
119149
if (!slugParam) return notFound();
120150

@@ -145,24 +175,32 @@ export default async function PostDetails({
145175
post.author?.city,
146176
].filter(Boolean) as string[];
147177
const authorMeta = authorMetaParts.join(' · ');
178+
const categoryLabel = post.categories?.[0];
148179

149180
return (
150181
<main className="max-w-3xl mx-auto px-6 py-12">
151-
<Link
152-
href="/blog"
153-
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"
154-
>
155-
<span>&larr;</span>
156-
<span>{t('goBack')}</span>
157-
</Link>
182+
<div className="mb-6 relative left-1/2 right-1/2 w-screen -translate-x-1/2 px-6">
183+
<div className="mx-auto flex max-w-6xl justify-start">
184+
<div className="flex items-center gap-2 text-sm text-gray-500 dark:text-gray-400">
185+
<Link
186+
href="/blog"
187+
className="transition hover:text-[var(--accent-primary)] hover:underline underline-offset-4"
188+
>
189+
{tNav('blog')}
190+
</Link>
191+
<span>&gt;</span>
192+
<span className="text-[var(--accent-primary)]">{post.title}</span>
193+
</div>
194+
</div>
195+
</div>
158196

159-
{post.categories?.[0] && (
197+
{categoryLabel && (
160198
<div className="text-sm font-medium text-gray-500 dark:text-gray-400 text-center">
161199
<Link
162-
href={`/blog?category=${encodeURIComponent(post.categories[0])}`}
163-
className="inline-flex items-center gap-1 hover:text-[#ff00ff] transition"
200+
href={`/blog?category=${encodeURIComponent(categoryLabel)}`}
201+
className="inline-flex items-center gap-1 text-[var(--accent-primary)] transition"
164202
>
165-
{post.categories[0] === 'Growth' ? 'Career' : post.categories[0]}
203+
{categoryLabel === 'Growth' ? 'Career' : categoryLabel}
166204
</Link>
167205
</div>
168206
)}
@@ -172,7 +210,14 @@ export default async function PostDetails({
172210

173211
{(authorName || post.publishedAt) && (
174212
<div className="mt-4 flex justify-center gap-2 text-sm text-gray-500 dark:text-gray-400">
175-
{authorName && <span>{authorName}</span>}
213+
{authorName && (
214+
<Link
215+
href={`/blog?author=${encodeURIComponent(authorName)}`}
216+
className="transition hover:text-[var(--accent-primary)]"
217+
>
218+
{authorName}
219+
</Link>
220+
)}
176221
{authorName && post.publishedAt && <span>·</span>}
177222
{post.publishedAt && (
178223
<span>{new Date(post.publishedAt).toLocaleDateString()}</span>
@@ -199,7 +244,14 @@ export default async function PostDetails({
199244
const text = (block.children || [])
200245
.map((c: any) => c.text || '')
201246
.join('');
202-
return <p key={block._key || `block-${index}`}>{text}</p>;
247+
return (
248+
<p
249+
key={block._key || `block-${index}`}
250+
className="whitespace-pre-line"
251+
>
252+
{linkifyText(text)}
253+
</p>
254+
);
203255
}
204256

205257
if (block?._type === 'image' && block?.url) {
@@ -219,57 +271,64 @@ export default async function PostDetails({
219271

220272
{recommendedPosts.length > 0 && (
221273
<>
222-
<div className="mt-16 flex justify-center">
223-
<div className="h-10 w-px bg-gray-200 dark:bg-gray-800" />
274+
<div className="mt-16 relative left-1/2 right-1/2 w-screen -translate-x-1/2 px-6">
275+
<div className="mx-auto h-px w-full max-w-6xl bg-gray-200 dark:bg-gray-800" />
224276
</div>
225277

226-
<section className="mt-10">
227-
<h2 className="text-2xl font-semibold text-gray-900 dark:text-gray-100">
228-
{t('recommendedPosts')}
229-
</h2>
230-
<div className="mt-6 grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
231-
{recommendedPosts.map(item => (
232-
<Link
233-
key={item._id}
234-
href={`/blog/${item.slug?.current}`}
235-
className="group block"
236-
>
237-
{item.mainImage && (
238-
<div className="relative h-44 w-full overflow-hidden rounded-2xl">
239-
<Image
240-
src={item.mainImage}
241-
alt={item.title || 'Post image'}
242-
fill
243-
className="object-cover transition-transform duration-300 group-hover:scale-[1.03]"
244-
/>
245-
</div>
246-
)}
247-
<h3 className="mt-4 text-lg font-semibold text-gray-900 transition group-hover:text-[#ff00ff] dark:text-gray-100">
278+
<section className="mt-10 relative left-1/2 right-1/2 w-screen -translate-x-1/2 px-6">
279+
<div className="mx-auto max-w-6xl">
280+
<h2 className="text-2xl font-semibold text-gray-900 dark:text-gray-100">
281+
{t('recommendedPosts')}
282+
</h2>
283+
<div className="mt-6 grid gap-6 auto-rows-fr sm:grid-cols-2 lg:grid-cols-3">
284+
{recommendedPosts.map(item => (
285+
<Link
286+
key={item._id}
287+
href={`/blog/${item.slug?.current}`}
288+
className="group flex h-full flex-col"
289+
>
290+
{item.mainImage && (
291+
<div className="relative h-48 w-full overflow-hidden rounded-2xl">
292+
<Image
293+
src={item.mainImage}
294+
alt={item.title || 'Post image'}
295+
fill
296+
className="object-cover transition-transform duration-300 group-hover:scale-[1.03]"
297+
/>
298+
</div>
299+
)}
300+
<h3 className="mt-4 text-lg font-semibold text-gray-900 transition group-hover:underline underline-offset-4 dark:text-gray-100">
248301
{item.title}
249302
</h3>
303+
{item.body && (
304+
<p className="mt-2 text-sm leading-relaxed text-gray-600 dark:text-gray-400 line-clamp-2">
305+
{plainTextFromPortableText(item.body)}
306+
</p>
307+
)}
250308
{(item.author?.name || item.publishedAt) && (
251-
<div className="mt-2 flex items-center gap-2 text-sm text-gray-500 dark:text-gray-400">
309+
<div className="mt-auto pt-3 flex items-center gap-2 text-sm text-gray-500 dark:text-gray-400">
252310
{item.author?.image && (
253311
<span className="relative h-5 w-5 overflow-hidden rounded-full">
254312
<Image
255313
src={item.author.image}
256-
alt={item.author.name || 'Author'}
257-
fill
258-
className="object-cover"
259-
/>
260-
</span>
261-
)}
262-
{item.author?.name && <span>{item.author.name}</span>}
263-
{item.author?.name && item.publishedAt && <span>·</span>}
264-
{item.publishedAt && (
265-
<span>
266-
{new Date(item.publishedAt).toLocaleDateString()}
267-
</span>
268-
)}
269-
</div>
270-
)}
271-
</Link>
272-
))}
314+
alt={item.author.name || 'Author'}
315+
fill
316+
className="object-cover"
317+
/>
318+
</span>
319+
)}
320+
{item.author?.name && <span>{item.author.name}</span>}
321+
{item.author?.name && item.publishedAt && <span>·</span>}
322+
{item.publishedAt && (
323+
<span>
324+
{new Date(item.publishedAt).toLocaleDateString()}
325+
</span>
326+
)}
327+
</div>
328+
)}
329+
</Link>
330+
))}
331+
</div>
273332
</div>
274333
</section>
275334
</>

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

Lines changed: 65 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import groq from 'groq';
22
import { notFound } from 'next/navigation';
33
import { getTranslations } from 'next-intl/server';
4+
import Image from 'next/image';
45
import { client } from '@/client';
6+
import { Link } from '@/i18n/routing';
57
import { BlogCategoryGrid } from '@/components/blog/BlogCategoryGrid';
68

79
export const revalidate = 0;
@@ -51,13 +53,11 @@ export default async function BlogCategoryPage({
5153

5254
if (!matchedCategory) return notFound();
5355
const categoryTitle = matchedCategory.title;
54-
const displayTitle =
55-
categoryTitle === 'Growth' ? 'Career' : categoryTitle;
5656

5757
const posts: Post[] = await client.withConfig({ useCdn: false }).fetch(
5858
groq`
5959
*[_type == "post" && defined(slug.current) && $category in categories[]->title]
60-
| order(publishedAt desc) {
60+
| order(coalesce(publishedAt, _createdAt) desc) {
6161
_id,
6262
"title": coalesce(title[$locale], title[lower($locale)], title.uk, title.en, title.pl, title),
6363
slug,
@@ -77,13 +77,73 @@ export default async function BlogCategoryPage({
7777
{ locale, category: categoryTitle }
7878
);
7979

80+
const featuredPost = posts[0];
81+
const restPosts = posts.slice(1);
82+
const featuredDate = featuredPost?.publishedAt
83+
? new Intl.DateTimeFormat(locale, {
84+
day: '2-digit',
85+
month: '2-digit',
86+
year: 'numeric',
87+
}).format(new Date(featuredPost.publishedAt))
88+
: '';
89+
8090
return (
8191
<main className="max-w-6xl mx-auto px-6 py-12">
8292
<h1 className="text-4xl font-bold mb-4 text-center">
83-
{displayTitle}
93+
{categoryTitle}
8494
</h1>
95+
{featuredPost?.mainImage && (
96+
<section className="mt-10">
97+
<article className="group relative overflow-hidden rounded-3xl bg-white dark:bg-black">
98+
<div className="h-[320px] w-full overflow-hidden sm:h-[380px] md:h-[450px] lg:h-[618px] max-h-[65vh]">
99+
<Image
100+
src={featuredPost.mainImage}
101+
alt={featuredPost.title}
102+
width={1400}
103+
height={800}
104+
className="h-full w-full object-cover transition-transform duration-300 group-hover:scale-[1.03]"
105+
priority={false}
106+
/>
107+
</div>
108+
<div className="pointer-events-none absolute inset-x-0 bottom-0 h-56 bg-gradient-to-t from-white/95 via-white/70 to-transparent dark:from-black/90 dark:via-black/60 sm:h-64" />
109+
<div className="absolute inset-x-0 bottom-0 p-6 sm:p-8">
110+
{featuredPost.categories?.[0] && (
111+
<div className="text-sm font-medium text-gray-900 dark:text-gray-100">
112+
{featuredPost.categories[0]}
113+
</div>
114+
)}
115+
<h2 className="mt-2 text-3xl font-semibold text-gray-900 transition group-hover:text-[var(--accent-primary)] dark:text-white dark:group-hover:text-[var(--accent-primary)] sm:text-4xl">
116+
{featuredPost.title}
117+
</h2>
118+
<div className="mt-3 flex items-center gap-3 text-sm text-gray-800 dark:text-gray-200">
119+
{featuredPost.author?.image && (
120+
<Image
121+
src={featuredPost.author.image}
122+
alt={featuredPost.author.name || 'Author'}
123+
width={28}
124+
height={28}
125+
className="h-7 w-7 rounded-full object-cover"
126+
/>
127+
)}
128+
{featuredPost.author?.name && (
129+
<span>{featuredPost.author.name}</span>
130+
)}
131+
{featuredPost.author?.name && featuredDate && <span>·</span>}
132+
{featuredDate && <span>{featuredDate}</span>}
133+
</div>
134+
<Link
135+
href={`/blog/${featuredPost.slug.current}`}
136+
className="absolute bottom-6 right-6 inline-flex h-11 w-11 items-center justify-center rounded-full bg-[var(--accent-primary)] text-white opacity-0 transition group-hover:opacity-100 hover:brightness-110"
137+
aria-label={featuredPost.title}
138+
>
139+
<span aria-hidden="true"></span>
140+
</Link>
141+
</div>
142+
</article>
143+
</section>
144+
)}
85145
<div className="mt-12">
86-
<BlogCategoryGrid posts={posts} />
146+
<BlogCategoryGrid posts={restPosts} />
87147
</div>
88148
{!posts.length && (
89149
<p className="text-center text-gray-500 mt-10">{t('noPosts')}</p>

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import groq from 'groq';
2+
import { unstable_noStore as noStore } from 'next/cache';
23
import { getTranslations } from 'next-intl/server';
34
import { client } from '@/client';
45
import BlogFilters from '@/components/blog/BlogFilters';
@@ -24,13 +25,14 @@ export default async function BlogPage({
2425
}: {
2526
params: Promise<{ locale: string }>;
2627
}) {
28+
noStore();
2729
const { locale } = await params;
2830
const t = await getTranslations({ locale, namespace: 'blog' });
2931

3032
const posts = await client.withConfig({ useCdn: false }).fetch(
3133
groq`
3234
*[_type == "post" && defined(slug.current)]
33-
| order(publishedAt desc) {
35+
| order(coalesce(publishedAt, _createdAt) desc) {
3436
_id,
3537
"title": coalesce(title[$locale], title[lower($locale)], title.uk, title.en, title.pl, title),
3638
slug,

frontend/client.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,6 @@ import { createClient } from "@sanity/client";
33
export const client = createClient({
44
projectId: "6y9ive6v",
55
dataset: "production",
6-
useCdn: true,
6+
useCdn: false,
77
apiVersion: "2025-11-29",
88
});

0 commit comments

Comments
 (0)