mirror of
https://github.com/theoludwig/theoludwig.git
synced 2024-11-08 22:31:30 +01:00
fix: loader improvements
This commit is contained in:
parent
e51e3bdc19
commit
d2578abeec
11
app/blog/[slug]/loading.tsx
Normal file
11
app/blog/[slug]/loading.tsx
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import { Loader } from '@/components/design/Loader'
|
||||||
|
|
||||||
|
const Loading = (): JSX.Element => {
|
||||||
|
return (
|
||||||
|
<main className='flex flex-col flex-1 items-center justify-center'>
|
||||||
|
<Loader />
|
||||||
|
</main>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Loading
|
@ -1,46 +1,10 @@
|
|||||||
import type { Metadata } from 'next'
|
import type { Metadata } from 'next'
|
||||||
import { notFound } from 'next/navigation'
|
import { notFound } from 'next/navigation'
|
||||||
import { cookies } from 'next/headers'
|
|
||||||
import Link from 'next/link'
|
|
||||||
import Image from 'next/image'
|
|
||||||
import { MDXRemote } from 'next-mdx-remote/rsc'
|
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
|
||||||
import { faLink } from '@fortawesome/free-solid-svg-icons'
|
|
||||||
import date from 'date-and-time'
|
|
||||||
import { nodeTypes } from '@mdx-js/mdx'
|
|
||||||
import rehypeRaw from 'rehype-raw'
|
|
||||||
import remarkGfm from 'remark-gfm'
|
|
||||||
import rehypeSlug from 'rehype-slug'
|
|
||||||
import remarkMath from 'remark-math'
|
|
||||||
import rehypeKatex from 'rehype-katex'
|
|
||||||
import { getHighlighter } from 'shiki'
|
|
||||||
|
|
||||||
import 'katex/dist/katex.min.css'
|
import 'katex/dist/katex.min.css'
|
||||||
|
|
||||||
import { remarkSyntaxHighlightingPlugin } from '@/blog/remarkSyntaxHighlightingPlugin'
|
|
||||||
import { getBlogPostBySlug } from '@/blog/blog'
|
import { getBlogPostBySlug } from '@/blog/blog'
|
||||||
import { BlogPostComments } from '@/blog/BlogPostComments'
|
import { BlogPost } from '@/blog/BlogPost'
|
||||||
import { getTheme } from '@/theme/theme.server'
|
|
||||||
|
|
||||||
const Heading = (
|
|
||||||
props: React.DetailedHTMLProps<
|
|
||||||
React.HTMLAttributes<HTMLHeadingElement>,
|
|
||||||
HTMLHeadingElement
|
|
||||||
>
|
|
||||||
): JSX.Element => {
|
|
||||||
const { children, id = '' } = props
|
|
||||||
return (
|
|
||||||
<h2 {...props} className='group'>
|
|
||||||
<Link
|
|
||||||
href={`#${id}`}
|
|
||||||
className='invisible !text-black group-hover:visible dark:!text-white'
|
|
||||||
>
|
|
||||||
<FontAwesomeIcon className='mr-2 inline h-4 w-4' icon={faLink} />
|
|
||||||
</Link>
|
|
||||||
{children}
|
|
||||||
</h2>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
interface BlogPostPageProps {
|
interface BlogPostPageProps {
|
||||||
params: {
|
params: {
|
||||||
@ -52,7 +16,7 @@ export const generateMetadata = async (
|
|||||||
props: BlogPostPageProps
|
props: BlogPostPageProps
|
||||||
): Promise<Metadata> => {
|
): Promise<Metadata> => {
|
||||||
const blogPost = await getBlogPostBySlug(props.params.slug)
|
const blogPost = await getBlogPostBySlug(props.params.slug)
|
||||||
if (blogPost == null || !blogPost.frontmatter.isPublished) {
|
if (blogPost == null) {
|
||||||
return notFound()
|
return notFound()
|
||||||
}
|
}
|
||||||
const title = `${blogPost.frontmatter.title} | Théo LUDWIG`
|
const title = `${blogPost.frontmatter.title} | Théo LUDWIG`
|
||||||
@ -74,85 +38,7 @@ export const generateMetadata = async (
|
|||||||
const BlogPostPage = async (props: BlogPostPageProps): Promise<JSX.Element> => {
|
const BlogPostPage = async (props: BlogPostPageProps): Promise<JSX.Element> => {
|
||||||
const { params } = props
|
const { params } = props
|
||||||
|
|
||||||
const blogPost = await getBlogPostBySlug(params.slug)
|
return <BlogPost slug={params.slug} />
|
||||||
if (blogPost == null || !blogPost.frontmatter.isPublished) {
|
|
||||||
return notFound()
|
|
||||||
}
|
|
||||||
|
|
||||||
const cookiesStore = cookies()
|
|
||||||
const theme = getTheme()
|
|
||||||
|
|
||||||
const highlighter = await getHighlighter({
|
|
||||||
theme: `${theme}-plus`
|
|
||||||
})
|
|
||||||
|
|
||||||
return (
|
|
||||||
<main className='break-wrap-words flex flex-1 flex-col flex-wrap items-center'>
|
|
||||||
<div className='my-10 flex flex-col items-center text-center'>
|
|
||||||
<h1 className='text-3xl font-semibold'>{blogPost.frontmatter.title}</h1>
|
|
||||||
<p className='mt-2' data-cy='blog-post-date'>
|
|
||||||
{date.format(
|
|
||||||
new Date(blogPost.frontmatter.publishedOn),
|
|
||||||
'DD/MM/YYYY'
|
|
||||||
)}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className='prose mb-10'>
|
|
||||||
<div className='px-8'>
|
|
||||||
<MDXRemote
|
|
||||||
source={blogPost.content}
|
|
||||||
options={{
|
|
||||||
mdxOptions: {
|
|
||||||
remarkPlugins: [
|
|
||||||
remarkGfm,
|
|
||||||
[remarkSyntaxHighlightingPlugin, { highlighter }],
|
|
||||||
remarkMath
|
|
||||||
],
|
|
||||||
rehypePlugins: [
|
|
||||||
rehypeSlug,
|
|
||||||
[rehypeRaw, { passThrough: nodeTypes }],
|
|
||||||
rehypeKatex
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
components={{
|
|
||||||
h1: Heading,
|
|
||||||
h2: Heading,
|
|
||||||
h3: Heading,
|
|
||||||
h4: Heading,
|
|
||||||
h5: Heading,
|
|
||||||
h6: Heading,
|
|
||||||
img: (properties) => {
|
|
||||||
const { src = '', alt = 'Blog Image' } = properties
|
|
||||||
const source = src.replace('../../public/', '/')
|
|
||||||
return (
|
|
||||||
<span className='flex flex-col items-center justify-center'>
|
|
||||||
<Image
|
|
||||||
src={source}
|
|
||||||
alt={alt}
|
|
||||||
width={1000}
|
|
||||||
height={1000}
|
|
||||||
className='h-auto w-auto'
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
)
|
|
||||||
},
|
|
||||||
a: (props) => {
|
|
||||||
const { href = '' } = props
|
|
||||||
if (href.startsWith('#')) {
|
|
||||||
return <a {...props} />
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<a target='_blank' rel='noopener noreferrer' {...props} />
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<BlogPostComments cookiesStore={cookiesStore.toString()} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default BlogPostPage
|
export default BlogPostPage
|
||||||
|
11
app/blog/loading.tsx
Normal file
11
app/blog/loading.tsx
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import { Loader } from '@/components/design/Loader'
|
||||||
|
|
||||||
|
const Loading = (): JSX.Element => {
|
||||||
|
return (
|
||||||
|
<main className='flex flex-col flex-1 items-center justify-center'>
|
||||||
|
<Loader />
|
||||||
|
</main>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Loading
|
@ -2,7 +2,7 @@ import { Suspense } from 'react'
|
|||||||
import type { Metadata } from 'next'
|
import type { Metadata } from 'next'
|
||||||
|
|
||||||
import { BlogPosts } from '@/blog/BlogPosts'
|
import { BlogPosts } from '@/blog/BlogPosts'
|
||||||
import { Loader } from '@/components/Loader/Loader'
|
import { Loader } from '@/components/design/Loader'
|
||||||
|
|
||||||
const title = 'Blog | Théo LUDWIG'
|
const title = 'Blog | Théo LUDWIG'
|
||||||
const description =
|
const description =
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { Loader } from '@/components/Loader/Loader'
|
import { Loader } from '@/components/design/Loader'
|
||||||
|
|
||||||
const Loading = (): JSX.Element => {
|
const Loading = (): JSX.Element => {
|
||||||
return (
|
return (
|
||||||
|
35
blog/BlogPost.tsx
Normal file
35
blog/BlogPost.tsx
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
import { notFound } from 'next/navigation'
|
||||||
|
import date from 'date-and-time'
|
||||||
|
|
||||||
|
import 'katex/dist/katex.min.css'
|
||||||
|
|
||||||
|
import { getBlogPostBySlug } from '@/blog/blog'
|
||||||
|
import { BlogPostContent } from '@/blog/BlogPostContent'
|
||||||
|
|
||||||
|
export interface BlogPostProps {
|
||||||
|
slug: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const BlogPost = async (props: BlogPostProps): Promise<JSX.Element> => {
|
||||||
|
const { slug } = props
|
||||||
|
|
||||||
|
const blogPost = await getBlogPostBySlug(slug)
|
||||||
|
if (blogPost == null) {
|
||||||
|
return notFound()
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<main className='break-wrap-words flex flex-1 flex-col flex-wrap items-center justify-center'>
|
||||||
|
<div className='my-10 flex flex-col items-center text-center'>
|
||||||
|
<h1 className='text-3xl font-semibold'>{blogPost.frontmatter.title}</h1>
|
||||||
|
<p className='mt-2' data-cy='blog-post-date'>
|
||||||
|
{date.format(
|
||||||
|
new Date(blogPost.frontmatter.publishedOn),
|
||||||
|
'DD/MM/YYYY'
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<BlogPostContent content={blogPost.content} />
|
||||||
|
</main>
|
||||||
|
)
|
||||||
|
}
|
111
blog/BlogPostContent.tsx
Normal file
111
blog/BlogPostContent.tsx
Normal file
@ -0,0 +1,111 @@
|
|||||||
|
import Image from 'next/image'
|
||||||
|
import Link from 'next/link'
|
||||||
|
import { cookies } from 'next/headers'
|
||||||
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||||
|
import { faLink } from '@fortawesome/free-solid-svg-icons'
|
||||||
|
import { MDXRemote } from 'next-mdx-remote/rsc'
|
||||||
|
import { nodeTypes } from '@mdx-js/mdx'
|
||||||
|
import rehypeRaw from 'rehype-raw'
|
||||||
|
import remarkGfm from 'remark-gfm'
|
||||||
|
import rehypeSlug from 'rehype-slug'
|
||||||
|
import remarkMath from 'remark-math'
|
||||||
|
import rehypeKatex from 'rehype-katex'
|
||||||
|
import { getHighlighter } from 'shiki'
|
||||||
|
|
||||||
|
import 'katex/dist/katex.min.css'
|
||||||
|
|
||||||
|
import { getTheme } from '@/theme/theme.server'
|
||||||
|
import { remarkSyntaxHighlightingPlugin } from '@/blog/remarkSyntaxHighlightingPlugin'
|
||||||
|
import { BlogPostComments } from '@/blog/BlogPostComments'
|
||||||
|
|
||||||
|
const Heading = (
|
||||||
|
props: React.DetailedHTMLProps<
|
||||||
|
React.HTMLAttributes<HTMLHeadingElement>,
|
||||||
|
HTMLHeadingElement
|
||||||
|
>
|
||||||
|
): JSX.Element => {
|
||||||
|
const { children, id = '' } = props
|
||||||
|
return (
|
||||||
|
<h2 {...props} className='group'>
|
||||||
|
<Link
|
||||||
|
href={`#${id}`}
|
||||||
|
className='invisible !text-black group-hover:visible dark:!text-white'
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon className='mr-2 inline h-4 w-4' icon={faLink} />
|
||||||
|
</Link>
|
||||||
|
{children}
|
||||||
|
</h2>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BlogPostContentProps {
|
||||||
|
content: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const BlogPostContent = async (
|
||||||
|
props: BlogPostContentProps
|
||||||
|
): Promise<JSX.Element> => {
|
||||||
|
const { content } = props
|
||||||
|
|
||||||
|
const cookiesStore = cookies()
|
||||||
|
const theme = getTheme()
|
||||||
|
|
||||||
|
const highlighter = await getHighlighter({
|
||||||
|
theme: `${theme}-plus`
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='prose mb-10'>
|
||||||
|
<div className='px-8'>
|
||||||
|
<MDXRemote
|
||||||
|
source={content}
|
||||||
|
options={{
|
||||||
|
mdxOptions: {
|
||||||
|
remarkPlugins: [
|
||||||
|
remarkGfm,
|
||||||
|
[remarkSyntaxHighlightingPlugin, { highlighter }],
|
||||||
|
remarkMath
|
||||||
|
],
|
||||||
|
rehypePlugins: [
|
||||||
|
rehypeSlug,
|
||||||
|
[rehypeRaw, { passThrough: nodeTypes }],
|
||||||
|
rehypeKatex
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
components={{
|
||||||
|
h1: Heading,
|
||||||
|
h2: Heading,
|
||||||
|
h3: Heading,
|
||||||
|
h4: Heading,
|
||||||
|
h5: Heading,
|
||||||
|
h6: Heading,
|
||||||
|
img: (properties) => {
|
||||||
|
const { src = '', alt = 'Blog Image' } = properties
|
||||||
|
const source = src.replace('../../public/', '/')
|
||||||
|
return (
|
||||||
|
<span className='flex flex-col items-center justify-center'>
|
||||||
|
<Image
|
||||||
|
src={source}
|
||||||
|
alt={alt}
|
||||||
|
width={1000}
|
||||||
|
height={1000}
|
||||||
|
className='h-auto w-auto'
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
a: (props) => {
|
||||||
|
const { href = '' } = props
|
||||||
|
if (href.startsWith('#')) {
|
||||||
|
return <a {...props} />
|
||||||
|
}
|
||||||
|
return <a target='_blank' rel='noopener noreferrer' {...props} />
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<BlogPostComments cookiesStore={cookiesStore.toString()} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
32
blog/blog.ts
32
blog/blog.ts
@ -4,7 +4,7 @@ import path from 'node:path'
|
|||||||
import { cache } from 'react'
|
import { cache } from 'react'
|
||||||
import matter from 'gray-matter'
|
import matter from 'gray-matter'
|
||||||
|
|
||||||
export const POSTS_PATH = path.join(process.cwd(), 'blog', 'posts')
|
export const BLOG_POSTS_PATH = path.join(process.cwd(), 'blog', 'posts')
|
||||||
|
|
||||||
export interface FrontMatter {
|
export interface FrontMatter {
|
||||||
title: string
|
title: string
|
||||||
@ -13,21 +13,21 @@ export interface FrontMatter {
|
|||||||
publishedOn: string
|
publishedOn: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Post {
|
export interface BlogPost {
|
||||||
frontmatter: FrontMatter
|
frontmatter: FrontMatter
|
||||||
slug: string
|
slug: string
|
||||||
content: string
|
content: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getBlogPosts = cache(async (): Promise<Post[]> => {
|
export const getBlogPosts = cache(async (): Promise<BlogPost[]> => {
|
||||||
const posts = await fs.promises.readdir(POSTS_PATH)
|
const blogPosts = await fs.promises.readdir(BLOG_POSTS_PATH)
|
||||||
const postsWithTime = await Promise.all(
|
const blogPostsWithTime = await Promise.all(
|
||||||
posts.map(async (postFilename) => {
|
blogPosts.map(async (blogPostFilename) => {
|
||||||
const [slug, extension] = postFilename.split('.')
|
const [slug, extension] = blogPostFilename.split('.')
|
||||||
if (slug == null || extension == null) {
|
if (slug == null || extension == null) {
|
||||||
throw new Error('Invalid postFilename.')
|
throw new Error('Invalid blog post filename.')
|
||||||
}
|
}
|
||||||
const blogPostPath = path.join(POSTS_PATH, `${slug}.${extension}`)
|
const blogPostPath = path.join(BLOG_POSTS_PATH, `${slug}.${extension}`)
|
||||||
const blogPostContent = await fs.promises.readFile(blogPostPath, {
|
const blogPostContent = await fs.promises.readFile(blogPostPath, {
|
||||||
encoding: 'utf8'
|
encoding: 'utf8'
|
||||||
})
|
})
|
||||||
@ -44,22 +44,22 @@ export const getBlogPosts = cache(async (): Promise<Post[]> => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
const postsWithTimeSorted = postsWithTime
|
const blogPostsSortedByPublicationDate = blogPostsWithTime
|
||||||
.filter((post) => {
|
.filter((post) => {
|
||||||
return post.frontmatter.isPublished
|
return post.frontmatter.isPublished
|
||||||
})
|
})
|
||||||
.sort((a, b) => {
|
.sort((a, b) => {
|
||||||
return b.time - a.time
|
return b.time - a.time
|
||||||
})
|
})
|
||||||
return postsWithTimeSorted
|
return blogPostsSortedByPublicationDate
|
||||||
})
|
})
|
||||||
|
|
||||||
export const getBlogPostBySlug = cache(
|
export const getBlogPostBySlug = cache(
|
||||||
async (slug: string): Promise<Post | undefined> => {
|
async (slug: string): Promise<BlogPost | undefined> => {
|
||||||
const posts = await getBlogPosts()
|
const blogPosts = await getBlogPosts()
|
||||||
const post = posts.find((post) => {
|
const blogPost = blogPosts.find((blogPost) => {
|
||||||
return post.slug === slug
|
return blogPost.slug === slug && blogPost.frontmatter.isPublished
|
||||||
})
|
})
|
||||||
return post
|
return blogPost
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
@ -1,39 +0,0 @@
|
|||||||
@keyframes progressSpinnerRotate {
|
|
||||||
100% {
|
|
||||||
transform: rotate(360deg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@keyframes progressSpinnerDash {
|
|
||||||
0% {
|
|
||||||
stroke-dasharray: 1, 200;
|
|
||||||
stroke-dashoffset: 0;
|
|
||||||
}
|
|
||||||
50% {
|
|
||||||
stroke-dasharray: 89, 200;
|
|
||||||
stroke-dashoffset: -35px;
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
stroke-dasharray: 89, 200;
|
|
||||||
stroke-dashoffset: -124px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.progressSpinnerSvg {
|
|
||||||
animation: progressSpinnerRotate 2s linear infinite;
|
|
||||||
height: 100%;
|
|
||||||
transform-origin: center center;
|
|
||||||
width: 100%;
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
bottom: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
margin: auto;
|
|
||||||
}
|
|
||||||
.progressSpinnerCircle {
|
|
||||||
stroke-dasharray: 89, 200;
|
|
||||||
stroke-dashoffset: 0;
|
|
||||||
stroke: #ffd800;
|
|
||||||
animation: progressSpinnerDash 1.5s ease-in-out infinite;
|
|
||||||
stroke-linecap: round;
|
|
||||||
}
|
|
@ -1,33 +0,0 @@
|
|||||||
import styles from './Loader.module.css'
|
|
||||||
|
|
||||||
export interface LoaderProps {
|
|
||||||
width?: number
|
|
||||||
height?: number
|
|
||||||
className?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export const Loader = (props: LoaderProps): JSX.Element => {
|
|
||||||
const { width = 50, height = 50, className } = props
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={className}>
|
|
||||||
<div
|
|
||||||
data-cy='progress-spinner'
|
|
||||||
className='relative mx-auto my-0 before:block before:pt-[100%] before:content-none'
|
|
||||||
style={{ width: `${width}px`, height: `${height}px` }}
|
|
||||||
>
|
|
||||||
<svg className={styles['progressSpinnerSvg']} viewBox='25 25 50 50'>
|
|
||||||
<circle
|
|
||||||
className={styles['progressSpinnerCircle']}
|
|
||||||
cx='50'
|
|
||||||
cy='50'
|
|
||||||
r='20'
|
|
||||||
fill='none'
|
|
||||||
strokeWidth='2'
|
|
||||||
strokeMiterlimit='10'
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
28
components/design/Loader.tsx
Normal file
28
components/design/Loader.tsx
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import classNames from 'clsx'
|
||||||
|
|
||||||
|
export interface LoaderProps {
|
||||||
|
width?: number
|
||||||
|
height?: number
|
||||||
|
className?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Loader = (props: LoaderProps): JSX.Element => {
|
||||||
|
const { width = 50, height = 50, className } = props
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width,
|
||||||
|
height
|
||||||
|
}}
|
||||||
|
className={classNames(
|
||||||
|
'animate-spin inline-block border-[3px] border-current border-t-transparent text-yellow dark:text-yellow-dark rounded-full',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
role='status'
|
||||||
|
aria-label='loading'
|
||||||
|
>
|
||||||
|
<span className='sr-only'>Loading...</span>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user