Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 39 additions & 10 deletions frontend/app/[locale]/blog/[slug]/PostDetails.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import groq from 'groq';
import { getTranslations } from 'next-intl/server';
import { client } from '@/client';
import { Link } from '@/i18n/routing';
import { formatBlogDate } from '@/lib/blog/date';

export const revalidate = 0;

Expand Down Expand Up @@ -67,6 +68,41 @@ function linkifyText(text: string) {
});
}

function renderPortableTextSpans(
children: Array<{ _type?: string; text?: string; marks?: string[] }> = [],
markDefs: Array<{ _key?: string; _type?: string; href?: string }> = []
) {
const linkMap = new Map(
markDefs
.filter(def => def?._type === 'link' && def?._key && def?.href)
.map(def => [def._key as string, def.href as string])
);

return children.map((child, index) => {
const text = child?.text || '';
if (!text) return null;
const marks = child?.marks || [];
const linkKey = marks.find(mark => linkMap.has(mark));

if (linkKey) {
const href = linkMap.get(linkKey)!;
return (
<a
key={`mark-link-${index}`}
href={href}
target="_blank"
rel="noopener noreferrer"
className="text-[var(--accent-primary)] underline underline-offset-4"
>
{text}
</a>
);
}

return <span key={`mark-text-${index}`}>{linkifyText(text)}</span>;
});
}

function seededShuffle<T>(items: T[], seed: number) {
const result = [...items];
let value = seed;
Expand Down Expand Up @@ -219,9 +255,7 @@ export default async function PostDetails({
</Link>
)}
{authorName && post.publishedAt && <span>·</span>}
{post.publishedAt && (
<span>{new Date(post.publishedAt).toLocaleDateString()}</span>
)}
{post.publishedAt && <span>{formatBlogDate(post.publishedAt)}</span>}
</div>
)}

Expand All @@ -241,15 +275,12 @@ export default async function PostDetails({
<article className="prose prose-gray max-w-none">
{post.body?.map((block: any, index: number) => {
if (block?._type === 'block') {
const text = (block.children || [])
.map((c: any) => c.text || '')
.join('');
return (
<p
key={block._key || `block-${index}`}
className="whitespace-pre-line"
>
{linkifyText(text)}
{renderPortableTextSpans(block.children, block.markDefs)}
</p>
);
}
Expand Down Expand Up @@ -320,9 +351,7 @@ export default async function PostDetails({
{item.author?.name && <span>{item.author.name}</span>}
{item.author?.name && item.publishedAt && <span>·</span>}
{item.publishedAt && (
<span>
{new Date(item.publishedAt).toLocaleDateString()}
</span>
<span>{formatBlogDate(item.publishedAt)}</span>
)}
</div>
)}
Expand Down
9 changes: 2 additions & 7 deletions frontend/app/[locale]/blog/category/[category]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import Image from 'next/image';
import { client } from '@/client';
import { Link } from '@/i18n/routing';
import { BlogCategoryGrid } from '@/components/blog/BlogCategoryGrid';
import { formatBlogDate } from '@/lib/blog/date';

export const revalidate = 0;

Expand Down Expand Up @@ -79,13 +80,7 @@ export default async function BlogCategoryPage({

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))
: '';
const featuredDate = formatBlogDate(featuredPost?.publishedAt);

return (
<main className="max-w-6xl mx-auto px-6 py-12">
Expand Down
41 changes: 41 additions & 0 deletions frontend/app/api/blog-author/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import groq from 'groq';
import { NextResponse } from 'next/server';
import { client } from '@/client';

export const revalidate = 0;

const authorQuery = groq`
*[_type == "author" && (
name[$locale] == $name ||
name[lower($locale)] == $name ||
name.en == $name ||
name.pl == $name ||
name.uk == $name
)][0]{
"name": coalesce(name[$locale], name[lower($locale)], name.uk, name.en, name.pl, name),
"company": coalesce(company[$locale], company[lower($locale)], company.uk, company.en, company.pl, company),
"jobTitle": coalesce(jobTitle[$locale], jobTitle[lower($locale)], jobTitle.uk, jobTitle.en, jobTitle.pl, jobTitle),
"city": coalesce(city[$locale], city[lower($locale)], city.uk, city.en, city.pl, city),
"bio": coalesce(bio[$locale], bio[lower($locale)], bio.uk, bio.en, bio.pl, bio),
"image": image.asset->url,
socialMedia[]{ _key, platform, url }
}
`;

export async function GET(request: Request) {
const { searchParams } = new URL(request.url);
const name = (searchParams.get('name') || '').trim();
const locale = (searchParams.get('locale') || 'en').trim();

if (!name) {
return NextResponse.json(null, { status: 400 });
}

const author = await client
.withConfig({ useCdn: false })
.fetch(authorQuery, { name, locale });

return NextResponse.json(author || null, {
headers: { 'Cache-Control': 'no-store' },
});
}
Loading