diff --git a/frontend/app/[locale]/blog/[slug]/PostDetails.tsx b/frontend/app/[locale]/blog/[slug]/PostDetails.tsx index 34169551..d1bcf0ce 100644 --- a/frontend/app/[locale]/blog/[slug]/PostDetails.tsx +++ b/frontend/app/[locale]/blog/[slug]/PostDetails.tsx @@ -45,6 +45,28 @@ function plainTextFromPortableText(value: any): string { .trim(); } +function linkifyText(text: string) { + const urlRegex = /(https?:\/\/[^\s]+)/g; + const parts = text.split(urlRegex); + return parts.map((part, index) => { + if (!part) return null; + if (urlRegex.test(part)) { + return ( + + {part} + + ); + } + return {part}; + }); +} + function seededShuffle(items: T[], seed: number) { const result = [...items]; let value = seed; @@ -103,6 +125,13 @@ const recommendedQuery = groq` "author": author->{ "name": coalesce(name[$locale], name[lower($locale)], name.uk, name.en, name.pl, name), "image": image.asset->url + }, + "body": coalesce(body[$locale], body[lower($locale)], body.uk, body.en, body.pl, body)[]{ + ..., + _type == "image" => { + ..., + "url": asset->url + } } } `; @@ -115,6 +144,7 @@ export default async function PostDetails({ locale: string; }) { const t = await getTranslations({ locale, namespace: 'blog' }); + const tNav = await getTranslations({ locale, namespace: 'navigation' }); const slugParam = String(slug || '').trim(); if (!slugParam) return notFound(); @@ -145,24 +175,32 @@ export default async function PostDetails({ post.author?.city, ].filter(Boolean) as string[]; const authorMeta = authorMetaParts.join(' · '); + const categoryLabel = post.categories?.[0]; return (
- - - {t('goBack')} - +
+
+
+ + {tNav('blog')} + + > + {post.title} +
+
+
- {post.categories?.[0] && ( + {categoryLabel && (
- {post.categories[0] === 'Growth' ? 'Career' : post.categories[0]} + {categoryLabel === 'Growth' ? 'Career' : categoryLabel}
)} @@ -172,7 +210,14 @@ export default async function PostDetails({ {(authorName || post.publishedAt) && (
- {authorName && {authorName}} + {authorName && ( + + {authorName} + + )} {authorName && post.publishedAt && ·} {post.publishedAt && ( {new Date(post.publishedAt).toLocaleDateString()} @@ -199,7 +244,14 @@ export default async function PostDetails({ const text = (block.children || []) .map((c: any) => c.text || '') .join(''); - return

{text}

; + return ( +

+ {linkifyText(text)} +

+ ); } if (block?._type === 'image' && block?.url) { @@ -219,57 +271,64 @@ export default async function PostDetails({ {recommendedPosts.length > 0 && ( <> -
-
+
+
-
-

- {t('recommendedPosts')} -

-
- {recommendedPosts.map(item => ( - - {item.mainImage && ( -
- {item.title -
- )} -

+
+
+

+ {t('recommendedPosts')} +

+
+ {recommendedPosts.map(item => ( + + {item.mainImage && ( +
+ {item.title +
+ )} +

{item.title}

+ {item.body && ( +

+ {plainTextFromPortableText(item.body)} +

+ )} {(item.author?.name || item.publishedAt) && ( -
+
{item.author?.image && ( {item.author.name - - )} - {item.author?.name && {item.author.name}} - {item.author?.name && item.publishedAt && ·} - {item.publishedAt && ( - - {new Date(item.publishedAt).toLocaleDateString()} - - )} -
- )} - - ))} + alt={item.author.name || 'Author'} + fill + className="object-cover" + /> + + )} + {item.author?.name && {item.author.name}} + {item.author?.name && item.publishedAt && ·} + {item.publishedAt && ( + + {new Date(item.publishedAt).toLocaleDateString()} + + )} +
+ )} + + ))} +
diff --git a/frontend/app/[locale]/blog/category/[category]/page.tsx b/frontend/app/[locale]/blog/category/[category]/page.tsx index 5de76c23..af501d25 100644 --- a/frontend/app/[locale]/blog/category/[category]/page.tsx +++ b/frontend/app/[locale]/blog/category/[category]/page.tsx @@ -1,7 +1,9 @@ import groq from 'groq'; import { notFound } from 'next/navigation'; import { getTranslations } from 'next-intl/server'; +import Image from 'next/image'; import { client } from '@/client'; +import { Link } from '@/i18n/routing'; import { BlogCategoryGrid } from '@/components/blog/BlogCategoryGrid'; export const revalidate = 0; @@ -51,13 +53,11 @@ export default async function BlogCategoryPage({ if (!matchedCategory) return notFound(); const categoryTitle = matchedCategory.title; - const displayTitle = - categoryTitle === 'Growth' ? 'Career' : categoryTitle; const posts: Post[] = await client.withConfig({ useCdn: false }).fetch( groq` *[_type == "post" && defined(slug.current) && $category in categories[]->title] - | order(publishedAt desc) { + | order(coalesce(publishedAt, _createdAt) desc) { _id, "title": coalesce(title[$locale], title[lower($locale)], title.uk, title.en, title.pl, title), slug, @@ -77,13 +77,73 @@ export default async function BlogCategoryPage({ { locale, category: categoryTitle } ); + const featuredPost = posts[0]; + const restPosts = posts.slice(1); + const featuredDate = featuredPost?.publishedAt + ? new Intl.DateTimeFormat(locale, { + day: '2-digit', + month: '2-digit', + year: 'numeric', + }).format(new Date(featuredPost.publishedAt)) + : ''; + return (

- {displayTitle} + {categoryTitle}

+ {featuredPost?.mainImage && ( +
+
+
+ {featuredPost.title} +
+
+
+ {featuredPost.categories?.[0] && ( +
+ {featuredPost.categories[0]} +
+ )} +

+ {featuredPost.title} +

+
+ {featuredPost.author?.image && ( + {featuredPost.author.name + )} + {featuredPost.author?.name && ( + {featuredPost.author.name} + )} + {featuredPost.author?.name && featuredDate && ·} + {featuredDate && {featuredDate}} +
+ + + +
+
+
+ )}
- +
{!posts.length && (

{t('noPosts')}

diff --git a/frontend/app/[locale]/blog/page.tsx b/frontend/app/[locale]/blog/page.tsx index bd751d04..7ebacae2 100644 --- a/frontend/app/[locale]/blog/page.tsx +++ b/frontend/app/[locale]/blog/page.tsx @@ -1,4 +1,5 @@ import groq from 'groq'; +import { unstable_noStore as noStore } from 'next/cache'; import { getTranslations } from 'next-intl/server'; import { client } from '@/client'; import BlogFilters from '@/components/blog/BlogFilters'; @@ -24,13 +25,14 @@ export default async function BlogPage({ }: { params: Promise<{ locale: string }>; }) { + noStore(); const { locale } = await params; const t = await getTranslations({ locale, namespace: 'blog' }); const posts = await client.withConfig({ useCdn: false }).fetch( groq` *[_type == "post" && defined(slug.current)] - | order(publishedAt desc) { + | order(coalesce(publishedAt, _createdAt) desc) { _id, "title": coalesce(title[$locale], title[lower($locale)], title.uk, title.en, title.pl, title), slug, diff --git a/frontend/client.ts b/frontend/client.ts index 7b8b0588..45ad6da9 100644 --- a/frontend/client.ts +++ b/frontend/client.ts @@ -3,6 +3,6 @@ import { createClient } from "@sanity/client"; export const client = createClient({ projectId: "6y9ive6v", dataset: "production", - useCdn: true, + useCdn: false, apiVersion: "2025-11-29", }); diff --git a/frontend/components/blog/BlogCard.tsx b/frontend/components/blog/BlogCard.tsx index 7599939d..8b59043a 100644 --- a/frontend/components/blog/BlogCard.tsx +++ b/frontend/components/blog/BlogCard.tsx @@ -14,9 +14,11 @@ import type { export default function BlogCard({ post, onAuthorSelect, + disableHoverColor = false, }: { post: Post; onAuthorSelect: (author: Author) => void; + disableHoverColor?: boolean; }) { const t = useTranslations('blog'); const locale = useLocale(); @@ -26,7 +28,7 @@ export default function BlogCard({ .map(b => (b.children ?? []).map((c: PortableTextSpan) => c.text ?? '').join(' ') ) - .join(' ') + .join('\n') .slice(0, 160) || ''; const formattedDate = useMemo(() => { if (!post.publishedAt) return ''; @@ -64,8 +66,7 @@ export default function BlogCard({ rounded-lg bg-gray-100 shadow-[0_8px_24px_rgba(0,0,0,0.08)] - dark:border dark:border-[rgba(56,189,248,0.25)] - dark:shadow-[0_0_0_1px_rgba(56,189,248,0.25),0_12px_28px_rgba(56,189,248,0.18)] + dark:border dark:border-[rgba(56,189,248,0.4)] dark:border-[0.5px] transition-transform duration-300 " > @@ -82,27 +83,26 @@ export default function BlogCard({
{post.title} {excerpt && ( -

+

{excerpt}

)} @@ -117,7 +117,7 @@ export default function BlogCard({ className="flex items-center gap-2 hover:text-[#ff00ff] hover:underline underline-offset-4 transition" > {post.author?.image && ( - + {post.author.name )} - {post.author?.name && formattedDate && ·} - {formattedDate && {formattedDate}} - {(formattedDate || post.author?.name) && categoryLabel && ( + {post.author?.name && categoryLabel && ·} + {categoryLabel && ( + + {categoryLabel} + + )} + {(post.author?.name || categoryLabel) && formattedDate && ( · )} - {categoryLabel && {categoryLabel}} + {formattedDate && {formattedDate}}
)} diff --git a/frontend/components/blog/BlogCategoryGrid.tsx b/frontend/components/blog/BlogCategoryGrid.tsx index 349ef1ed..5b7f2e14 100644 --- a/frontend/components/blog/BlogCategoryGrid.tsx +++ b/frontend/components/blog/BlogCategoryGrid.tsx @@ -6,5 +6,11 @@ import type { Post } from '@/components/blog/BlogFilters'; export function BlogCategoryGrid({ posts }: { posts: Post[] }) { if (!posts.length) return null; - return {}} />; + return ( + {}} + disableHoverColor + /> + ); } diff --git a/frontend/components/blog/BlogCategoryLinks.tsx b/frontend/components/blog/BlogCategoryLinks.tsx index a18f39de..e3207821 100644 --- a/frontend/components/blog/BlogCategoryLinks.tsx +++ b/frontend/components/blog/BlogCategoryLinks.tsx @@ -28,7 +28,12 @@ export function BlogCategoryLinks({ // Helper function to get translated category label const getCategoryLabel = (categoryName: string): string => { - const key = categoryName.toLowerCase() as 'tech' | 'career' | 'insights' | 'news' | 'growth'; + const key = categoryName.toLowerCase() as + | 'tech' + | 'career' + | 'insights' + | 'news' + | 'growth'; const categoryTranslations: Record = { tech: t('categories.tech'), career: t('categories.career'), @@ -54,7 +59,23 @@ export function BlogCategoryLinks({ .filter(category => category.slug); return ( -