feat: add custom body link component

This commit is contained in:
Thomas Bishop 2025-09-26 14:33:07 +01:00
parent f22f380b96
commit 47abc44506
8 changed files with 314 additions and 247 deletions

42
package-lock.json generated
View file

@ -10,7 +10,7 @@
"dependencies": {
"@radix-ui/react-collapsible": "^1.1.7",
"@radix-ui/react-dialog": "^1.1.10",
"@radix-ui/react-hover-card": "^1.1.14",
"@radix-ui/react-hover-card": "^1.1.15",
"@radix-ui/react-label": "^2.1.7",
"@radix-ui/react-separator": "^1.1.4",
"@radix-ui/react-slot": "^1.2.3",
@ -1300,18 +1300,18 @@
}
},
"node_modules/@radix-ui/react-hover-card": {
"version": "1.1.14",
"resolved": "https://registry.npmjs.org/@radix-ui/react-hover-card/-/react-hover-card-1.1.14.tgz",
"integrity": "sha512-CPYZ24Mhirm+g6D8jArmLzjYu4Eyg3TTUHswR26QgzXBHBe64BO/RHOJKzmF/Dxb4y4f9PKyJdwm/O/AhNkb+Q==",
"version": "1.1.15",
"resolved": "https://registry.npmjs.org/@radix-ui/react-hover-card/-/react-hover-card-1.1.15.tgz",
"integrity": "sha512-qgTkjNT1CfKMoP0rcasmlH2r1DAiYicWsDsufxl940sT2wHNEWWv6FMWIQXWhVdmC1d/HYfbhQx60KYyAtKxjg==",
"license": "MIT",
"dependencies": {
"@radix-ui/primitive": "1.1.2",
"@radix-ui/primitive": "1.1.3",
"@radix-ui/react-compose-refs": "1.1.2",
"@radix-ui/react-context": "1.1.2",
"@radix-ui/react-dismissable-layer": "1.1.10",
"@radix-ui/react-popper": "1.2.7",
"@radix-ui/react-dismissable-layer": "1.1.11",
"@radix-ui/react-popper": "1.2.8",
"@radix-ui/react-portal": "1.1.9",
"@radix-ui/react-presence": "1.1.4",
"@radix-ui/react-presence": "1.1.5",
"@radix-ui/react-primitive": "2.1.3",
"@radix-ui/react-use-controllable-state": "1.2.2"
},
@ -1330,6 +1330,12 @@
}
}
},
"node_modules/@radix-ui/react-hover-card/node_modules/@radix-ui/primitive": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz",
"integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==",
"license": "MIT"
},
"node_modules/@radix-ui/react-hover-card/node_modules/@radix-ui/react-arrow": {
"version": "1.1.7",
"resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz",
@ -1354,12 +1360,12 @@
}
},
"node_modules/@radix-ui/react-hover-card/node_modules/@radix-ui/react-dismissable-layer": {
"version": "1.1.10",
"resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.10.tgz",
"integrity": "sha512-IM1zzRV4W3HtVgftdQiiOmA0AdJlCtMLe00FXaHwgt3rAnNsIyDqshvkIW3hj/iu5hu8ERP7KIYki6NkqDxAwQ==",
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.11.tgz",
"integrity": "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==",
"license": "MIT",
"dependencies": {
"@radix-ui/primitive": "1.1.2",
"@radix-ui/primitive": "1.1.3",
"@radix-ui/react-compose-refs": "1.1.2",
"@radix-ui/react-primitive": "2.1.3",
"@radix-ui/react-use-callback-ref": "1.1.1",
@ -1381,9 +1387,9 @@
}
},
"node_modules/@radix-ui/react-hover-card/node_modules/@radix-ui/react-popper": {
"version": "1.2.7",
"resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.7.tgz",
"integrity": "sha512-IUFAccz1JyKcf/RjB552PlWwxjeCJB8/4KxT7EhBHOJM+mN7LdW+B3kacJXILm32xawcMMjb2i0cIZpo+f9kiQ==",
"version": "1.2.8",
"resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.8.tgz",
"integrity": "sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw==",
"license": "MIT",
"dependencies": {
"@floating-ui/react-dom": "^2.0.0",
@ -1437,9 +1443,9 @@
}
},
"node_modules/@radix-ui/react-hover-card/node_modules/@radix-ui/react-presence": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.4.tgz",
"integrity": "sha512-ueDqRbdc4/bkaQT3GIpLQssRlFgWaL/U2z/S31qRwwLWoxHLgry3SIfCwhxeQNbirEUXFa+lq3RL3oBYXtcmIA==",
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz",
"integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-compose-refs": "1.1.2",

View file

@ -12,7 +12,7 @@
"dependencies": {
"@radix-ui/react-collapsible": "^1.1.7",
"@radix-ui/react-dialog": "^1.1.10",
"@radix-ui/react-hover-card": "^1.1.14",
"@radix-ui/react-hover-card": "^1.1.15",
"@radix-ui/react-label": "^2.1.7",
"@radix-ui/react-separator": "^1.1.4",
"@radix-ui/react-slot": "^1.2.3",

View file

@ -1,25 +1,34 @@
@import url("https://fonts.googleapis.com/css2?family=JetBrains+Mono:ital,wght@0,100..800;1,100..800&display=swap");
code {
font-family: "JetBrains Mono";
font-family: "JetBrains Mono";
}
pre > code {
padding: 0.5rem;
font-size: 14px;
pre>code {
padding: 0.5rem;
font-size: 14px;
}
h2 > code {
font-size: 1rem;
h2>code {
font-size: 1rem;
}
.btn[data-state="active"] {
box-shadow: none !important;
box-shadow: none !important;
}
button[data-state="active"] {
--tw-shadow: none;
--tw-shadow-colored: none;
box-shadow:
var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
--tw-shadow: none;
--tw-shadow-colored: none;
box-shadow:
var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
}
/* .HoverCardContent { */
/* width: 300px; */
/* max-height: 500px; */
/* } */
/* .HoverCardContent { */
/* transform-origin: 10px; */
/* } */

View file

@ -0,0 +1,50 @@
import { useEffect, useState } from "react"
import { useQueryClient } from "@tanstack/react-query"
import api from "@/api/eolas-api"
export default function BodyLink({ link, children }) {
const [entryExists, setEntryExists] = useState(false)
const path = link.split("/").pop().split(".")[0]
const queryClient = useQueryClient()
useEffect(() => {
const fetchEntryPreview = async () => {
const cachedEntry = queryClient.getQueryData([`entry_${path}`])
if (cachedEntry) {
setEntryExists(true)
console.info("INFO: Entry exists in cache.")
} else {
try {
const remoteEntry = await queryClient.fetchQuery({
queryKey: [`entry_${path}`],
queryFn: () => api.get(`/entries/${path}`).then((res) => res.data),
})
setEntryExists(true)
console.info("INFO: Entry exists on remote.")
} catch (error) {
console.log(`INFO: Could not fetch entry ${path} ${error}`)
setEntryExists(false)
}
}
}
fetchEntryPreview()
}, [path, queryClient])
if (entryExists) {
return (
<a
className="text-foreground underline-offset-4 underline hover:text-gray-700 dark:hover:text-green-300"
href={`/entries/${path}`}
>
{children}
</a>
)
} else
return (
<a className="text-red-500 line-through" href={`/entries/${path}`}>
{children}
</a>
)
}

View file

@ -10,68 +10,68 @@ import { Link } from "react-router"
import { useState, useRef, useEffect } from "react"
export default function EntriesListSidebar() {
const scrollRef = useRef(null)
const [isOpen, setIsOpen] = useState(() => {
if (typeof window !== "undefined") {
const saved = sessionStorage.getItem("entries_list_sidebar_open")
return saved ? JSON.parse(saved) : false
}
return false
})
const scrollRef = useRef(null)
const [isOpen, setIsOpen] = useState(() => {
if (typeof window !== "undefined") {
const saved = sessionStorage.getItem("entries_list_sidebar_open")
return saved ? JSON.parse(saved) : false
}
return false
})
const { data: entries, isLoading } = useQuery({
queryKey: ["entries_list"],
queryFn: () => api.get("/entries").then((res) => res.data),
})
const { data: entries, isLoading } = useQuery({
queryKey: ["entries_list"],
queryFn: () => api.get("/entries").then((res) => res.data),
})
useEffect(() => {
sessionStorage.setItem("entries_list_sidebar_open", JSON.stringify(isOpen))
}, [isOpen])
useEffect(() => {
sessionStorage.setItem("entries_list_sidebar_open", JSON.stringify(isOpen))
}, [isOpen])
useEffect(() => {
const savedScroll = sessionStorage.getItem("entries_list_sidebar_scroll_position")
if (savedScroll && scrollRef.current) {
scrollRef.current.scrollTop = parseInt(savedScroll)
}
}, [entries])
useEffect(() => {
const savedScroll = sessionStorage.getItem("entries_list_sidebar_scroll_position")
if (savedScroll && scrollRef.current) {
scrollRef.current.scrollTop = parseInt(savedScroll)
}
}, [entries])
const handleScroll = () => {
if (scrollRef.current) {
sessionStorage.setItem("entries_list_sidebar_scroll_position", scrollRef.current.scrollTop)
}
}
const handleScroll = () => {
if (scrollRef.current) {
sessionStorage.setItem("entries_list_sidebar_scroll_position", scrollRef.current.scrollTop)
}
}
return (
<Collapsible open={isOpen} onOpenChange={setIsOpen} className="group/collapsible">
<SidebarMenuItem key="entries">
<CollapsibleTrigger asChild className="rounded-none">
<SidebarMenuButton asChild className="rounded-none">
<a href="#">
<FileText />
<span>Entries</span>
<Badge className="ml-0" variant="secondary">
{entries?.count}
</Badge>
<ChevronRight className="ml-auto transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90" />
</a>
</SidebarMenuButton>
</CollapsibleTrigger>
<CollapsibleContent>
<div ref={scrollRef} onScroll={handleScroll} className="max-h-100 overflow-y-auto">
<SidebarMenuSub>
{entries?.data.map((item, i) => (
<SidebarMenuItem key={i}>
<Link to={`/entries/${item.title}`}>
<span className="text-xs text-foreground dark:hover:text-green-300 dark:active:text-green-400 hover:text-gray-600 active:text-gray-700 focus:text-gray-900 dark:focus:text-green-900">
{item.title.replace(/_/g, " ")}
</span>
</Link>
</SidebarMenuItem>
))}
</SidebarMenuSub>
</div>
</CollapsibleContent>
</SidebarMenuItem>
</Collapsible>
)
return (
<Collapsible open={isOpen} onOpenChange={setIsOpen} className="group/collapsible">
<SidebarMenuItem key="entries">
<CollapsibleTrigger asChild className="rounded-none">
<SidebarMenuButton asChild className="rounded-none">
<a href="#">
<FileText />
<span>Entries</span>
<Badge className="ml-0" variant="secondary">
{entries?.count}
</Badge>
<ChevronRight className="ml-auto transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90" />
</a>
</SidebarMenuButton>
</CollapsibleTrigger>
<CollapsibleContent>
<div ref={scrollRef} onScroll={handleScroll} className="max-h-100 overflow-y-auto">
<SidebarMenuSub>
{entries?.data.map((item, i) => (
<SidebarMenuItem key={i}>
<Link to={`/entries/${item.title}`}>
<span className="text-xs text-foreground dark:hover:text-green-300 dark:active:text-green-400 hover:text-gray-600 active:text-gray-700 focus:text-gray-900 dark:focus:text-green-900">
{item.title.replace(/_/g, " ")}
</span>
</Link>
</SidebarMenuItem>
))}
</SidebarMenuSub>
</div>
</CollapsibleContent>
</SidebarMenuItem>
</Collapsible>
)
}

View file

@ -5,100 +5,99 @@ import remarkMath from "remark-math"
import rehypeKatex from "rehype-katex"
import "katex/dist/katex.min.css"
import { Skeleton } from "@/components/ui/skeleton"
import BodyLink from "./BodyLink"
const EntryLoadingSkeleton = () => {
return (
<div className="space-y-2 max-w-2xl p-4 lg:p-6">
{/*
return (
<div className="space-y-2 max-w-2xl p-4 lg:p-6">
{/*
<Skeleton className="h-[400px] md:h-[800px] max-w-2xl rounded-none" />
*/}
<Skeleton className="h-4 max-w-full" />
<Skeleton className="h-4 md:max-w-xl max-w-[300px]" />
<Skeleton className="h-4 md:max-w-[400px] max-w-[250px]" />
<Skeleton className="h-4 md:max-w-[300px] max-w-[200px]" />
<Skeleton className="h-4 md:max-w-[200px] max-w-[150px]" />
<Skeleton className="h-4 md:max-w-[100px] max-w-[100px]" />
</div>
)
<Skeleton className="h-4 max-w-full" />
<Skeleton className="h-4 md:max-w-xl max-w-[300px]" />
<Skeleton className="h-4 md:max-w-[400px] max-w-[250px]" />
<Skeleton className="h-4 md:max-w-[300px] max-w-[200px]" />
<Skeleton className="h-4 md:max-w-[200px] max-w-[150px]" />
<Skeleton className="h-4 md:max-w-[100px] max-w-[100px]" />
</div>
)
}
const ImagePreprocessor = (src) => {
const filename = src.src.split("/").pop()
const s3RootUrl = "https://eolas.s3.systemsobscure.net/"
return <img src={s3RootUrl + filename} />
const filename = src.src.split("/").pop()
const s3RootUrl = "https://eolas.s3.systemsobscure.net/"
return <img src={s3RootUrl + filename} />
}
export default function EntryBody({ body, isLoading }) {
if (isLoading) {
return <EntryLoadingSkeleton />
} else
return (
<div className="max-w-2xl p-4 lg:p-6">
<ReactMarkdown
remarkPlugins={[remarkGfm, remarkMath]}
rehypePlugins={[rehypeKatex]}
components={{
h1: () => null,
h2: ({ children }) => (
<h2 className="scroll-m-20 font-semibold mt-8 mb-4 first:mt-0">{children}</h2>
),
h3: ({ children }) => (
<h3 className="scroll-m-20 font-semibold mt-8 mb-4 first:mt-0">{children}</h3>
),
h4: ({ children }) => (
<h4 className="scroll-m-20 font-semibold mt-8 mb-4 first:mt-0">{children}</h4>
),
p: ({ children }) => (
<p className="leading-[1.5] mb-4 not-first:mt-4">{children}</p>
),
ul: ({ children }) => (
<ul className="list-disc ml-10 mb-4 space-y-1">{children}</ul>
),
ol: ({ children }) => (
<ol className="list-decimal ml-10 mb-4 space-y-1">{children}</ol>
),
a: ({ href, children }) => (
<a href={href} target="_blank" rel="noopener noreferrer">
{children}
</a>
),
table: ({ children }) => (
<table className="w-full mb-4 text-sm">{children}</table>
),
tr: ({ children }) => (
<tr className="even:bg-muted m-0 border-t p-0">{children}</tr>
),
th: ({ children }) => (
<th className="border px-4 py-2 text-left font-bold [&[align=center]]:text-center [&[align=right]]:text-right">
{children}
</th>
),
td: ({ children }) => (
<td className="border px-4 py-2 text-left [&[align=center]]:text-center [&[align=right]]:text-right">
{children}
</td>
),
blockquote: ({ children }) => (
<blockquote className="mt-4 border-l-2 pl-6 text-muted-foreground">
{children}
</blockquote>
),
pre: ({ children }) => {
const child = children.props
return <CodeBlock className={child.className}>{child.children}</CodeBlock>
},
code: ({ children }) => (
<code className="rounded bg-muted px-[0.3rem] py-[0.2rem] font-mono text-sm">
{children}
</code>
),
img: ({ children, src }) => <ImagePreprocessor src={src} />,
}}
>
{body}
</ReactMarkdown>
</div>
)
if (isLoading) {
return <EntryLoadingSkeleton />
} else
return (
<div className="max-w-2xl p-4 lg:p-6">
<ReactMarkdown
remarkPlugins={[remarkGfm, remarkMath]}
rehypePlugins={[rehypeKatex]}
components={{
h1: () => null,
h2: ({ children }) => (
<h2 className="scroll-m-20 font-semibold mt-8 mb-4 first:mt-0">{children}</h2>
),
h3: ({ children }) => (
<h3 className="scroll-m-20 font-semibold mt-8 mb-4 first:mt-0">{children}</h3>
),
h4: ({ children }) => (
<h4 className="scroll-m-20 font-semibold mt-8 mb-4 first:mt-0">{children}</h4>
),
p: ({ children }) => (
<p className="leading-[1.5] mb-4 not-first:mt-4">{children}</p>
),
ul: ({ children }) => (
<ul className="list-disc ml-10 mb-4 space-y-1">{children}</ul>
),
ol: ({ children }) => (
<ol className="list-decimal ml-10 mb-4 space-y-1">{children}</ol>
),
table: ({ children }) => (
<table className="w-full mb-4 text-sm">{children}</table>
),
tr: ({ children }) => (
<tr className="even:bg-muted m-0 border-t p-0">{children}</tr>
),
th: ({ children }) => (
<th className="border px-4 py-2 text-left font-bold [&[align=center]]:text-center [&[align=right]]:text-right">
{children}
</th>
),
td: ({ children }) => (
<td className="border px-4 py-2 text-left [&[align=center]]:text-center [&[align=right]]:text-right">
{children}
</td>
),
blockquote: ({ children }) => (
<blockquote className="mt-4 border-l-2 pl-6 text-muted-foreground">
{children}
</blockquote>
),
pre: ({ children }) => {
const child = children.props
return <CodeBlock className={child.className}>{child.children}</CodeBlock>
},
code: ({ children }) => (
<code className="rounded bg-muted px-[0.3rem] py-[0.2rem] font-mono text-sm">
{children}
</code>
),
img: ({ src }) => <ImagePreprocessor src={src} />,
a: ({ href, children }) => {
return <BodyLink link={href} children={children} />
},
}}
>
{body}
</ReactMarkdown>
</div>
)
}

View file

@ -4,85 +4,84 @@ import { Link } from "react-router"
import { Badge } from "./ui/badge"
export default function EntryReferences({ entryTitle }) {
const { data: tags, isLoading: tagsLoading } = useQuery({
queryKey: [`tags_for_${entryTitle}`],
queryFn: () => api.get(`/tags/${entryTitle}`).then((res) => res.data),
})
const { data: tags, isLoading: tagsLoading } = useQuery({
queryKey: [`tags_for_${entryTitle}`],
queryFn: () => api.get(`/tags/${entryTitle}`).then((res) => res.data),
})
const { data: backlinks, isLoading: backlinksLoading } = useQuery({
queryKey: [`backlinks_for_${entryTitle}`],
queryFn: () => api.get(`/entries/backlinks/${entryTitle}`).then((res) => res.data),
})
const { data: backlinks, isLoading: backlinksLoading } = useQuery({
queryKey: [`backlinks_for_${entryTitle}`],
queryFn: () => api.get(`/entries/backlinks/${entryTitle}`).then((res) => res.data),
})
const { data: outlinks, isLoading: outlinksLoading } = useQuery({
queryKey: [`outlinks_for_${entryTitle}`],
queryFn: () => api.get(`/entries/outlinks/${entryTitle}`).then((res) => res.data),
})
const { data: outlinks, isLoading: outlinksLoading } = useQuery({
queryKey: [`outlinks_for_${entryTitle}`],
queryFn: () => api.get(`/entries/outlinks/${entryTitle}`).then((res) => res.data),
})
console.log(backlinks)
return (
<div className="w-full flex flex-row justify-between gap-3">
<div className="w-full">
<div className="flex flex row justify-between bg-sidebar">
<h3 className="font-medium text-sm p-1 ml-1">Tags</h3>
<Badge variant="secondary" className="rounded-none">
{tags?.count}
</Badge>
</div>
<div className="mt-2 mb-3 pl-1">
{tags?.data.map((item, i) => (
<Link
key={i}
to={`/tags/${item}`}
className="text-foreground underline-offset-3 text-sm underline hover:text-gray-700 dark:hover:text-green-300 pr-2"
>
{item}
</Link>
))}
</div>
return (
<div className="w-full flex flex-row justify-between gap-3">
<div className="w-full">
<div className="flex flex row justify-between bg-sidebar">
<h3 className="font-medium text-sm p-1 ml-1">Tags</h3>
<Badge variant="secondary" className="rounded-none">
{tags?.count}
</Badge>
</div>
<div className="mt-2 mb-3 pl-1">
{tags?.data.map((item, i) => (
<Link
key={i}
to={`/tags/${item}`}
className="text-foreground underline-offset-3 text-sm underline hover:text-gray-700 dark:hover:text-green-300 pr-2"
>
{item}
</Link>
))}
</div>
<div className="flex flex row justify-between bg-sidebar">
<h3 className="font-medium text-sm p-1 ml-1">Incoming links</h3>
<Badge variant="secondary" className="rounded-none">
{backlinks?.count}
</Badge>
</div>
<div className="flex flex row justify-between bg-sidebar">
<h3 className="font-medium text-sm p-1 ml-1">Incoming links</h3>
<Badge variant="secondary" className="rounded-none">
{backlinks?.count}
</Badge>
</div>
<div className="mt-2 mb-3 pl-1">
{backlinks &&
backlinks?.data.map((item, i) => (
<Link
key={i}
to={`/entries/${item}`}
className="text-foreground underline-offset-3 text-sm underline hover:text-gray-700 dark:hover:text-green-300 pr-2 block mb-2"
>
{item.replace(/_/g, " ")}
</Link>
))}
</div>
</div>
<div className="mt-2 mb-3 pl-1">
{backlinks &&
backlinks?.data.map((item, i) => (
<Link
key={i}
to={`/entries/${item}`}
className="text-foreground underline-offset-3 text-sm underline hover:text-gray-700 dark:hover:text-green-300 pr-2 block mb-2"
>
{item.replace(/_/g, " ")}
</Link>
))}
</div>
</div>
<div className="w-full">
<div className="flex flex row justify-between bg-sidebar">
<h3 className="font-medium text-sm p-1 ml-1">Outgoing links</h3>
<Badge variant="secondary" className="rounded-none">
{outlinks?.count}
</Badge>
</div>
<div className="w-full">
<div className="flex flex row justify-between bg-sidebar">
<h3 className="font-medium text-sm p-1 ml-1">Outgoing links</h3>
<Badge variant="secondary" className="rounded-none">
{outlinks?.count}
</Badge>
</div>
<div className="mt-2 mb-3 pl-1">
{outlinks &&
outlinks?.data.map((item, i) => (
<Link
key={i}
to={`/entries/${item}`}
className="text-foreground underline-offset-3 text-sm underline hover:text-gray-700 dark:hover:text-green-300 pr-2 block mb-2"
>
{item.replace(/_/g, " ")}
</Link>
))}
</div>
</div>
</div>
)
<div className="mt-2 mb-3 pl-1">
{outlinks &&
outlinks?.data.map((item, i) => (
<Link
key={i}
to={`/entries/${item}`}
className="text-foreground underline-offset-3 text-sm underline hover:text-gray-700 dark:hover:text-green-300 pr-2 block mb-2"
>
{item.replace(/_/g, " ")}
</Link>
))}
</div>
</div>
</div>
)
}

View file

@ -1,6 +1,10 @@
@import "tailwindcss";
@import "tw-animate-css";
html {
overflow-y: hidden;
}
@custom-variant dark (&:is(.dark *));
@theme inline {