@@ -3,6 +3,7 @@ import { notFound } from 'next/navigation';
33import groq from 'groq' ;
44import { getTranslations } from 'next-intl/server' ;
55import { client } from '@/client' ;
6+ import { Link } from '@/i18n/routing' ;
67
78type SocialLink = {
89 _key ?: string ;
@@ -21,6 +22,7 @@ type Author = {
2122} ;
2223
2324type Post = {
25+ _id ?: string ;
2426 title ?: string ;
2527 publishedAt ?: string ;
2628 mainImage ?: string ;
@@ -29,6 +31,7 @@ type Post = {
2931 resourceLink ?: string ;
3032 author ?: Author ;
3133 body ?: any [ ] ;
34+ slug ?: { current ?: string } ;
3235} ;
3336
3437function plainTextFromPortableText ( value : any ) : string {
@@ -42,24 +45,25 @@ function plainTextFromPortableText(value: any): string {
4245
4346const query = groq `
4447 *[_type=="post" && slug.current==$slug][0]{
45- "title": coalesce(title[$locale], title.en, title),
48+ _id,
49+ "title": coalesce(title[$locale], title[lower($locale)], title.uk, title.en, title.pl, title),
4650 publishedAt,
4751 "mainImage": mainImage.asset->url,
4852 "categories": categories[]->title,
4953 tags,
5054 resourceLink,
5155
5256 "author": author->{
53- "name": coalesce(name[$locale], name.en , name),
54- "company": coalesce(company[$locale], company.en , company),
55- "jobTitle": coalesce(jobTitle[$locale], jobTitle.en , jobTitle),
56- "city": coalesce(city[$locale], city.en , city),
57- "bio": coalesce(bio[$locale], bio.en , bio),
57+ "name": coalesce(name[$locale], name[lower($locale)], name.uk, name.en, name.pl , name),
58+ "company": coalesce(company[$locale], company[lower($locale)], company.uk, company.en, company.pl , company),
59+ "jobTitle": coalesce(jobTitle[$locale], jobTitle[lower($locale)], jobTitle.uk, jobTitle.en, jobTitle.pl , jobTitle),
60+ "city": coalesce(city[$locale], city[lower($locale)], city.uk, city.en, city.pl , city),
61+ "bio": coalesce(bio[$locale], bio[lower($locale)], bio.uk, bio.en, bio.pl , bio),
5862 "image": image.asset->url,
5963 socialMedia[]{ _key, platform, url }
6064 },
6165
62- "body": coalesce(body[$locale], body.en , body)[]{
66+ "body": coalesce(body[$locale], body[lower($locale)], body.uk, body.en, body.pl , body)[]{
6367 ...,
6468 _type == "image" => {
6569 ...,
@@ -68,6 +72,19 @@ const query = groq`
6872 }
6973 }
7074` ;
75+ const recommendedQuery = groq `
76+ *[_type=="post" && defined(slug.current) && slug.current != $slug]{
77+ _id,
78+ "title": coalesce(title[$locale], title[lower($locale)], title.uk, title.en, title.pl, title),
79+ publishedAt,
80+ "mainImage": mainImage.asset->url,
81+ slug,
82+ "author": author->{
83+ "name": coalesce(name[$locale], name[lower($locale)], name.uk, name.en, name.pl, name),
84+ "image": image.asset->url
85+ }
86+ }
87+ ` ;
7188
7289export default async function PostDetails ( {
7390 slug,
@@ -84,6 +101,13 @@ export default async function PostDetails({
84101 slug : slugParam ,
85102 locale,
86103 } ) ;
104+ const recommendedAll : Post [ ] = await client . fetch ( recommendedQuery , {
105+ slug : slugParam ,
106+ locale,
107+ } ) ;
108+ const recommendedPosts = recommendedAll
109+ . sort ( ( ) => Math . random ( ) - 0.5 )
110+ . slice ( 0 , 3 ) ;
87111
88112 if ( ! post ?. title ) return notFound ( ) ;
89113
@@ -98,38 +122,40 @@ export default async function PostDetails({
98122
99123 return (
100124 < main className = "max-w-3xl mx-auto px-6 py-12" >
101- < h1 className = "text-4xl font-bold text-gray-900" > { post . title } </ h1 >
102-
103- < div className = "mt-4 text-sm text-gray-500" >
104- { post . publishedAt && new Date ( post . publishedAt ) . toLocaleDateString ( ) }
105- </ div >
106-
107- { ( post . categories ?. length || 0 ) > 0 && (
108- < div className = "mt-4 flex flex-wrap gap-2" >
109- { post . categories ! . map ( ( cat , i ) => (
110- < span
111- key = { ` ${ cat } - ${ i } ` }
112- className = "text-xs bg-blue-50 text-blue-700 px-2 py-1 rounded-md"
113- >
114- { cat }
115- </ span >
116- ) ) }
125+ < Link
126+ href = "/blog"
127+ 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"
128+ >
129+ < span > ← </ span >
130+ < span > { t ( 'goBack' ) } </ span >
131+ </ Link >
132+
133+ { post . categories ?. [ 0 ] && (
134+ < div className = "text-sm font-medium text-gray-500 dark:text-gray-400 text-center" >
135+ < Link
136+ href = { `/blog?category= ${ encodeURIComponent ( post . categories [ 0 ] ) } ` }
137+ className = "inline-flex items-center gap-1 hover:text-[#ff00ff] transition"
138+ >
139+ { post . categories [ 0 ] }
140+ </ Link >
117141 </ div >
118142 ) }
143+ < h1 className = "mt-3 text-4xl font-bold text-gray-900 dark:text-gray-100 text-center" >
144+ { post . title }
145+ </ h1 >
119146
120- { ( post . tags ?. length || 0 ) > 0 && (
121- < div className = "mt-3 flex flex-wrap gap-2" >
122- { post . tags ! . map ( ( tag , i ) => (
123- < span
124- key = { `${ tag } -${ i } ` }
125- className = "text-xs bg-purple-50 text-purple-700 px-2 py-1 rounded-md"
126- >
127- #{ tag }
128- </ span >
129- ) ) }
147+ { ( authorName || post . publishedAt ) && (
148+ < div className = "mt-4 flex justify-center gap-2 text-sm text-gray-500 dark:text-gray-400" >
149+ { authorName && < span > { authorName } </ span > }
150+ { authorName && post . publishedAt && < span > ·</ span > }
151+ { post . publishedAt && (
152+ < span > { new Date ( post . publishedAt ) . toLocaleDateString ( ) } </ span >
153+ ) }
130154 </ div >
131155 ) }
132156
157+ { ( post . tags ?. length || 0 ) > 0 && null }
158+
133159 { post . mainImage && (
134160 < div className = "relative w-full h-[420px] rounded-2xl overflow-hidden border border-gray-200 my-8" >
135161 < Image
@@ -165,52 +191,67 @@ export default async function PostDetails({
165191 } ) }
166192 </ article >
167193
168- { post . resourceLink && (
169- < div className = "mt-10" >
170- < a
171- href = { post . resourceLink }
172- target = "_blank"
173- rel = "noopener noreferrer"
174- className = "inline-flex bg-green-600 text-white px-5 py-3 rounded-lg hover:bg-green-700 transition"
175- >
176- Visit Resource →
177- </ a >
178- </ div >
179- ) }
194+ { recommendedPosts . length > 0 && (
195+ < >
196+ < div className = "mt-16 flex justify-center" >
197+ < div className = "h-10 w-px bg-gray-200 dark:bg-gray-800" />
198+ </ div >
180199
181- { ( authorBio || authorName || authorMeta ) && (
182- < section className = "mt-12 p-6 rounded-2xl border border-gray-200 bg-white" >
183- < h2 className = "text-lg font-semibold" > { t ( 'aboutAuthor' ) } </ h2 >
184- < div className = "mt-4 flex items-start gap-4" >
185- { post . author ?. image && (
186- < div className = "relative w-14 h-14 shrink-0" >
187- < Image
188- src = { post . author . image }
189- alt = { authorName || 'Author' }
190- fill
191- className = "rounded-full object-cover border border-gray-200"
192- />
193- </ div >
194- ) }
195-
196- < div className = "min-w-0" >
197- { authorName && (
198- < p className = "text-sm font-semibold text-gray-900" >
199- { authorName }
200- </ p >
201- ) }
202- { authorMeta && (
203- < p className = "mt-1 text-sm text-gray-600" > { authorMeta } </ p >
204- ) }
205- { authorBio && (
206- < p className = "mt-3 text-sm text-gray-700 whitespace-pre-line leading-relaxed" >
207- { authorBio }
208- </ p >
209- ) }
200+ < section className = "mt-10" >
201+ < h2 className = "text-2xl font-semibold text-gray-900 dark:text-gray-100" >
202+ { t ( 'recommendedPosts' ) }
203+ </ h2 >
204+ < div className = "mt-6 grid gap-6 sm:grid-cols-2 lg:grid-cols-3" >
205+ { recommendedPosts . map ( item => (
206+ < Link
207+ key = { item . _id }
208+ href = { `/blog/${ item . slug ?. current } ` }
209+ className = "group block"
210+ >
211+ { item . mainImage && (
212+ < div className = "relative h-44 w-full overflow-hidden rounded-2xl" >
213+ < Image
214+ src = { item . mainImage }
215+ alt = { item . title || 'Post image' }
216+ fill
217+ className = "object-cover transition-transform duration-300 group-hover:scale-[1.03]"
218+ />
219+ </ div >
220+ ) }
221+ < h3 className = "mt-4 text-lg font-semibold text-gray-900 transition group-hover:text-[#ff00ff] dark:text-gray-100" >
222+ { item . title }
223+ </ h3 >
224+ { ( item . author ?. name || item . publishedAt ) && (
225+ < div className = "mt-2 flex items-center gap-2 text-sm text-gray-500 dark:text-gray-400" >
226+ { item . author ?. image && (
227+ < span className = "relative h-5 w-5 overflow-hidden rounded-full" >
228+ < Image
229+ src = { item . author . image }
230+ alt = { item . author . name || 'Author' }
231+ fill
232+ className = "object-cover"
233+ />
234+ </ span >
235+ ) }
236+ { item . author ?. name && < span > { item . author . name } </ span > }
237+ { item . author ?. name && item . publishedAt && < span > ·</ span > }
238+ { item . publishedAt && (
239+ < span >
240+ { new Date ( item . publishedAt ) . toLocaleDateString ( ) }
241+ </ span >
242+ ) }
243+ </ div >
244+ ) }
245+ </ Link >
246+ ) ) }
210247 </ div >
211- </ div >
212- </ section >
248+ </ section >
249+ </ >
213250 ) }
251+
252+ { post . resourceLink && null }
253+
254+ { ( authorBio || authorName || authorMeta ) && null }
214255 </ main >
215256 ) ;
216257}
0 commit comments