1
1
mirror of https://github.com/theoludwig/theoludwig.git synced 2024-09-19 22:15:53 +02:00

fix: loader improvements

This commit is contained in:
Théo LUDWIG 2023-08-01 18:59:45 +02:00
parent e51e3bdc19
commit d2578abeec
Signed by: theoludwig
GPG Key ID: ADFE5A563D718F3B
11 changed files with 217 additions and 207 deletions

View 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

View File

@ -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
View 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

View File

@ -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 =

View File

@ -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
View 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
View 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>
)
}

View File

@ -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
} }
) )

View File

@ -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;
}

View File

@ -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>
)
}

View 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>
)
}