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
12 changes: 9 additions & 3 deletions frontend/app/[locale]/blog/[slug]/PostDetails.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import { getTranslations } from 'next-intl/server';
import { client } from '@/client';
import { Link } from '@/i18n/routing';

export const revalidate = 0;

type SocialLink = {
_key?: string;
platform?: string;
Expand Down Expand Up @@ -116,11 +118,15 @@ export default async function PostDetails({
const slugParam = String(slug || '').trim();
if (!slugParam) return notFound();

const post: Post | null = await client.fetch(query, {
const post: Post | null = await client
.withConfig({ useCdn: false })
.fetch(query, {
slug: slugParam,
locale,
});
const recommendedAll: Post[] = await client.fetch(recommendedQuery, {
const recommendedAll: Post[] = await client
.withConfig({ useCdn: false })
.fetch(recommendedQuery, {
slug: slugParam,
locale,
});
Expand Down Expand Up @@ -156,7 +162,7 @@ export default async function PostDetails({
href={`/blog?category=${encodeURIComponent(post.categories[0])}`}
className="inline-flex items-center gap-1 hover:text-[#ff00ff] transition"
>
{post.categories[0]}
{post.categories[0] === 'Growth' ? 'Career' : post.categories[0]}
</Link>
</div>
)}
Expand Down
101 changes: 101 additions & 0 deletions frontend/app/[locale]/blog/category/[category]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import groq from 'groq';
import { notFound } from 'next/navigation';
import { getTranslations } from 'next-intl/server';
import { client } from '@/client';
import { BlogCategoryGrid } from '@/components/blog/BlogCategoryGrid';

export const revalidate = 0;

type Author = {
name?: string;
image?: string;
};

type Post = {
_id: string;
title: string;
slug: { current: string };
publishedAt?: string;
categories?: string[];
mainImage?: string;
body?: any[];
author?: Author;
};

type Category = {
_id: string;
title: string;
};

const categoriesQuery = groq`
*[_type == "category"] | order(orderRank asc) {
_id,
title
}
`;

export default async function BlogCategoryPage({
params,
}: {
params: Promise<{ locale: string; category: string }>;
}) {
const { locale, category } = await params;
const t = await getTranslations({ locale, namespace: 'blog' });
const categoryKey = String(category || '').toLowerCase();
const categories: Category[] = await client
.withConfig({ useCdn: false })
.fetch(categoriesQuery);
const matchedCategory = categories.find(
item => slugify(item.title) === categoryKey
);

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) {
_id,
"title": coalesce(title[$locale], title[lower($locale)], title.uk, title.en, title.pl, title),
slug,
publishedAt,
"categories": categories[]->title,
"body": coalesce(body[$locale], body[lower($locale)], body.uk, body.en, body.pl, body)[]{
...,
children[]{ text }
},
"mainImage": mainImage.asset->url,
"author": author->{
"name": coalesce(name[$locale], name[lower($locale)], name.uk, name.en, name.pl, name),
"image": image.asset->url
}
}
`,
{ locale, category: categoryTitle }
);

return (
<main className="max-w-6xl mx-auto px-6 py-12">
<h1 className="text-4xl font-bold mb-4 text-center">
{displayTitle}
</h1>
<div className="mt-12">
<BlogCategoryGrid posts={posts} />
</div>
{!posts.length && (
<p className="text-center text-gray-500 mt-10">{t('noPosts')}</p>
)}
</main>
);
}

function slugify(value: string) {
return value
.toLowerCase()
.trim()
.replace(/[^a-z0-9\s-]/g, '')
.replace(/\s+/g, '-');
}
6 changes: 4 additions & 2 deletions frontend/app/[locale]/blog/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { getTranslations } from 'next-intl/server';
import { client } from '@/client';
import BlogFilters from '@/components/blog/BlogFilters';

export const revalidate = 0;

export async function generateMetadata({
params,
}: {
Expand All @@ -25,7 +27,7 @@ export default async function BlogPage({
const { locale } = await params;
const t = await getTranslations({ locale, namespace: 'blog' });

const posts = await client.fetch(
const posts = await client.withConfig({ useCdn: false }).fetch(
groq`
*[_type == "post" && defined(slug.current)]
| order(publishedAt desc) {
Expand Down Expand Up @@ -62,7 +64,7 @@ export default async function BlogPage({
`,
{ locale }
);
const categories = await client.fetch(
const categories = await client.withConfig({ useCdn: false }).fetch(
groq`
*[_type == "category"] | order(orderRank asc) {
_id,
Expand Down
26 changes: 24 additions & 2 deletions frontend/app/[locale]/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@ import { Toaster } from 'sonner';
import { NextIntlClientProvider } from 'next-intl';
import { getMessages } from 'next-intl/server';
import { notFound } from 'next/navigation';
import groq from 'groq';

import { locales } from '@/i18n/config';
import Footer from '@/components/shared/Footer';
import { ThemeProvider } from '@/components/theme/ThemeProvider';
import { getCurrentUser } from '@/lib/auth';
import { client } from '@/client';

import { MainSwitcher } from '@/components/header/MainSwitcher';
import { AppChrome } from '@/components/header/AppChrome';
Expand All @@ -29,6 +31,16 @@ export default async function LocaleLayout({

const messages = await getMessages({ locale });
const user = await getCurrentUser();
const blogCategories: Array<{ _id: string; title: string }> = await client
.withConfig({ useCdn: false })
.fetch(
groq`
*[_type == "category"] | order(orderRank asc) {
_id,
title
}
`
);
Comment on lines +34 to +43
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Category fetch lacks error handling.

If the Sanity client fetch fails, this will throw and crash the layout. Consider wrapping in try-catch with a fallback to an empty array:

let blogCategories: Array<{ _id: string; title: string }> = [];
try {
  blogCategories = await client
    .withConfig({ useCdn: false })
    .fetch(groq`...`);
} catch (error) {
  console.error('Failed to fetch blog categories:', error);
}

This ensures the layout remains functional even if the CMS is temporarily unavailable.

🤖 Prompt for AI Agents
In `@frontend/app/`[locale]/layout.tsx around lines 34 - 43, The blogCategories
fetch can throw and crash the layout; initialize blogCategories as an empty
array and wrap the client.withConfig(...).fetch(groq`...`) call in a try-catch,
assigning the result to blogCategories on success and logging the error (e.g.,
console.error('Failed to fetch blog categories:', error)) on failure so the
layout continues to render with an empty array; ensure the variable keeps the
same type Array<{ _id: string; title: string }> and reference the existing
blogCategories identifier and the client.withConfig(...).fetch call when making
the change.


const userExists = Boolean(user);
const enableAdmin =
Expand All @@ -49,8 +61,18 @@ export default async function LocaleLayout({
enableSystem
disableTransitionOnChange
>
<AppChrome userExists={userExists} showAdminLink={showAdminNavLink}>
<MainSwitcher>{children}</MainSwitcher>
<AppChrome
userExists={userExists}
showAdminLink={showAdminNavLink}
blogCategories={blogCategories}
>
<MainSwitcher
userExists={userExists}
showAdminLink={showAdminNavLink}
blogCategories={blogCategories}
>
{children}
</MainSwitcher>
</AppChrome>

<Footer />
Expand Down
20 changes: 20 additions & 0 deletions frontend/app/api/blog-search/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import groq from 'groq';
import { NextResponse } from 'next/server';
import { client } from '@/client';

const searchQuery = groq`
*[_type == "post" && defined(slug.current)] | order(publishedAt desc) {
_id,
"title": coalesce(title.en, title.uk, title.pl, title),
"body": coalesce(body.en, body.uk, body.pl, body)[]{
...,
children[]{ text }
},
slug
}
`;

export async function GET() {
const items = await client.withConfig({ useCdn: false }).fetch(searchQuery);
return NextResponse.json(items || []);
}
2 changes: 2 additions & 0 deletions frontend/app/globals.css
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
@import url(https://fonts.googleapis.com/css?family=Lato:100,300,400,700);
@import url(https://raw.github.com/FortAwesome/Font-Awesome/master/docs/assets/css/font-awesome.min.css);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cd /repo && find . -name "globals.css" -o -name "globals.css" | head -5

Repository: DevLoversTeam/devlovers.net

Length of output: 126


🏁 Script executed:

cat -n frontend/app/globals.css | head -20

Repository: DevLoversTeam/devlovers.net

Length of output: 939


🌐 Web query:

Font Awesome official CDN URL latest stable version free

💡 Result:

Use jsDelivr (official npm package) for the latest free release — CSS minified (v7.1.0):
https://cdn.jsdelivr.net/npm/@fortawesome/fontawesome-free@7.1.0/css/all.min.css. [1][2]

Sources:
[1] FortAwesome GitHub releases (7.1.0).
[2] jsDelivr package page for @fortawesome/fontawesome-free (v7.1.0).


Don't load Font Awesome from raw GitHub master.

This URL is unversioned and points to the master branch, which can change or break unexpectedly—a supply-chain and stability risk. Use a pinned official CDN URL or install @fortawesome/fontawesome-free locally.

🔧 Example solutions

Official jsDelivr CDN (v7.1.0):

-@import url(https://raw.github.com/FortAwesome/Font-Awesome/master/docs/assets/css/font-awesome.min.css);
+@import url(https://cdn.jsdelivr.net/npm/@fortawesome/fontawesome-free@7.1.0/css/all.min.css);

Or install locally:

-@import url(https://raw.github.com/FortAwesome/Font-Awesome/master/docs/assets/css/font-awesome.min.css);
+@import '@fortawesome/fontawesome-free/css/all.min.css';
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@import url(https://raw.github.com/FortAwesome/Font-Awesome/master/docs/assets/css/font-awesome.min.css);
`@import` url(https://cdn.jsdelivr.net/npm/@fortawesome/fontawesome-free@7.1.0/css/all.min.css);
🤖 Prompt for AI Agents
In `@frontend/app/globals.css` at line 2, The `@import` line in globals.css pulls
Font Awesome directly from an unversioned raw GitHub master URL which is
unstable and a supply-chain risk; replace that import with a pinned, official
CDN URL (e.g., jsDelivr with a specific version) or remove the import and
install/use the local package `@fortawesome/fontawesome-free` and import its CSS
from node_modules instead, updating the existing `@import` statement (or
equivalent stylesheet reference) to the chosen pinned CDN path or local package
path so the dependency is versioned and reliable.

@import 'tailwindcss';

@custom-variant dark (&:is(.dark *));
Expand Down
58 changes: 33 additions & 25 deletions frontend/components/blog/BlogCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ export default function BlogCard({
year: 'numeric',
}).format(date);
}, [post.publishedAt, locale]);
const categoryLabel =
post.categories?.[0] === 'Growth' ? 'Career' : post.categories?.[0];

return (
<article
Expand Down Expand Up @@ -77,15 +79,15 @@ export default function BlogCard({
</Link>
)}

<div className="pt-8 flex flex-col flex-1">
<div className="pt-2 px-1 flex flex-col flex-1">
<Link
href={`/blog/${post.slug.current}`}
className="
block
text-[22px] md:text-[26px]
text-[18px] md:text-[22px]
font-semibold
tracking-tight
leading-[1.2]
leading-[1.15]
text-gray-950 dark:text-gray-100
transition
hover:text-[#ff00ff]
Expand All @@ -100,33 +102,39 @@ export default function BlogCard({
</Link>

{excerpt && (
<p className="mt-4 text-[16px] md:text-[17px] leading-[1.65] text-gray-700 dark:text-gray-300 max-w-[60ch] line-clamp-3">
<p className="mt-2 text-[15px] md:text-[16px] leading-[1.55] text-gray-700 dark:text-gray-300 max-w-[60ch] line-clamp-3">
{excerpt}
</p>
)}

<div className="mt-auto pt-6">
{post.author?.name && (
<div className="mb-3 flex items-center gap-2 text-[13px] md:text-[14px] text-gray-500 dark:text-gray-400">
<button
type="button"
onClick={() => post.author && onAuthorSelect(post.author)}
className="flex items-center gap-2 hover:text-[#ff00ff] hover:underline underline-offset-4 transition"
>
{post.author?.image && (
<span className="relative h-6 w-6 overflow-hidden rounded-full">
<Image
src={post.author.image}
alt={post.author.name || 'Author'}
fill
className="object-cover"
/>
</span>
)}
{post.author.name}
</button>
{formattedDate && <span>·</span>}
<div className="mt-auto pt-3">
{(post.author?.name || formattedDate || categoryLabel) && (
<div className="mb-2 flex flex-wrap items-center gap-2 text-[12px] md:text-[13px] text-gray-500 dark:text-gray-400">
{post.author?.name && (
<button
type="button"
onClick={() => post.author && onAuthorSelect(post.author)}
className="flex items-center gap-2 hover:text-[#ff00ff] hover:underline underline-offset-4 transition"
>
{post.author?.image && (
<span className="relative h-6 w-6 overflow-hidden rounded-full">
<Image
src={post.author.image}
alt={post.author.name || 'Author'}
fill
className="object-cover"
/>
</span>
)}
{post.author.name}
</button>
)}
{post.author?.name && formattedDate && <span>·</span>}
{formattedDate && <span>{formattedDate}</span>}
{(formattedDate || post.author?.name) && categoryLabel && (
<span>·</span>
)}
{categoryLabel && <span>{categoryLabel}</span>}
</div>
)}

Expand Down
10 changes: 10 additions & 0 deletions frontend/components/blog/BlogCategoryGrid.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
'use client';

import BlogGrid from '@/components/blog/BlogGrid';
import type { Post } from '@/components/blog/BlogFilters';

export function BlogCategoryGrid({ posts }: { posts: Post[] }) {
if (!posts.length) return null;

return <BlogGrid posts={posts} onAuthorSelect={() => {}} />;
Comment on lines +6 to +9
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Inconsistent empty-state handling compared to BlogGrid.

BlogGrid displays a "no posts" message when posts is empty (see BlogGrid.tsx lines 14-16), but this component returns null. This creates inconsistent UX between the main blog page and category pages.

Consider delegating to BlogGrid to handle the empty state, or ensure the parent component handles the empty case with appropriate messaging.

💡 Suggested fix
 export function BlogCategoryGrid({ posts }: { posts: Post[] }) {
-  if (!posts.length) return null;
-
   return <BlogGrid posts={posts} onAuthorSelect={() => {}} />;
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export function BlogCategoryGrid({ posts }: { posts: Post[] }) {
if (!posts.length) return null;
return <BlogGrid posts={posts} onAuthorSelect={() => {}} />;
export function BlogCategoryGrid({ posts }: { posts: Post[] }) {
return <BlogGrid posts={posts} onAuthorSelect={() => {}} />;
}
🤖 Prompt for AI Agents
In `@frontend/components/blog/BlogCategoryGrid.tsx` around lines 6 - 9,
BlogCategoryGrid currently returns null when posts is empty, causing
inconsistent UX with BlogGrid which shows a "no posts" message; remove the early
return in BlogCategoryGrid (the block checking posts.length) and always render
<BlogGrid posts={posts} onAuthorSelect={...} /> so BlogGrid can handle the
empty-state messaging, ensuring onAuthorSelect remains provided (keep the
existing no-op or forward a real handler if available).

}
Loading