Skip to content

Commit fcd9cf1

Browse files
committed
feat: add portable text highlight and client code
1 parent c068dfb commit fcd9cf1

File tree

13 files changed

+529
-14
lines changed

13 files changed

+529
-14
lines changed

app/(personal)/blog/[slug]/page.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import dynamic from 'next/dynamic'
44
import { draftMode } from 'next/headers'
55
import { notFound } from 'next/navigation'
66

7-
import { Page } from '@/components/pages/page/Page'
7+
import { Post } from '@/components/pages/post/Post'
88
import { generateStaticSlugs } from '@/sanity/loader/generateStaticSlugs'
99
import { loadPost } from '@/sanity/loader/loadQuery'
1010
const PagePreview = dynamic(() => import('@/components/pages/page/PagePreview'))
@@ -42,5 +42,5 @@ export default async function PageSlugRoute({ params }: Props) {
4242
notFound()
4343
}
4444

45-
return <Page data={initial.data} />
45+
return <Post data={initial.data} />
4646
}

components/pages/home/PostListItem.tsx

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import clsx from 'clsx'
33

44
import { CustomPortableText } from '@/components/shared/CustomPortableText'
55
import ImageBox from '@/components/shared/ImageBox'
6+
import { formatTimeSince } from '@/sanity/lib/utils'
67
import type { ShowcasePost } from '@/types'
78

89
interface PostProps {
@@ -12,8 +13,7 @@ interface PostProps {
1213

1314
export function PostListItem(props: PostProps) {
1415
const { post, odd } = props
15-
const formattedDate = new Date(post.publishedAt as string).toLocaleDateString('en-US')
16-
const daysAgo = Math.floor((new Date().getTime() - new Date(post.publishedAt as string).getTime()) / (1000 * 3600 * 24))
16+
const timeSince = formatTimeSince(post.publishedAt)
1717

1818
return (
1919
<div
@@ -42,7 +42,7 @@ export function PostListItem(props: PostProps) {
4242
])}>
4343
<p className={clsx([
4444
'font-mono text-sm leading-mono last:mb-0 mb-1 font-normal uppercase text-gray-dark'
45-
])}>{`${formattedDate} (${daysAgo < 1 ? 'today' : `${daysAgo} days ago`})`}</p>
45+
])}>{`${timeSince && `${timeSince.formattedDate} (about ${timeSince.timeSince})`}`}</p>
4646

4747
<h2 className={clsx([
4848
'font-extrabold tracking-tight',
@@ -60,7 +60,11 @@ export function PostListItem(props: PostProps) {
6060
height={42}
6161
image={post.author?.coverImage}
6262
alt={`Cover image from ${post.author?.title?.replace(/[\u200B-\u200D\uFEFF]/g, '')}`}
63-
classesWrapper='px-[0.5px] flex shrink-0 grow-0 w-[42px] relative rounded-full aspect-[1/1]'
63+
classesWrapper={clsx([
64+
'flex shrink-0 grow-0 w-[42px] relative rounded-full aspect-[1/1] object-cover border',
65+
'border-slate-500',
66+
'dark:border-black'
67+
])}
6468
/>
6569
</div>
6670

components/pages/page/Page.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ export function Page({ data }: PageProps) {
2727
<div className={clsx([
2828
'block -m-[0.5px] p-16'
2929
])}>
30-
{body && <CustomPortableText paragraphClasses='tracking-wide font-serif text-xl w-full max-w-[60rem] mx-auto py-2' value={body} />}
30+
{body && <CustomPortableText paragraphClasses='tracking-wide font-serif text-xl w-full max-w-[60rem] mx-auto py-[0.5rem]' value={body} />}
3131
</div>
3232
</GeometricContainer>
3333
</>

components/pages/post/Post.tsx

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import clsx from 'clsx'
2+
3+
import { CustomPortableText } from '@/components/shared/CustomPortableText'
4+
import GeometricContainer from '@/components/shared/GeometricContainer'
5+
import { Header } from '@/components/shared/Header'
6+
import ImageBox from '@/components/shared/ImageBox'
7+
import { formatTimeSince } from '@/sanity/lib/utils'
8+
import type { PostPayload } from '@/types'
9+
10+
export interface PostProps {
11+
data: PostPayload | null
12+
}
13+
14+
export function Post({ data }: PostProps) {
15+
const { body, publishedAt, title, author, categories } = data ?? {}
16+
const timeSince = formatTimeSince(publishedAt)
17+
18+
return (
19+
<>
20+
<GeometricContainer>
21+
<div className={clsx([
22+
'p-16 relative w-full-1 overflow-hidden',
23+
'bg-neutral-100/90',
24+
'dark:bg-neutral-900/90',
25+
])}>
26+
<div className={clsx([
27+
'max-w-[60rem] mx-auto [text-wrap:pretty]',
28+
'tracking-wide font-serif text-xl'
29+
])}>
30+
<p className={clsx([
31+
'font-mono text-sm leading-mono last:mb-0 mb-1 px-1 font-normal uppercase text-gray-dark',
32+
])}>{`${timeSince && `${timeSince.formattedDate} (about ${timeSince.timeSince})`}`}</p>
33+
34+
<Header title={title} />
35+
36+
<div className={clsx([
37+
'flex gap-[1rem] flex-row items-center px-1',
38+
])}>
39+
<div className={clsx([
40+
'w-[42px]',
41+
])}>
42+
<ImageBox
43+
width={42}
44+
height={42}
45+
image={author?.coverImage}
46+
alt={`Cover image from ${author?.title}`}
47+
classesWrapper={clsx([
48+
'flex shrink-0 grow-0 w-[42px] relative rounded-full aspect-[1/1] object-cover border',
49+
'border-slate-500 hover:border-slate-800 focus-visible:border-slate-800',
50+
'dark:border-black dark:hover:border-neutral-700 dark:focus-visible:border-neutral-700'
51+
])}
52+
/>
53+
</div>
54+
55+
<div className='font-mono text-sm leading-mono font-normal uppercase text-gray-dark'>
56+
by {author?.title}{categories?.map((tag, key) => (
57+
<span className='whitespace-nowrap' key={key}>{tag}</span>
58+
))}
59+
</div>
60+
</div>
61+
62+
<hr className={clsx([
63+
'border-neutral-300 dark:border-neutral-700 my-2 first:mt-0'
64+
])} />
65+
66+
{body && <CustomPortableText value={body} />}
67+
</div>
68+
</div>
69+
</GeometricContainer>
70+
</>
71+
)
72+
}
73+
74+
export default Post
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
'use client'
2+
3+
import { type QueryResponseInitial } from '@sanity/react-loader/rsc'
4+
5+
import { pagesBySlugQuery } from '@/sanity/lib/queries'
6+
import { useQuery } from '@/sanity/loader/useQuery'
7+
import { PagePayload } from '@/types'
8+
9+
import Post from './Post'
10+
11+
type Props = {
12+
params: { slug: string }
13+
initial: QueryResponseInitial<PagePayload | null>
14+
}
15+
16+
export default function PostPreview(props: Props) {
17+
const { params, initial } = props
18+
const { data } = useQuery<PagePayload | null>(pagesBySlugQuery, params, {
19+
initial,
20+
})
21+
22+
return <Post data={data!} />
23+
}

components/shared/CustomPortableText.tsx

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { PortableText, PortableTextComponents } from '@portabletext/react'
22
import type { PortableTextBlock } from '@portabletext/types'
33
import { Image } from 'sanity'
44

5+
import HighlightCode from '@/components/shared/HighlightCode'
56
import ImageBox from '@/components/shared/ImageBox'
67
import { TimelineSection } from '@/components/shared/TimelineSection'
78

@@ -13,10 +14,39 @@ export function CustomPortableText({
1314
value: PortableTextBlock[]
1415
}) {
1516
const components: PortableTextComponents = {
17+
list: {
18+
bullet: ({children}) => <ul className="mt-xl list-disc">{children}</ul>,
19+
number: ({children}) => <ol className="mt-lg list-decimal">{children}</ol>,
20+
},
1621
block: {
1722
normal: ({ children }) => {
1823
return <p className={paragraphClasses}>{children}</p>
1924
},
25+
h1: ({ children }) => {
26+
return <h1 className="text-4xl font-serif font-bold">{children}</h1>
27+
},
28+
h2: ({ children }) => {
29+
return <h2 className="text-3xl font-serif font-bold">{children}</h2>
30+
},
31+
h3: ({ children }) => {
32+
return <h3 className="text-2xl font-serif font-bold">{children}</h3>
33+
},
34+
h4: ({ children }) => {
35+
return <h4 className="text-xl font-serif font-bold">{children}</h4>
36+
},
37+
h5: ({ children }) => {
38+
return <h5 className="text-lg font-serif font-bold">{children}</h5>
39+
},
40+
h6: ({ children }) => {
41+
return <h6 className="text-base font-serif font-bold">{children}</h6>
42+
},
43+
blockquote: ({ children }) => {
44+
return (
45+
<blockquote className="border-l-4 border-neutral-500 pl-1">
46+
{children}
47+
</blockquote>
48+
)
49+
},
2050
},
2151
marks: {
2252
link: ({ children, value }) => {
@@ -30,6 +60,10 @@ export function CustomPortableText({
3060
</a>
3161
)
3262
},
63+
em: ({children}) => <em className="text-gray-600 font-semibold">{children}</em>,
64+
strong: ({children}) => <strong className="font-semibold">{children}</strong>,
65+
strikeThrough: ({children}) => <del className="line-through">{children}</del>,
66+
underline: ({children}) => <u className="underline">{children}</u>,
3367
},
3468
types: {
3569
image: ({
@@ -38,7 +72,7 @@ export function CustomPortableText({
3872
value: Image & { alt?: string; caption?: string }
3973
}) => {
4074
return (
41-
<div className="my-6 space-y-2">
75+
<div className="my-1 space-y-1 max-w-[60rem] mx-auto">
4276
<ImageBox
4377
image={value}
4478
alt={value.alt}
@@ -56,6 +90,13 @@ export function CustomPortableText({
5690
const { items } = value || {}
5791
return <TimelineSection timelines={items} />
5892
},
93+
code: ({ value }) => {
94+
const { code, language } = value || {}
95+
96+
return (
97+
<HighlightCode code={code} language={language} />
98+
)
99+
},
59100
},
60101
}
61102

components/shared/Header.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@ export function Header(props: HeaderProps) {
1515
}
1616
return (
1717
<div className={clsx([
18-
'font-mono',
19-
centered ? 'text-center p-1 -m-[0.5px]' : 'w-5/6 lg:w-4/6'
18+
'font-mono p-1 -m-[0.5px]',
19+
centered ? 'text-center' : 'text-left'
2020
])}>
2121
{/* Title */}
2222
{title && <h1 className="text-3xl font-extrabold tracking-tight md:text-5xl">{title}</h1>}
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
'use client';
2+
3+
import clsx from 'clsx'
4+
import { Highlight, themes } from 'prism-react-renderer'
5+
6+
interface HighlightCodeProps {
7+
code: string
8+
language: string
9+
}
10+
11+
export function HighlightCode(props: HighlightCodeProps) {
12+
const { code, language } = props
13+
14+
return (
15+
<Highlight code={code} language={language} theme={themes.oneDark}>
16+
{({ style, tokens, getLineProps, getTokenProps }) => (
17+
<figure className={clsx([
18+
'block bg-white text-black border -m-[0.5px] relative w-full-1 rounded-xl overflow-hidden',
19+
'dark:bg-black dark:text-white max-w-[60rem] mx-auto',
20+
])}>
21+
<figcaption className={clsx([
22+
'bg-putty text-black dark:text-white border -mx-[1px] -mt-[1px] flex justify-between',
23+
'dark:bg-charcoal ',
24+
])}>
25+
<div className={clsx([
26+
'font-body text-base tracking-wide leading-normal font-normal normal-case py-[0.5rem] px-[0.5rem]'
27+
])}>{language}</div>
28+
29+
<button className={clsx([
30+
'relative group/button inline-block -m-[1px] font-mono text-sm leading-mono font-normal uppercase',
31+
])}>
32+
<span
33+
className="opacity-100 pointer-events-auto col-start-1 row-start-1 flex items-center justify-center">
34+
<svg
35+
width="16" height="18" viewBox="0 0 16 18" fill="none" xmlns="http://www.w3.org/2000/svg"
36+
className="mr-0.5">
37+
<rect x="3.5" y="3.5" width="12" height="14" className="stroke-current" vectorEffect="non-scaling-stroke"></rect>
38+
<path d="M3.5 14.5H0.5V0.5H12.5V3.5" className="stroke-current" vectorEffect="non-scaling-stroke"></path>
39+
</svg>
40+
<span className="sr-only">Copy Icon</span>
41+
Copy
42+
</span>
43+
</button>
44+
</figcaption>
45+
46+
<pre style={style} className={clsx([
47+
'overflow-x-scroll px-[0.875rem] max-h-[42rem] overflow-y-scroll py-0'
48+
])}>
49+
<div className={clsx([
50+
'sticky top-0 h-[0.5rem] mb-[0.5rem] bg-gradient-to-b from-black z-10'
51+
])}></div>
52+
53+
<code className={clsx([
54+
'font-mono leading-code normal-case max-w-full text-sm [counter-reset:linenumber]',
55+
])}>
56+
{tokens.map((line, i) => (
57+
<div key={i} {...getLineProps({ line })} className={clsx([
58+
`[padding-left:calc(0.875rem+2ch)]`
59+
])}>
60+
<span className={clsx([
61+
'[counter-increment:linenumber] inline-block w-0',
62+
`before:whitespace-nowrap before:content-[counter(linenumber)] relative text-right inline-block w-[2ch] [left:calc(-0.875rem-2ch)]`
63+
])}></span>
64+
{line.map((token, key) => (
65+
<span key={key} {...getTokenProps({ token })} />
66+
))}
67+
</div>
68+
))}
69+
</code>
70+
71+
<div className={clsx([
72+
'sticky bottom-0 h-[0.5rem] mt-[0.5rem] bg-gradient-to-t from-black z-10'
73+
])}></div>
74+
</pre>
75+
</figure>
76+
)}
77+
</Highlight>
78+
);
79+
}
80+
81+
export default HighlightCode

0 commit comments

Comments
 (0)