Skip to content

Commit 1a8f5d4

Browse files
Merge pull request #155 from DevLoversTeam/sanity
Sanity
2 parents 387cc55 + 511c99f commit 1a8f5d4

21 files changed

Lines changed: 4990 additions & 3857 deletions

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

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import { getTranslations } from 'next-intl/server';
55
import { client } from '@/client';
66
import { Link } from '@/i18n/routing';
77

8+
export const revalidate = 0;
9+
810
type SocialLink = {
911
_key?: string;
1012
platform?: string;
@@ -116,11 +118,15 @@ export default async function PostDetails({
116118
const slugParam = String(slug || '').trim();
117119
if (!slugParam) return notFound();
118120

119-
const post: Post | null = await client.fetch(query, {
121+
const post: Post | null = await client
122+
.withConfig({ useCdn: false })
123+
.fetch(query, {
120124
slug: slugParam,
121125
locale,
122126
});
123-
const recommendedAll: Post[] = await client.fetch(recommendedQuery, {
127+
const recommendedAll: Post[] = await client
128+
.withConfig({ useCdn: false })
129+
.fetch(recommendedQuery, {
124130
slug: slugParam,
125131
locale,
126132
});
@@ -156,7 +162,7 @@ export default async function PostDetails({
156162
href={`/blog?category=${encodeURIComponent(post.categories[0])}`}
157163
className="inline-flex items-center gap-1 hover:text-[#ff00ff] transition"
158164
>
159-
{post.categories[0]}
165+
{post.categories[0] === 'Growth' ? 'Career' : post.categories[0]}
160166
</Link>
161167
</div>
162168
)}
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
import groq from 'groq';
2+
import { notFound } from 'next/navigation';
3+
import { getTranslations } from 'next-intl/server';
4+
import { client } from '@/client';
5+
import { BlogCategoryGrid } from '@/components/blog/BlogCategoryGrid';
6+
7+
export const revalidate = 0;
8+
9+
type Author = {
10+
name?: string;
11+
image?: string;
12+
};
13+
14+
type Post = {
15+
_id: string;
16+
title: string;
17+
slug: { current: string };
18+
publishedAt?: string;
19+
categories?: string[];
20+
mainImage?: string;
21+
body?: any[];
22+
author?: Author;
23+
};
24+
25+
type Category = {
26+
_id: string;
27+
title: string;
28+
};
29+
30+
const categoriesQuery = groq`
31+
*[_type == "category"] | order(orderRank asc) {
32+
_id,
33+
title
34+
}
35+
`;
36+
37+
export default async function BlogCategoryPage({
38+
params,
39+
}: {
40+
params: Promise<{ locale: string; category: string }>;
41+
}) {
42+
const { locale, category } = await params;
43+
const t = await getTranslations({ locale, namespace: 'blog' });
44+
const categoryKey = String(category || '').toLowerCase();
45+
const categories: Category[] = await client
46+
.withConfig({ useCdn: false })
47+
.fetch(categoriesQuery);
48+
const matchedCategory = categories.find(
49+
item => slugify(item.title) === categoryKey
50+
);
51+
52+
if (!matchedCategory) return notFound();
53+
const categoryTitle = matchedCategory.title;
54+
const displayTitle =
55+
categoryTitle === 'Growth' ? 'Career' : categoryTitle;
56+
57+
const posts: Post[] = await client.withConfig({ useCdn: false }).fetch(
58+
groq`
59+
*[_type == "post" && defined(slug.current) && $category in categories[]->title]
60+
| order(publishedAt desc) {
61+
_id,
62+
"title": coalesce(title[$locale], title[lower($locale)], title.uk, title.en, title.pl, title),
63+
slug,
64+
publishedAt,
65+
"categories": categories[]->title,
66+
"body": coalesce(body[$locale], body[lower($locale)], body.uk, body.en, body.pl, body)[]{
67+
...,
68+
children[]{ text }
69+
},
70+
"mainImage": mainImage.asset->url,
71+
"author": author->{
72+
"name": coalesce(name[$locale], name[lower($locale)], name.uk, name.en, name.pl, name),
73+
"image": image.asset->url
74+
}
75+
}
76+
`,
77+
{ locale, category: categoryTitle }
78+
);
79+
80+
return (
81+
<main className="max-w-6xl mx-auto px-6 py-12">
82+
<h1 className="text-4xl font-bold mb-4 text-center">
83+
{displayTitle}
84+
</h1>
85+
<div className="mt-12">
86+
<BlogCategoryGrid posts={posts} />
87+
</div>
88+
{!posts.length && (
89+
<p className="text-center text-gray-500 mt-10">{t('noPosts')}</p>
90+
)}
91+
</main>
92+
);
93+
}
94+
95+
function slugify(value: string) {
96+
return value
97+
.toLowerCase()
98+
.trim()
99+
.replace(/[^a-z0-9\s-]/g, '')
100+
.replace(/\s+/g, '-');
101+
}

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

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ import { getTranslations } from 'next-intl/server';
33
import { client } from '@/client';
44
import BlogFilters from '@/components/blog/BlogFilters';
55

6+
export const revalidate = 0;
7+
68
export async function generateMetadata({
79
params,
810
}: {
@@ -25,7 +27,7 @@ export default async function BlogPage({
2527
const { locale } = await params;
2628
const t = await getTranslations({ locale, namespace: 'blog' });
2729

28-
const posts = await client.fetch(
30+
const posts = await client.withConfig({ useCdn: false }).fetch(
2931
groq`
3032
*[_type == "post" && defined(slug.current)]
3133
| order(publishedAt desc) {
@@ -62,7 +64,7 @@ export default async function BlogPage({
6264
`,
6365
{ locale }
6466
);
65-
const categories = await client.fetch(
67+
const categories = await client.withConfig({ useCdn: false }).fetch(
6668
groq`
6769
*[_type == "category"] | order(orderRank asc) {
6870
_id,

frontend/app/[locale]/layout.tsx

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,13 @@ import { Toaster } from 'sonner';
33
import { NextIntlClientProvider } from 'next-intl';
44
import { getMessages } from 'next-intl/server';
55
import { notFound } from 'next/navigation';
6+
import groq from 'groq';
67

78
import { locales } from '@/i18n/config';
89
import Footer from '@/components/shared/Footer';
910
import { ThemeProvider } from '@/components/theme/ThemeProvider';
1011
import { getCurrentUser } from '@/lib/auth';
12+
import { client } from '@/client';
1113

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

3032
const messages = await getMessages({ locale });
3133
const user = await getCurrentUser();
34+
const blogCategories: Array<{ _id: string; title: string }> = await client
35+
.withConfig({ useCdn: false })
36+
.fetch(
37+
groq`
38+
*[_type == "category"] | order(orderRank asc) {
39+
_id,
40+
title
41+
}
42+
`
43+
);
3244

3345
const userExists = Boolean(user);
3446
const enableAdmin =
@@ -49,8 +61,18 @@ export default async function LocaleLayout({
4961
enableSystem
5062
disableTransitionOnChange
5163
>
52-
<AppChrome userExists={userExists} showAdminLink={showAdminNavLink}>
53-
<MainSwitcher>{children}</MainSwitcher>
64+
<AppChrome
65+
userExists={userExists}
66+
showAdminLink={showAdminNavLink}
67+
blogCategories={blogCategories}
68+
>
69+
<MainSwitcher
70+
userExists={userExists}
71+
showAdminLink={showAdminNavLink}
72+
blogCategories={blogCategories}
73+
>
74+
{children}
75+
</MainSwitcher>
5476
</AppChrome>
5577

5678
<Footer />
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import groq from 'groq';
2+
import { NextResponse } from 'next/server';
3+
import { client } from '@/client';
4+
5+
const searchQuery = groq`
6+
*[_type == "post" && defined(slug.current)] | order(publishedAt desc) {
7+
_id,
8+
"title": coalesce(title.en, title.uk, title.pl, title),
9+
"body": coalesce(body.en, body.uk, body.pl, body)[]{
10+
...,
11+
children[]{ text }
12+
},
13+
slug
14+
}
15+
`;
16+
17+
export async function GET() {
18+
const items = await client.withConfig({ useCdn: false }).fetch(searchQuery);
19+
return NextResponse.json(items || []);
20+
}

frontend/app/globals.css

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
@import url(https://fonts.googleapis.com/css?family=Lato:100,300,400,700);
2+
@import url(https://raw.github.com/FortAwesome/Font-Awesome/master/docs/assets/css/font-awesome.min.css);
13
@import 'tailwindcss';
24

35
@custom-variant dark (&:is(.dark *));

frontend/components/blog/BlogCard.tsx

Lines changed: 33 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ export default function BlogCard({
3838
year: 'numeric',
3939
}).format(date);
4040
}, [post.publishedAt, locale]);
41+
const categoryLabel =
42+
post.categories?.[0] === 'Growth' ? 'Career' : post.categories?.[0];
4143

4244
return (
4345
<article
@@ -77,15 +79,15 @@ export default function BlogCard({
7779
</Link>
7880
)}
7981

80-
<div className="pt-8 flex flex-col flex-1">
82+
<div className="pt-2 px-1 flex flex-col flex-1">
8183
<Link
8284
href={`/blog/${post.slug.current}`}
8385
className="
8486
block
85-
text-[22px] md:text-[26px]
87+
text-[18px] md:text-[22px]
8688
font-semibold
8789
tracking-tight
88-
leading-[1.2]
90+
leading-[1.15]
8991
text-gray-950 dark:text-gray-100
9092
transition
9193
hover:text-[#ff00ff]
@@ -100,33 +102,39 @@ export default function BlogCard({
100102
</Link>
101103

102104
{excerpt && (
103-
<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">
105+
<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">
104106
{excerpt}
105107
</p>
106108
)}
107109

108-
<div className="mt-auto pt-6">
109-
{post.author?.name && (
110-
<div className="mb-3 flex items-center gap-2 text-[13px] md:text-[14px] text-gray-500 dark:text-gray-400">
111-
<button
112-
type="button"
113-
onClick={() => post.author && onAuthorSelect(post.author)}
114-
className="flex items-center gap-2 hover:text-[#ff00ff] hover:underline underline-offset-4 transition"
115-
>
116-
{post.author?.image && (
117-
<span className="relative h-6 w-6 overflow-hidden rounded-full">
118-
<Image
119-
src={post.author.image}
120-
alt={post.author.name || 'Author'}
121-
fill
122-
className="object-cover"
123-
/>
124-
</span>
125-
)}
126-
{post.author.name}
127-
</button>
128-
{formattedDate && <span>·</span>}
110+
<div className="mt-auto pt-3">
111+
{(post.author?.name || formattedDate || categoryLabel) && (
112+
<div className="mb-2 flex flex-wrap items-center gap-2 text-[12px] md:text-[13px] text-gray-500 dark:text-gray-400">
113+
{post.author?.name && (
114+
<button
115+
type="button"
116+
onClick={() => post.author && onAuthorSelect(post.author)}
117+
className="flex items-center gap-2 hover:text-[#ff00ff] hover:underline underline-offset-4 transition"
118+
>
119+
{post.author?.image && (
120+
<span className="relative h-6 w-6 overflow-hidden rounded-full">
121+
<Image
122+
src={post.author.image}
123+
alt={post.author.name || 'Author'}
124+
fill
125+
className="object-cover"
126+
/>
127+
</span>
128+
)}
129+
{post.author.name}
130+
</button>
131+
)}
132+
{post.author?.name && formattedDate && <span>·</span>}
129133
{formattedDate && <span>{formattedDate}</span>}
134+
{(formattedDate || post.author?.name) && categoryLabel && (
135+
<span>·</span>
136+
)}
137+
{categoryLabel && <span>{categoryLabel}</span>}
130138
</div>
131139
)}
132140

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
'use client';
2+
3+
import BlogGrid from '@/components/blog/BlogGrid';
4+
import type { Post } from '@/components/blog/BlogFilters';
5+
6+
export function BlogCategoryGrid({ posts }: { posts: Post[] }) {
7+
if (!posts.length) return null;
8+
9+
return <BlogGrid posts={posts} onAuthorSelect={() => {}} />;
10+
}

0 commit comments

Comments
 (0)