Fixed few formatting things; started on support for mobile/PWA
This commit is contained in:
parent
1a713fe1b6
commit
b1de88f998
31 changed files with 276 additions and 239 deletions
|
|
@ -10,9 +10,5 @@
|
|||
"arrowParens": "always",
|
||||
"endOfLine": "lf",
|
||||
"insertFinalNewline": true,
|
||||
"ignorePatterns": [
|
||||
"node_modules/**",
|
||||
".cache/**",
|
||||
"_site/**"
|
||||
]
|
||||
"ignorePatterns": ["node_modules/**", ".cache/**", "_site/**"]
|
||||
}
|
||||
|
|
@ -5,7 +5,6 @@ import type { Metadata } from 'next'
|
|||
import Link from 'next/link'
|
||||
import { metadata } from '@/app/metadata'
|
||||
import { dateFormatter } from '@/utils/time'
|
||||
import { RecentlyUpdatedPosts } from '@/components/common/RecentlyUpdatedPosts'
|
||||
|
||||
export async function generateMetadata(): Promise<Metadata> {
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ export default function BlogIndex() {
|
|||
}
|
||||
|
||||
return (
|
||||
<main className="max-w-3xl mx-auto px-4 py-8 lg:py-16">
|
||||
<main className="max-w-3xl mx-auto px-4 py-8 mb-16 lg:py-16">
|
||||
<section>
|
||||
{posts.length === 0 ? (
|
||||
<div className="rounded-xl border border-border p-6 text-fg-dim">
|
||||
|
|
|
|||
|
|
@ -1,24 +1,22 @@
|
|||
import { metadata } from "@/app/metadata"
|
||||
import { getAllPostsMeta } from "@/lib/posts"
|
||||
import { metadata } from '@/app/metadata'
|
||||
import { getAllPostsMeta } from '@/lib/posts'
|
||||
|
||||
const SITE_URL = process.env.SITE_URL || "http://localhost:3000"
|
||||
const SITE_URL = process.env.SITE_URL || 'http://localhost:3000'
|
||||
|
||||
function escapeXml(str: string) {
|
||||
return str
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """)
|
||||
.replace(/'/g, "'")
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''')
|
||||
}
|
||||
|
||||
export async function GET(req: Request) {
|
||||
const posts = getAllPostsMeta()
|
||||
|
||||
const updated =
|
||||
posts.length > 0
|
||||
? new Date(posts[0].date).toISOString()
|
||||
: new Date().toISOString()
|
||||
posts.length > 0 ? new Date(posts[0].date).toISOString() : new Date().toISOString()
|
||||
|
||||
const entries = posts
|
||||
.map((p) => {
|
||||
|
|
@ -35,7 +33,7 @@ export async function GET(req: Request) {
|
|||
<summary>${escapeXml(p.description)}</summary>
|
||||
</entry>`
|
||||
})
|
||||
.join("")
|
||||
.join('')
|
||||
|
||||
const feed = `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<feed xmlns="http://www.w3.org/2005/Atom">
|
||||
|
|
@ -51,12 +49,12 @@ export async function GET(req: Request) {
|
|||
${entries}
|
||||
</feed>`
|
||||
|
||||
const accept = req.headers.get("accept") || ""
|
||||
const contentType = accept.includes("application/atom+xml")
|
||||
? "application/atom+xml; charset=utf-8"
|
||||
: "application/xml; charset=utf-8"
|
||||
const accept = req.headers.get('accept') || ''
|
||||
const contentType = accept.includes('application/atom+xml')
|
||||
? 'application/atom+xml; charset=utf-8'
|
||||
: 'application/xml; charset=utf-8'
|
||||
|
||||
return new Response(feed, {
|
||||
headers: { "Content-Type": contentType },
|
||||
headers: { 'Content-Type': contentType },
|
||||
})
|
||||
}
|
||||
|
|
@ -1,7 +1,9 @@
|
|||
import { Space_Grotesk } from 'next/font/google'
|
||||
import Script from 'next/script'
|
||||
import { Sidebar } from '@/components/layout/Sidebar'
|
||||
import { MobileNavbar } from '@/components/layout/MobileNavbar'
|
||||
import { metadata } from './metadata'
|
||||
// eslint-disable-next-line import/no-unassigned-import
|
||||
import './globals.css'
|
||||
|
||||
const spaceGrotesk = Space_Grotesk({
|
||||
|
|
@ -24,6 +26,7 @@ export default function RootLayout({
|
|||
/>
|
||||
<body className="min-h-full flex">
|
||||
<Sidebar />
|
||||
<MobileNavbar />
|
||||
{children}
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -1,14 +1,14 @@
|
|||
import { Metadata } from "next";
|
||||
import { Metadata } from 'next'
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Ephemeris",
|
||||
description: "A blog about me and my interests",
|
||||
creator: "hangerthem",
|
||||
title: 'Ephemeris',
|
||||
description: 'A blog about me and my interests',
|
||||
creator: 'hangerthem',
|
||||
openGraph: {
|
||||
type: "website",
|
||||
title: "Ephemeris",
|
||||
description: "A blog about me and my interests",
|
||||
type: 'website',
|
||||
title: 'Ephemeris',
|
||||
description: 'A blog about me and my interests',
|
||||
url: process.env.SITE_URL,
|
||||
siteName: "Ephemeris",
|
||||
siteName: 'Ephemeris',
|
||||
},
|
||||
}
|
||||
|
|
@ -47,7 +47,7 @@ export default async function Post({ params }: { params: Promise<{ slug: string
|
|||
{meta.category && (
|
||||
<Link
|
||||
href={`/categories/${slugify(meta.category)}`}
|
||||
className="text-accent hover:underline font-sm mb-2 block"
|
||||
className="text-accent hover:underline font-sm mb-2 inline-block"
|
||||
>
|
||||
{meta.category}
|
||||
</Link>
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ const noteTypeIcons: Record<NoteType, React.ReactNode> = {
|
|||
warning: <TriangleAlert className="w-4 h-4" />,
|
||||
tip: <Lightbulb className="w-4 h-4" />,
|
||||
info: <FileWarning className="w-4 h-4" />,
|
||||
quote: <Quote className="w-4 h-4" />
|
||||
quote: <Quote className="w-4 h-4" />,
|
||||
}
|
||||
|
||||
export function BlockquoteWrapper({ type, children }: BlockquoteWrapperProps) {
|
||||
|
|
|
|||
|
|
@ -27,7 +27,10 @@ export function CopyButton({ textToCopy, className, ...props }: CopyButtonProps)
|
|||
|
||||
return (
|
||||
<button
|
||||
className={cn('text-fg-dim hover:text-fg transition-colors js-only text-xs cursor-pointer disabled:cursor-default disabled:hover:text-fg-dim', className)}
|
||||
className={cn(
|
||||
'text-fg-dim hover:text-fg transition-colors js-only text-xs cursor-pointer disabled:cursor-default disabled:hover:text-fg-dim',
|
||||
className,
|
||||
)}
|
||||
onClick={handleCopy}
|
||||
ref={buttonRef}
|
||||
{...props}
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ export function ImageWrapper({ src, alt, isListing }: ImageWrapperProps) {
|
|||
alt={alt ?? ''}
|
||||
width={isListing ? 400 : 800}
|
||||
height={isListing ? 240 : 400}
|
||||
quality={100}
|
||||
quality={isListing ? 75 : 100}
|
||||
priority={true}
|
||||
className="w-auto mx-auto object-cover h-full"
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -1,14 +1,14 @@
|
|||
import { siBluesky, siX, siFacebook, siReddit } from 'simple-icons';
|
||||
import { ExternalLink } from '@/components/common/ExternalLink';
|
||||
import { siBluesky, siX, siFacebook, siReddit } from 'simple-icons'
|
||||
import { ExternalLink } from '@/components/common/ExternalLink'
|
||||
|
||||
type ShareLinksProps = {
|
||||
url: string;
|
||||
title: string;
|
||||
description?: string;
|
||||
};
|
||||
url: string
|
||||
title: string
|
||||
description?: string
|
||||
}
|
||||
|
||||
export function ShareLinks({ url, title, description }: ShareLinksProps) {
|
||||
const shareText = `Check out this post: ${title} - ${description}\n\n${url}`;
|
||||
const shareText = `Check out this post: ${title} - ${description}\n\n${url}`
|
||||
|
||||
const networks = [
|
||||
{
|
||||
|
|
@ -33,7 +33,7 @@ export function ShareLinks({ url, title, description }: ShareLinksProps) {
|
|||
title,
|
||||
)}&text=${encodeURIComponent(description ?? '')}`,
|
||||
},
|
||||
];
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="flex gap-4 mx-auto">
|
||||
|
|
@ -46,5 +46,5 @@ export function ShareLinks({ url, title, description }: ShareLinksProps) {
|
|||
</ExternalLink>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
|
|
|||
38
components/layout/MobileNavbar.tsx
Normal file
38
components/layout/MobileNavbar.tsx
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
'use client'
|
||||
|
||||
import { usePathname } from 'next/navigation'
|
||||
import Link from 'next/link'
|
||||
import { navItems } from './navItems'
|
||||
import { cn } from '@/utils/cn'
|
||||
|
||||
export function MobileNavbar() {
|
||||
const pathname = usePathname()
|
||||
|
||||
const isActive = (href: string) => {
|
||||
if (href === '/') {
|
||||
return pathname === href
|
||||
}
|
||||
return pathname?.startsWith(href)
|
||||
}
|
||||
|
||||
return (
|
||||
<nav className="lg:hidden fixed bottom-0 left-0 right-0 bg-bg border-t border-border z-50">
|
||||
<div className="flex justify-between items-center p-4 max-w-90 mx-auto">
|
||||
{navItems
|
||||
.filter((item) => !item.isExternal)
|
||||
.map((item) => (
|
||||
<Link
|
||||
key={item.href}
|
||||
href={item.href}
|
||||
className={cn(
|
||||
'transition-colors',
|
||||
isActive(item.href) ? 'text-accent' : 'text-fg-dim hover:text-fg',
|
||||
)}
|
||||
>
|
||||
{item.icon}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</nav>
|
||||
)
|
||||
}
|
||||
|
|
@ -1,36 +1,10 @@
|
|||
'use client'
|
||||
|
||||
import { cn } from '@/utils/cn'
|
||||
import { Folder, GitBranch, Home, Info, Rss, Tags } from 'lucide-react'
|
||||
import Link from 'next/link'
|
||||
import { usePathname } from 'next/navigation'
|
||||
import { ExternalLink } from '../common/ExternalLink'
|
||||
|
||||
type NavItem = {
|
||||
href: string
|
||||
label: string
|
||||
icon: React.ReactNode
|
||||
isExternal?: boolean
|
||||
}
|
||||
|
||||
const navItems: NavItem[] = [
|
||||
{ href: '/', label: 'Home', icon: <Home className="w-4 h-4" /> },
|
||||
{ href: '/categories', label: 'Categories', icon: <Folder className="w-4 h-4" /> },
|
||||
{ href: '/about', label: 'About', icon: <Info className="w-4 h-4" /> },
|
||||
{ href: '/tags', label: 'Tags', icon: <Tags className="w-4 h-4" /> },
|
||||
{
|
||||
href: '/feed.xml',
|
||||
label: 'RSS Feed',
|
||||
icon: <Rss className="w-4 h-4" />,
|
||||
isExternal: true,
|
||||
},
|
||||
{
|
||||
href: 'https://git.hangerthem.com/hangerthem/blog.hangerthem.com',
|
||||
label: 'Source Code',
|
||||
icon: <GitBranch className="w-4 h-4" />,
|
||||
isExternal: true,
|
||||
},
|
||||
]
|
||||
import { navItems } from './navItems'
|
||||
|
||||
export function NavLinks() {
|
||||
const pathname = usePathname()
|
||||
|
|
|
|||
29
components/layout/navItems.tsx
Normal file
29
components/layout/navItems.tsx
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
import { Folder, GitBranch, Home, Info, Rss, Tags } from 'lucide-react'
|
||||
|
||||
type NavItem = {
|
||||
href: string
|
||||
label: string
|
||||
icon: React.ReactNode
|
||||
isExternal?: boolean
|
||||
}
|
||||
|
||||
const navIconsClassName = 'w-6 h-6'
|
||||
|
||||
export const navItems: NavItem[] = [
|
||||
{ href: '/', label: 'Home', icon: <Home /> },
|
||||
{ href: '/about', label: 'About', icon: <Info className={navIconsClassName} /> },
|
||||
{ href: '/categories', label: 'Categories', icon: <Folder className={navIconsClassName} /> },
|
||||
{ href: '/tags', label: 'Tags', icon: <Tags className={navIconsClassName} /> },
|
||||
{
|
||||
href: '/feed.xml',
|
||||
label: 'RSS Feed',
|
||||
icon: <Rss className={navIconsClassName} />,
|
||||
isExternal: true,
|
||||
},
|
||||
{
|
||||
href: 'https://git.hangerthem.com/hangerthem/blog.hangerthem.com',
|
||||
label: 'Source Code',
|
||||
icon: <GitBranch className={navIconsClassName} />,
|
||||
isExternal: true,
|
||||
},
|
||||
]
|
||||
|
|
@ -10,6 +10,7 @@ mermaid: true
|
|||
---
|
||||
|
||||
> [!QUOTE] "The only person you are destined to become is the person you decide to be."
|
||||
>
|
||||
> - Ralph Waldo Emerson
|
||||
|
||||
Right at the beginning of this post, I want to make it clear that I am not a self-help guru or a life coach. I am just a regular person who is passionate about personal development and enjoys exploring ways to improve myself. In this post, I want to share some thoughts on self-development and how gamification can be used to enhance productivity and personal growth.
|
||||
|
|
|
|||
|
|
@ -255,7 +255,6 @@ $$
|
|||
\label{eq:em_field_strength_tensor}
|
||||
\end{equation}
|
||||
$$
|
||||
|
||||
- **Explanation**: This term describes the field strength of the electromagnetic field. It involves the derivative of the electromagnetic potential $A_\mu$. The field strength tensor is antisymmetric, reflecting the nature of the electromagnetic force.
|
||||
|
||||
2. **Electron Kinetic Term** $(\bar{\psi}(i\gamma^\mu D_\mu-m)\psi)$:
|
||||
|
|
@ -270,7 +269,6 @@ $$
|
|||
\label{eq:covariant_derivative_for_electrons}
|
||||
\end{equation}
|
||||
$$
|
||||
|
||||
- **Explanation**: This accounts for the interaction of electrons with the electromagnetic field. $e$ is the electric charge, and $A_\mu$ is the electromagnetic potential.
|
||||
|
||||
3. **Electron Mass Term** $(-m)$:
|
||||
|
|
@ -307,7 +305,6 @@ $$
|
|||
\label{eq:w_field_strength_tensor}
|
||||
\end{equation}
|
||||
$$
|
||||
|
||||
- **$B$ Field Strength Tensor** ($B_{\mu\nu}$):
|
||||
|
||||
$$
|
||||
|
|
@ -316,7 +313,6 @@ $$
|
|||
\label{eq:b_field_strength_tensor}
|
||||
\end{equation}
|
||||
$$
|
||||
|
||||
- **Explanation**: These terms describe the field strengths of the $W$ and $B$ fields, which are associated with the weak and electromagnetic forces, respectively. The $W$ field strength tensor involves the non-Abelian field strength term, reflecting the non-Abelian nature of the weak force.
|
||||
|
||||
2. **Lepton/Quark Kinetic Term** $(\bar{\psi}(i\gamma^\mu D_\mu-m)\psi)$:
|
||||
|
|
@ -331,7 +327,6 @@ $$
|
|||
\label{eq:covariant_derivative_for_leptons_quarks}
|
||||
\end{equation}
|
||||
$$
|
||||
|
||||
- **Explanation**: This accounts for the interaction of leptons/quarks with the $W$ and $B$ fields. $g'$ and $g$ are the coupling constants for the $U(1)$ and $SU(2)$ gauge groups, respectively. $Y$ is the weak hypercharge, and $\frac{\tau^a}{2}$ are the generators of the $SU(2)$ group.
|
||||
|
||||
3. **Lepton/Quark Mass Term** $(-m)$:
|
||||
|
|
@ -366,7 +361,6 @@ $$
|
|||
\label{eq:gluon_field_strength_tensor}
|
||||
\end{equation}
|
||||
$$
|
||||
|
||||
- **Explanation**: This term describes the field strength of gluons. It involves the derivative of the gluon fields $(\partial_\mu G^a_\nu$ and $\partial_\nu G^a_\mu)$ and the non-Abelian field strength term involving the structure constants $f^{abc}$, which accounts for the interaction between gluons themselves.
|
||||
|
||||
2. **Quark Kinetic Term** $(\bar{q}(i\gamma^\mu D_\mu-m_q)q)$:
|
||||
|
|
@ -381,7 +375,6 @@ $$
|
|||
\label{eq:quark_covariant_derivative_qcd}
|
||||
\end{equation}
|
||||
$$
|
||||
|
||||
- **Explanation**: This accounts for the interaction of quarks with gluons. $g_s$ is the strong coupling constant, and $\frac{\lambda^a}{2}$ are the generators of the SU(3) color gauge group. $G^a_\mu$ are the gluon fields.
|
||||
|
||||
3. **Quark Mass Term** $(-m_q)$:
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ You can also use other code blocks in your Markdown, like this:
|
|||
|
||||
```ts
|
||||
function helloWorld() {
|
||||
console.log("Hello, world!");
|
||||
console.log('Hello, world!')
|
||||
}
|
||||
```
|
||||
|
||||
|
|
@ -54,7 +54,7 @@ It will be rendered as:
|
|||
> [!TIP] You can also use `>` to add blockquotes to your posts.
|
||||
|
||||
| Column 1 | Column 2 | Column 3 |
|
||||
|----------|----------|----------|
|
||||
| -------- | -------- | -------- |
|
||||
| Cell 1 | Cell 2 | Cell 3 |
|
||||
| Cell 4 | Cell 5 | Cell 6 |
|
||||
|
||||
|
|
@ -91,10 +91,9 @@ Term 2
|
|||
: Definition A for term 2.
|
||||
: Definition B for term 2, on a new line.
|
||||
|
||||
H~2~O
|
||||
|
||||
- [ ] Task 1
|
||||
- [x] Task 2
|
||||
|
||||
[^1]: The `src/assets/blog` ==directory== is where you can place images for your blog posts. You can reference them in your Markdown using the relative path to the image file.
|
||||
|
||||
[^2]: You can also add footnotes to your posts. This is a footnote reference, and the footnote text is at the bottom of the post.
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { defineConfig, globalIgnores } from "eslint/config";
|
||||
import nextVitals from "eslint-config-next/core-web-vitals";
|
||||
import nextTs from "eslint-config-next/typescript";
|
||||
import { defineConfig, globalIgnores } from 'eslint/config'
|
||||
import nextVitals from 'eslint-config-next/core-web-vitals'
|
||||
import nextTs from 'eslint-config-next/typescript'
|
||||
|
||||
const eslintConfig = defineConfig([
|
||||
...nextVitals,
|
||||
|
|
@ -8,11 +8,11 @@ const eslintConfig = defineConfig([
|
|||
// Override default ignores of eslint-config-next.
|
||||
globalIgnores([
|
||||
// Default ignores of eslint-config-next:
|
||||
".next/**",
|
||||
"out/**",
|
||||
"build/**",
|
||||
"next-env.d.ts",
|
||||
'.next/**',
|
||||
'out/**',
|
||||
'build/**',
|
||||
'next-env.d.ts',
|
||||
]),
|
||||
]);
|
||||
])
|
||||
|
||||
export default eslintConfig;
|
||||
export default eslintConfig
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { findSlugInArray } from "@/utils/slug"
|
||||
import { getAllPostsMeta } from "@/lib/posts"
|
||||
import { findSlugInArray } from '@/utils/slug'
|
||||
import { getAllPostsMeta } from '@/lib/posts'
|
||||
|
||||
/**
|
||||
* Retrieves all unique categories from the posts and counts the number of posts in each category.
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import path from 'path'
|
|||
import matter from 'gray-matter'
|
||||
import { remark } from 'remark'
|
||||
import remarkParse from 'remark-parse'
|
||||
import gfm from 'remark-gfm'
|
||||
import remarkGfm from 'remark-gfm'
|
||||
import remarkMath from 'remark-math'
|
||||
import remarkRehype from 'remark-rehype'
|
||||
import rehypeHighlight from 'rehype-highlight'
|
||||
|
|
@ -66,7 +66,7 @@ export async function getPostBySlug(slug: string) {
|
|||
const fullPath = path.join(postsDirectory, `${slug}.md`)
|
||||
const { data, content } = matter(fs.readFileSync(fullPath, 'utf8'))
|
||||
|
||||
let processor = remark().use(remarkParse).use(gfm)
|
||||
let processor = remark().use(remarkParse).use(remarkGfm)
|
||||
|
||||
if (data.math) {
|
||||
processor = processor.use(remarkMath)
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { cloneElement, isValidElement, type ReactNode } from "react";
|
||||
import { cloneElement, isValidElement, type ReactNode } from 'react'
|
||||
|
||||
/**
|
||||
* Recursively traverses a ReactNode tree and extracts the text content.
|
||||
|
|
@ -6,14 +6,14 @@ import { cloneElement, isValidElement, type ReactNode } from "react";
|
|||
* @returns A string containing the concatenated text content of the ReactNode tree.
|
||||
*/
|
||||
export function getTextContent(node: ReactNode): string {
|
||||
if (node === null || typeof node === "boolean") return "";
|
||||
if (typeof node === "string" || typeof node === "number") return String(node);
|
||||
if (Array.isArray(node)) return node.map(getTextContent).join("");
|
||||
if (node === null || typeof node === 'boolean') return ''
|
||||
if (typeof node === 'string' || typeof node === 'number') return String(node)
|
||||
if (Array.isArray(node)) return node.map(getTextContent).join('')
|
||||
if (isValidElement(node)) {
|
||||
const props = node.props as { children?: ReactNode };
|
||||
return getTextContent(props.children);
|
||||
const props = node.props as { children?: ReactNode }
|
||||
return getTextContent(props.children)
|
||||
}
|
||||
return "";
|
||||
return ''
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -23,36 +23,36 @@ export function getTextContent(node: ReactNode): string {
|
|||
* @returns A new ReactNode with the leading whitespace and specified pattern removed from text nodes.
|
||||
*/
|
||||
export function stripLeadingMatch(node: ReactNode, pattern: RegExp): ReactNode {
|
||||
if (typeof node === "string") {
|
||||
const trimmed = node.replace(/^\s+/, "");
|
||||
if (typeof node === 'string') {
|
||||
const trimmed = node.replace(/^\s+/, '')
|
||||
if (pattern.test(trimmed)) {
|
||||
return trimmed.replace(pattern, "").replace(/^\s+/, "");
|
||||
return trimmed.replace(pattern, '').replace(/^\s+/, '')
|
||||
}
|
||||
return node;
|
||||
return node
|
||||
}
|
||||
if (Array.isArray(node)) {
|
||||
let stripped = false;
|
||||
let stripped = false
|
||||
return node.map((child) => {
|
||||
if (stripped) return child;
|
||||
if (typeof child === "string" && child.trim() === "") return child; // skip whitespace nodes
|
||||
if (typeof child === "string") {
|
||||
const trimmed = child.replace(/^\s+/, "");
|
||||
if (stripped) return child
|
||||
if (typeof child === 'string' && child.trim() === '') return child // skip whitespace nodes
|
||||
if (typeof child === 'string') {
|
||||
const trimmed = child.replace(/^\s+/, '')
|
||||
if (pattern.test(trimmed)) {
|
||||
stripped = true;
|
||||
return trimmed.replace(pattern, "").replace(/^\s+/, "");
|
||||
stripped = true
|
||||
return trimmed.replace(pattern, '').replace(/^\s+/, '')
|
||||
}
|
||||
return child;
|
||||
return child
|
||||
}
|
||||
if (isValidElement(child)) {
|
||||
stripped = true;
|
||||
return stripLeadingMatch(child, pattern);
|
||||
stripped = true
|
||||
return stripLeadingMatch(child, pattern)
|
||||
}
|
||||
return child;
|
||||
});
|
||||
return child
|
||||
})
|
||||
}
|
||||
if (isValidElement(node)) {
|
||||
const props = node.props as { children?: ReactNode };
|
||||
return cloneElement(node, {}, stripLeadingMatch(props.children, pattern));
|
||||
const props = node.props as { children?: ReactNode }
|
||||
return cloneElement(node, {}, stripLeadingMatch(props.children, pattern))
|
||||
}
|
||||
return node;
|
||||
return node
|
||||
}
|
||||
|
|
@ -12,7 +12,12 @@ export const rehypeMermaidSsg: Plugin<[], Root> = () => async (tree: Root) => {
|
|||
const targets: { node: Element; parent: Element | Root; index: number }[] = []
|
||||
|
||||
visit(tree, 'element', (node: Element, index, parent) => {
|
||||
if (node.tagName === 'div' && node.properties?.['data-mermaid'] && parent && typeof index === 'number') {
|
||||
if (
|
||||
node.tagName === 'div' &&
|
||||
node.properties?.['data-mermaid'] &&
|
||||
parent &&
|
||||
typeof index === 'number'
|
||||
) {
|
||||
targets.push({ node, parent: parent as Element | Root, index })
|
||||
}
|
||||
})
|
||||
|
|
@ -25,14 +30,12 @@ export const rehypeMermaidSsg: Plugin<[], Root> = () => async (tree: Root) => {
|
|||
const svgTree = fromHtmlIsomorphic(svg, { fragment: true })
|
||||
const svgNode = svgTree.children.find((c) => c.type === 'element') as Element | undefined
|
||||
if (svgNode) {
|
||||
; (parent.children as Element[])[index] = svgNode
|
||||
;(parent.children as Element[])[index] = svgNode
|
||||
}
|
||||
} catch (err) {
|
||||
node.tagName = 'pre'
|
||||
node.properties = { className: ['text-red-500', 'text-sm'] }
|
||||
node.children = [
|
||||
{ type: 'text', value: `Mermaid render error: ${(err as Error).message}` },
|
||||
]
|
||||
node.children = [{ type: 'text', value: `Mermaid render error: ${(err as Error).message}` }]
|
||||
}
|
||||
}),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
import type { NextConfig } from "next";
|
||||
import type { NextConfig } from 'next'
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
/* config options here */
|
||||
};
|
||||
images: {
|
||||
qualities: [75, 100],
|
||||
},
|
||||
}
|
||||
|
||||
export default nextConfig;
|
||||
export default nextConfig
|
||||
|
|
|
|||
|
|
@ -9,8 +9,7 @@
|
|||
"lint": "oxlint . && oxfmt --check",
|
||||
"lint:fix": "oxlint . --fix && oxfmt --write",
|
||||
"format": "oxfmt --check",
|
||||
"format:write": "oxfmt --write",
|
||||
"postinstall": "puppeteer browsers install chrome"
|
||||
"format:write": "oxfmt --write"
|
||||
},
|
||||
"dependencies": {
|
||||
"clsx": "^2.1.1",
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
const config = {
|
||||
plugins: {
|
||||
"@tailwindcss/postcss": {},
|
||||
'@tailwindcss/postcss': {},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export default config;
|
||||
export default config
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
import { clsx, type ClassValue } from 'clsx';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
import { clsx, type ClassValue } from 'clsx'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
|
||||
/**
|
||||
* Utility function to merge Tailwind CSS classes
|
||||
* Combines clsx for conditional classes with tailwind-merge for deduplication
|
||||
*/
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs));
|
||||
return twMerge(clsx(inputs))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,4 +5,4 @@ export const dateFormatter = new Intl.DateTimeFormat('en', {
|
|||
day: 'numeric',
|
||||
month: 'long',
|
||||
year: 'numeric',
|
||||
});
|
||||
})
|
||||
|
|
|
|||
Loading…
Reference in a new issue