@@ -45,6 +45,28 @@ function plainTextFromPortableText(value: any): string {
4545 . trim ( ) ;
4646}
4747
48+ function linkifyText ( text : string ) {
49+ const urlRegex = / ( h t t p s ? : \/ \/ [ ^ \s ] + ) / g;
50+ const parts = text . split ( urlRegex ) ;
51+ return parts . map ( ( part , index ) => {
52+ if ( ! part ) return null ;
53+ if ( urlRegex . test ( part ) ) {
54+ return (
55+ < a
56+ key = { `link-${ index } ` }
57+ href = { part }
58+ target = "_blank"
59+ rel = "noopener noreferrer"
60+ className = "text-[var(--accent-primary)] underline underline-offset-4"
61+ >
62+ { part }
63+ </ a >
64+ ) ;
65+ }
66+ return < span key = { `text-${ index } ` } > { part } </ span > ;
67+ } ) ;
68+ }
69+
4870function seededShuffle < T > ( items : T [ ] , seed : number ) {
4971 const result = [ ...items ] ;
5072 let value = seed ;
@@ -103,6 +125,13 @@ const recommendedQuery = groq`
103125 "author": author->{
104126 "name": coalesce(name[$locale], name[lower($locale)], name.uk, name.en, name.pl, name),
105127 "image": image.asset->url
128+ },
129+ "body": coalesce(body[$locale], body[lower($locale)], body.uk, body.en, body.pl, body)[]{
130+ ...,
131+ _type == "image" => {
132+ ...,
133+ "url": asset->url
134+ }
106135 }
107136 }
108137` ;
@@ -115,6 +144,7 @@ export default async function PostDetails({
115144 locale : string ;
116145} ) {
117146 const t = await getTranslations ( { locale, namespace : 'blog' } ) ;
147+ const tNav = await getTranslations ( { locale, namespace : 'navigation' } ) ;
118148 const slugParam = String ( slug || '' ) . trim ( ) ;
119149 if ( ! slugParam ) return notFound ( ) ;
120150
@@ -145,24 +175,32 @@ export default async function PostDetails({
145175 post . author ?. city ,
146176 ] . filter ( Boolean ) as string [ ] ;
147177 const authorMeta = authorMetaParts . join ( ' · ' ) ;
178+ const categoryLabel = post . categories ?. [ 0 ] ;
148179
149180 return (
150181 < main className = "max-w-3xl mx-auto px-6 py-12" >
151- < Link
152- href = "/blog"
153- 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"
154- >
155- < span > ←</ span >
156- < span > { t ( 'goBack' ) } </ span >
157- </ Link >
182+ < div className = "mb-6 relative left-1/2 right-1/2 w-screen -translate-x-1/2 px-6" >
183+ < div className = "mx-auto flex max-w-6xl justify-start" >
184+ < div className = "flex items-center gap-2 text-sm text-gray-500 dark:text-gray-400" >
185+ < Link
186+ href = "/blog"
187+ className = "transition hover:text-[var(--accent-primary)] hover:underline underline-offset-4"
188+ >
189+ { tNav ( 'blog' ) }
190+ </ Link >
191+ < span > ></ span >
192+ < span className = "text-[var(--accent-primary)]" > { post . title } </ span >
193+ </ div >
194+ </ div >
195+ </ div >
158196
159- { post . categories ?. [ 0 ] && (
197+ { categoryLabel && (
160198 < div className = "text-sm font-medium text-gray-500 dark:text-gray-400 text-center" >
161199 < Link
162- href = { `/blog?category=${ encodeURIComponent ( post . categories [ 0 ] ) } ` }
163- className = "inline-flex items-center gap-1 hover: text-[#ff00ff ] transition"
200+ href = { `/blog?category=${ encodeURIComponent ( categoryLabel ) } ` }
201+ className = "inline-flex items-center gap-1 text-[var(--accent-primary) ] transition"
164202 >
165- { post . categories [ 0 ] === 'Growth' ? 'Career' : post . categories [ 0 ] }
203+ { categoryLabel === 'Growth' ? 'Career' : categoryLabel }
166204 </ Link >
167205 </ div >
168206 ) }
@@ -172,7 +210,14 @@ export default async function PostDetails({
172210
173211 { ( authorName || post . publishedAt ) && (
174212 < div className = "mt-4 flex justify-center gap-2 text-sm text-gray-500 dark:text-gray-400" >
175- { authorName && < span > { authorName } </ span > }
213+ { authorName && (
214+ < Link
215+ href = { `/blog?author=${ encodeURIComponent ( authorName ) } ` }
216+ className = "transition hover:text-[var(--accent-primary)]"
217+ >
218+ { authorName }
219+ </ Link >
220+ ) }
176221 { authorName && post . publishedAt && < span > ·</ span > }
177222 { post . publishedAt && (
178223 < span > { new Date ( post . publishedAt ) . toLocaleDateString ( ) } </ span >
@@ -199,7 +244,14 @@ export default async function PostDetails({
199244 const text = ( block . children || [ ] )
200245 . map ( ( c : any ) => c . text || '' )
201246 . join ( '' ) ;
202- return < p key = { block . _key || `block-${ index } ` } > { text } </ p > ;
247+ return (
248+ < p
249+ key = { block . _key || `block-${ index } ` }
250+ className = "whitespace-pre-line"
251+ >
252+ { linkifyText ( text ) }
253+ </ p >
254+ ) ;
203255 }
204256
205257 if ( block ?. _type === 'image' && block ?. url ) {
@@ -219,57 +271,64 @@ export default async function PostDetails({
219271
220272 { recommendedPosts . length > 0 && (
221273 < >
222- < div className = "mt-16 flex justify-center " >
223- < div className = "h-10 w-px bg-gray-200 dark:bg-gray-800" />
274+ < div className = "mt-16 relative left-1/2 right-1/2 w-screen -translate-x-1/2 px-6 " >
275+ < div className = "mx-auto h-px w-full max-w-6xl bg-gray-200 dark:bg-gray-800" />
224276 </ div >
225277
226- < section className = "mt-10" >
227- < h2 className = "text-2xl font-semibold text-gray-900 dark:text-gray-100" >
228- { t ( 'recommendedPosts' ) }
229- </ h2 >
230- < div className = "mt-6 grid gap-6 sm:grid-cols-2 lg:grid-cols-3" >
231- { recommendedPosts . map ( item => (
232- < Link
233- key = { item . _id }
234- href = { `/blog/${ item . slug ?. current } ` }
235- className = "group block"
236- >
237- { item . mainImage && (
238- < div className = "relative h-44 w-full overflow-hidden rounded-2xl" >
239- < Image
240- src = { item . mainImage }
241- alt = { item . title || 'Post image' }
242- fill
243- className = "object-cover transition-transform duration-300 group-hover:scale-[1.03]"
244- />
245- </ div >
246- ) }
247- < h3 className = "mt-4 text-lg font-semibold text-gray-900 transition group-hover:text-[#ff00ff] dark:text-gray-100" >
278+ < section className = "mt-10 relative left-1/2 right-1/2 w-screen -translate-x-1/2 px-6" >
279+ < div className = "mx-auto max-w-6xl" >
280+ < h2 className = "text-2xl font-semibold text-gray-900 dark:text-gray-100" >
281+ { t ( 'recommendedPosts' ) }
282+ </ h2 >
283+ < div className = "mt-6 grid gap-6 auto-rows-fr sm:grid-cols-2 lg:grid-cols-3" >
284+ { recommendedPosts . map ( item => (
285+ < Link
286+ key = { item . _id }
287+ href = { `/blog/${ item . slug ?. current } ` }
288+ className = "group flex h-full flex-col"
289+ >
290+ { item . mainImage && (
291+ < div className = "relative h-48 w-full overflow-hidden rounded-2xl" >
292+ < Image
293+ src = { item . mainImage }
294+ alt = { item . title || 'Post image' }
295+ fill
296+ className = "object-cover transition-transform duration-300 group-hover:scale-[1.03]"
297+ />
298+ </ div >
299+ ) }
300+ < h3 className = "mt-4 text-lg font-semibold text-gray-900 transition group-hover:underline underline-offset-4 dark:text-gray-100" >
248301 { item . title }
249302 </ h3 >
303+ { item . body && (
304+ < p className = "mt-2 text-sm leading-relaxed text-gray-600 dark:text-gray-400 line-clamp-2" >
305+ { plainTextFromPortableText ( item . body ) }
306+ </ p >
307+ ) }
250308 { ( item . author ?. name || item . publishedAt ) && (
251- < div className = "mt-2 flex items-center gap-2 text-sm text-gray-500 dark:text-gray-400" >
309+ < div className = "mt-auto pt-3 flex items-center gap-2 text-sm text-gray-500 dark:text-gray-400" >
252310 { item . author ?. image && (
253311 < span className = "relative h-5 w-5 overflow-hidden rounded-full" >
254312 < Image
255313 src = { item . author . image }
256- alt = { item . author . name || 'Author' }
257- fill
258- className = "object-cover"
259- />
260- </ span >
261- ) }
262- { item . author ?. name && < span > { item . author . name } </ span > }
263- { item . author ?. name && item . publishedAt && < span > ·</ span > }
264- { item . publishedAt && (
265- < span >
266- { new Date ( item . publishedAt ) . toLocaleDateString ( ) }
267- </ span >
268- ) }
269- </ div >
270- ) }
271- </ Link >
272- ) ) }
314+ alt = { item . author . name || 'Author' }
315+ fill
316+ className = "object-cover"
317+ />
318+ </ span >
319+ ) }
320+ { item . author ?. name && < span > { item . author . name } </ span > }
321+ { item . author ?. name && item . publishedAt && < span > ·</ span > }
322+ { item . publishedAt && (
323+ < span >
324+ { new Date ( item . publishedAt ) . toLocaleDateString ( ) }
325+ </ span >
326+ ) }
327+ </ div >
328+ ) }
329+ </ Link >
330+ ) ) }
331+ </ div >
273332 </ div >
274333 </ section >
275334 </ >
0 commit comments