feat: add custom body link component
This commit is contained in:
parent
f22f380b96
commit
47abc44506
8 changed files with 314 additions and 247 deletions
42
package-lock.json
generated
42
package-lock.json
generated
|
|
@ -10,7 +10,7 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@radix-ui/react-collapsible": "^1.1.7",
|
"@radix-ui/react-collapsible": "^1.1.7",
|
||||||
"@radix-ui/react-dialog": "^1.1.10",
|
"@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-label": "^2.1.7",
|
||||||
"@radix-ui/react-separator": "^1.1.4",
|
"@radix-ui/react-separator": "^1.1.4",
|
||||||
"@radix-ui/react-slot": "^1.2.3",
|
"@radix-ui/react-slot": "^1.2.3",
|
||||||
|
|
@ -1300,18 +1300,18 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@radix-ui/react-hover-card": {
|
"node_modules/@radix-ui/react-hover-card": {
|
||||||
"version": "1.1.14",
|
"version": "1.1.15",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-hover-card/-/react-hover-card-1.1.14.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-hover-card/-/react-hover-card-1.1.15.tgz",
|
||||||
"integrity": "sha512-CPYZ24Mhirm+g6D8jArmLzjYu4Eyg3TTUHswR26QgzXBHBe64BO/RHOJKzmF/Dxb4y4f9PKyJdwm/O/AhNkb+Q==",
|
"integrity": "sha512-qgTkjNT1CfKMoP0rcasmlH2r1DAiYicWsDsufxl940sT2wHNEWWv6FMWIQXWhVdmC1d/HYfbhQx60KYyAtKxjg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@radix-ui/primitive": "1.1.2",
|
"@radix-ui/primitive": "1.1.3",
|
||||||
"@radix-ui/react-compose-refs": "1.1.2",
|
"@radix-ui/react-compose-refs": "1.1.2",
|
||||||
"@radix-ui/react-context": "1.1.2",
|
"@radix-ui/react-context": "1.1.2",
|
||||||
"@radix-ui/react-dismissable-layer": "1.1.10",
|
"@radix-ui/react-dismissable-layer": "1.1.11",
|
||||||
"@radix-ui/react-popper": "1.2.7",
|
"@radix-ui/react-popper": "1.2.8",
|
||||||
"@radix-ui/react-portal": "1.1.9",
|
"@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-primitive": "2.1.3",
|
||||||
"@radix-ui/react-use-controllable-state": "1.2.2"
|
"@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": {
|
"node_modules/@radix-ui/react-hover-card/node_modules/@radix-ui/react-arrow": {
|
||||||
"version": "1.1.7",
|
"version": "1.1.7",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz",
|
"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": {
|
"node_modules/@radix-ui/react-hover-card/node_modules/@radix-ui/react-dismissable-layer": {
|
||||||
"version": "1.1.10",
|
"version": "1.1.11",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.10.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.11.tgz",
|
||||||
"integrity": "sha512-IM1zzRV4W3HtVgftdQiiOmA0AdJlCtMLe00FXaHwgt3rAnNsIyDqshvkIW3hj/iu5hu8ERP7KIYki6NkqDxAwQ==",
|
"integrity": "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@radix-ui/primitive": "1.1.2",
|
"@radix-ui/primitive": "1.1.3",
|
||||||
"@radix-ui/react-compose-refs": "1.1.2",
|
"@radix-ui/react-compose-refs": "1.1.2",
|
||||||
"@radix-ui/react-primitive": "2.1.3",
|
"@radix-ui/react-primitive": "2.1.3",
|
||||||
"@radix-ui/react-use-callback-ref": "1.1.1",
|
"@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": {
|
"node_modules/@radix-ui/react-hover-card/node_modules/@radix-ui/react-popper": {
|
||||||
"version": "1.2.7",
|
"version": "1.2.8",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.7.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.8.tgz",
|
||||||
"integrity": "sha512-IUFAccz1JyKcf/RjB552PlWwxjeCJB8/4KxT7EhBHOJM+mN7LdW+B3kacJXILm32xawcMMjb2i0cIZpo+f9kiQ==",
|
"integrity": "sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@floating-ui/react-dom": "^2.0.0",
|
"@floating-ui/react-dom": "^2.0.0",
|
||||||
|
|
@ -1437,9 +1443,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@radix-ui/react-hover-card/node_modules/@radix-ui/react-presence": {
|
"node_modules/@radix-ui/react-hover-card/node_modules/@radix-ui/react-presence": {
|
||||||
"version": "1.1.4",
|
"version": "1.1.5",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.4.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz",
|
||||||
"integrity": "sha512-ueDqRbdc4/bkaQT3GIpLQssRlFgWaL/U2z/S31qRwwLWoxHLgry3SIfCwhxeQNbirEUXFa+lq3RL3oBYXtcmIA==",
|
"integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@radix-ui/react-compose-refs": "1.1.2",
|
"@radix-ui/react-compose-refs": "1.1.2",
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@radix-ui/react-collapsible": "^1.1.7",
|
"@radix-ui/react-collapsible": "^1.1.7",
|
||||||
"@radix-ui/react-dialog": "^1.1.10",
|
"@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-label": "^2.1.7",
|
||||||
"@radix-ui/react-separator": "^1.1.4",
|
"@radix-ui/react-separator": "^1.1.4",
|
||||||
"@radix-ui/react-slot": "^1.2.3",
|
"@radix-ui/react-slot": "^1.2.3",
|
||||||
|
|
|
||||||
31
src/App.css
31
src/App.css
|
|
@ -1,25 +1,34 @@
|
||||||
@import url("https://fonts.googleapis.com/css2?family=JetBrains+Mono:ital,wght@0,100..800;1,100..800&display=swap");
|
@import url("https://fonts.googleapis.com/css2?family=JetBrains+Mono:ital,wght@0,100..800;1,100..800&display=swap");
|
||||||
|
|
||||||
code {
|
code {
|
||||||
font-family: "JetBrains Mono";
|
font-family: "JetBrains Mono";
|
||||||
}
|
}
|
||||||
|
|
||||||
pre > code {
|
pre>code {
|
||||||
padding: 0.5rem;
|
padding: 0.5rem;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
h2 > code {
|
h2>code {
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn[data-state="active"] {
|
.btn[data-state="active"] {
|
||||||
box-shadow: none !important;
|
box-shadow: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
button[data-state="active"] {
|
button[data-state="active"] {
|
||||||
--tw-shadow: none;
|
--tw-shadow: none;
|
||||||
--tw-shadow-colored: none;
|
--tw-shadow-colored: none;
|
||||||
box-shadow:
|
box-shadow:
|
||||||
var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-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; */
|
||||||
|
/* } */
|
||||||
|
|
|
||||||
50
src/components/BodyLink.tsx
Normal file
50
src/components/BodyLink.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -10,68 +10,68 @@ import { Link } from "react-router"
|
||||||
import { useState, useRef, useEffect } from "react"
|
import { useState, useRef, useEffect } from "react"
|
||||||
|
|
||||||
export default function EntriesListSidebar() {
|
export default function EntriesListSidebar() {
|
||||||
const scrollRef = useRef(null)
|
const scrollRef = useRef(null)
|
||||||
const [isOpen, setIsOpen] = useState(() => {
|
const [isOpen, setIsOpen] = useState(() => {
|
||||||
if (typeof window !== "undefined") {
|
if (typeof window !== "undefined") {
|
||||||
const saved = sessionStorage.getItem("entries_list_sidebar_open")
|
const saved = sessionStorage.getItem("entries_list_sidebar_open")
|
||||||
return saved ? JSON.parse(saved) : false
|
return saved ? JSON.parse(saved) : false
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
})
|
})
|
||||||
|
|
||||||
const { data: entries, isLoading } = useQuery({
|
const { data: entries, isLoading } = useQuery({
|
||||||
queryKey: ["entries_list"],
|
queryKey: ["entries_list"],
|
||||||
queryFn: () => api.get("/entries").then((res) => res.data),
|
queryFn: () => api.get("/entries").then((res) => res.data),
|
||||||
})
|
})
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
sessionStorage.setItem("entries_list_sidebar_open", JSON.stringify(isOpen))
|
sessionStorage.setItem("entries_list_sidebar_open", JSON.stringify(isOpen))
|
||||||
}, [isOpen])
|
}, [isOpen])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const savedScroll = sessionStorage.getItem("entries_list_sidebar_scroll_position")
|
const savedScroll = sessionStorage.getItem("entries_list_sidebar_scroll_position")
|
||||||
if (savedScroll && scrollRef.current) {
|
if (savedScroll && scrollRef.current) {
|
||||||
scrollRef.current.scrollTop = parseInt(savedScroll)
|
scrollRef.current.scrollTop = parseInt(savedScroll)
|
||||||
}
|
}
|
||||||
}, [entries])
|
}, [entries])
|
||||||
|
|
||||||
const handleScroll = () => {
|
const handleScroll = () => {
|
||||||
if (scrollRef.current) {
|
if (scrollRef.current) {
|
||||||
sessionStorage.setItem("entries_list_sidebar_scroll_position", scrollRef.current.scrollTop)
|
sessionStorage.setItem("entries_list_sidebar_scroll_position", scrollRef.current.scrollTop)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Collapsible open={isOpen} onOpenChange={setIsOpen} className="group/collapsible">
|
<Collapsible open={isOpen} onOpenChange={setIsOpen} className="group/collapsible">
|
||||||
<SidebarMenuItem key="entries">
|
<SidebarMenuItem key="entries">
|
||||||
<CollapsibleTrigger asChild className="rounded-none">
|
<CollapsibleTrigger asChild className="rounded-none">
|
||||||
<SidebarMenuButton asChild className="rounded-none">
|
<SidebarMenuButton asChild className="rounded-none">
|
||||||
<a href="#">
|
<a href="#">
|
||||||
<FileText />
|
<FileText />
|
||||||
<span>Entries</span>
|
<span>Entries</span>
|
||||||
<Badge className="ml-0" variant="secondary">
|
<Badge className="ml-0" variant="secondary">
|
||||||
{entries?.count}
|
{entries?.count}
|
||||||
</Badge>
|
</Badge>
|
||||||
<ChevronRight className="ml-auto transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90" />
|
<ChevronRight className="ml-auto transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90" />
|
||||||
</a>
|
</a>
|
||||||
</SidebarMenuButton>
|
</SidebarMenuButton>
|
||||||
</CollapsibleTrigger>
|
</CollapsibleTrigger>
|
||||||
<CollapsibleContent>
|
<CollapsibleContent>
|
||||||
<div ref={scrollRef} onScroll={handleScroll} className="max-h-100 overflow-y-auto">
|
<div ref={scrollRef} onScroll={handleScroll} className="max-h-100 overflow-y-auto">
|
||||||
<SidebarMenuSub>
|
<SidebarMenuSub>
|
||||||
{entries?.data.map((item, i) => (
|
{entries?.data.map((item, i) => (
|
||||||
<SidebarMenuItem key={i}>
|
<SidebarMenuItem key={i}>
|
||||||
<Link to={`/entries/${item.title}`}>
|
<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">
|
<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, " ")}
|
{item.title.replace(/_/g, " ")}
|
||||||
</span>
|
</span>
|
||||||
</Link>
|
</Link>
|
||||||
</SidebarMenuItem>
|
</SidebarMenuItem>
|
||||||
))}
|
))}
|
||||||
</SidebarMenuSub>
|
</SidebarMenuSub>
|
||||||
</div>
|
</div>
|
||||||
</CollapsibleContent>
|
</CollapsibleContent>
|
||||||
</SidebarMenuItem>
|
</SidebarMenuItem>
|
||||||
</Collapsible>
|
</Collapsible>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,100 +5,99 @@ import remarkMath from "remark-math"
|
||||||
import rehypeKatex from "rehype-katex"
|
import rehypeKatex from "rehype-katex"
|
||||||
import "katex/dist/katex.min.css"
|
import "katex/dist/katex.min.css"
|
||||||
import { Skeleton } from "@/components/ui/skeleton"
|
import { Skeleton } from "@/components/ui/skeleton"
|
||||||
|
import BodyLink from "./BodyLink"
|
||||||
|
|
||||||
const EntryLoadingSkeleton = () => {
|
const EntryLoadingSkeleton = () => {
|
||||||
return (
|
return (
|
||||||
<div className="space-y-2 max-w-2xl p-4 lg:p-6">
|
<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-[400px] md:h-[800px] max-w-2xl rounded-none" />
|
||||||
*/}
|
*/}
|
||||||
|
|
||||||
<Skeleton className="h-4 max-w-full" />
|
<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-xl max-w-[300px]" />
|
||||||
<Skeleton className="h-4 md:max-w-[400px] max-w-[250px]" />
|
<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-[300px] max-w-[200px]" />
|
||||||
<Skeleton className="h-4 md:max-w-[200px] max-w-[150px]" />
|
<Skeleton className="h-4 md:max-w-[200px] max-w-[150px]" />
|
||||||
<Skeleton className="h-4 md:max-w-[100px] max-w-[100px]" />
|
<Skeleton className="h-4 md:max-w-[100px] max-w-[100px]" />
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const ImagePreprocessor = (src) => {
|
const ImagePreprocessor = (src) => {
|
||||||
const filename = src.src.split("/").pop()
|
const filename = src.src.split("/").pop()
|
||||||
const s3RootUrl = "https://eolas.s3.systemsobscure.net/"
|
const s3RootUrl = "https://eolas.s3.systemsobscure.net/"
|
||||||
return <img src={s3RootUrl + filename} />
|
return <img src={s3RootUrl + filename} />
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function EntryBody({ body, isLoading }) {
|
export default function EntryBody({ body, isLoading }) {
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return <EntryLoadingSkeleton />
|
return <EntryLoadingSkeleton />
|
||||||
} else
|
} else
|
||||||
return (
|
return (
|
||||||
<div className="max-w-2xl p-4 lg:p-6">
|
<div className="max-w-2xl p-4 lg:p-6">
|
||||||
<ReactMarkdown
|
<ReactMarkdown
|
||||||
remarkPlugins={[remarkGfm, remarkMath]}
|
remarkPlugins={[remarkGfm, remarkMath]}
|
||||||
rehypePlugins={[rehypeKatex]}
|
rehypePlugins={[rehypeKatex]}
|
||||||
components={{
|
components={{
|
||||||
h1: () => null,
|
h1: () => null,
|
||||||
h2: ({ children }) => (
|
h2: ({ children }) => (
|
||||||
<h2 className="scroll-m-20 font-semibold mt-8 mb-4 first:mt-0">{children}</h2>
|
<h2 className="scroll-m-20 font-semibold mt-8 mb-4 first:mt-0">{children}</h2>
|
||||||
),
|
),
|
||||||
h3: ({ children }) => (
|
h3: ({ children }) => (
|
||||||
<h3 className="scroll-m-20 font-semibold mt-8 mb-4 first:mt-0">{children}</h3>
|
<h3 className="scroll-m-20 font-semibold mt-8 mb-4 first:mt-0">{children}</h3>
|
||||||
),
|
),
|
||||||
h4: ({ children }) => (
|
h4: ({ children }) => (
|
||||||
<h4 className="scroll-m-20 font-semibold mt-8 mb-4 first:mt-0">{children}</h4>
|
<h4 className="scroll-m-20 font-semibold mt-8 mb-4 first:mt-0">{children}</h4>
|
||||||
),
|
),
|
||||||
p: ({ children }) => (
|
p: ({ children }) => (
|
||||||
<p className="leading-[1.5] mb-4 not-first:mt-4">{children}</p>
|
<p className="leading-[1.5] mb-4 not-first:mt-4">{children}</p>
|
||||||
),
|
),
|
||||||
ul: ({ children }) => (
|
ul: ({ children }) => (
|
||||||
<ul className="list-disc ml-10 mb-4 space-y-1">{children}</ul>
|
<ul className="list-disc ml-10 mb-4 space-y-1">{children}</ul>
|
||||||
),
|
),
|
||||||
ol: ({ children }) => (
|
ol: ({ children }) => (
|
||||||
<ol className="list-decimal ml-10 mb-4 space-y-1">{children}</ol>
|
<ol className="list-decimal ml-10 mb-4 space-y-1">{children}</ol>
|
||||||
),
|
),
|
||||||
a: ({ href, children }) => (
|
table: ({ children }) => (
|
||||||
<a href={href} target="_blank" rel="noopener noreferrer">
|
<table className="w-full mb-4 text-sm">{children}</table>
|
||||||
{children}
|
),
|
||||||
</a>
|
tr: ({ children }) => (
|
||||||
),
|
<tr className="even:bg-muted m-0 border-t p-0">{children}</tr>
|
||||||
table: ({ children }) => (
|
),
|
||||||
<table className="w-full mb-4 text-sm">{children}</table>
|
th: ({ children }) => (
|
||||||
),
|
<th className="border px-4 py-2 text-left font-bold [&[align=center]]:text-center [&[align=right]]:text-right">
|
||||||
tr: ({ children }) => (
|
{children}
|
||||||
<tr className="even:bg-muted m-0 border-t p-0">{children}</tr>
|
</th>
|
||||||
),
|
),
|
||||||
th: ({ children }) => (
|
td: ({ children }) => (
|
||||||
<th className="border px-4 py-2 text-left font-bold [&[align=center]]:text-center [&[align=right]]:text-right">
|
<td className="border px-4 py-2 text-left [&[align=center]]:text-center [&[align=right]]:text-right">
|
||||||
{children}
|
{children}
|
||||||
</th>
|
</td>
|
||||||
),
|
),
|
||||||
td: ({ children }) => (
|
blockquote: ({ children }) => (
|
||||||
<td className="border px-4 py-2 text-left [&[align=center]]:text-center [&[align=right]]:text-right">
|
<blockquote className="mt-4 border-l-2 pl-6 text-muted-foreground">
|
||||||
{children}
|
{children}
|
||||||
</td>
|
</blockquote>
|
||||||
),
|
),
|
||||||
blockquote: ({ children }) => (
|
pre: ({ children }) => {
|
||||||
<blockquote className="mt-4 border-l-2 pl-6 text-muted-foreground">
|
const child = children.props
|
||||||
{children}
|
return <CodeBlock className={child.className}>{child.children}</CodeBlock>
|
||||||
</blockquote>
|
},
|
||||||
),
|
code: ({ children }) => (
|
||||||
pre: ({ children }) => {
|
<code className="rounded bg-muted px-[0.3rem] py-[0.2rem] font-mono text-sm">
|
||||||
const child = children.props
|
{children}
|
||||||
return <CodeBlock className={child.className}>{child.children}</CodeBlock>
|
</code>
|
||||||
},
|
),
|
||||||
code: ({ children }) => (
|
img: ({ src }) => <ImagePreprocessor src={src} />,
|
||||||
<code className="rounded bg-muted px-[0.3rem] py-[0.2rem] font-mono text-sm">
|
a: ({ href, children }) => {
|
||||||
{children}
|
return <BodyLink link={href} children={children} />
|
||||||
</code>
|
},
|
||||||
),
|
}}
|
||||||
img: ({ children, src }) => <ImagePreprocessor src={src} />,
|
>
|
||||||
}}
|
{body}
|
||||||
>
|
</ReactMarkdown>
|
||||||
{body}
|
</div>
|
||||||
</ReactMarkdown>
|
)
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,85 +4,84 @@ import { Link } from "react-router"
|
||||||
import { Badge } from "./ui/badge"
|
import { Badge } from "./ui/badge"
|
||||||
|
|
||||||
export default function EntryReferences({ entryTitle }) {
|
export default function EntryReferences({ entryTitle }) {
|
||||||
const { data: tags, isLoading: tagsLoading } = useQuery({
|
const { data: tags, isLoading: tagsLoading } = useQuery({
|
||||||
queryKey: [`tags_for_${entryTitle}`],
|
queryKey: [`tags_for_${entryTitle}`],
|
||||||
queryFn: () => api.get(`/tags/${entryTitle}`).then((res) => res.data),
|
queryFn: () => api.get(`/tags/${entryTitle}`).then((res) => res.data),
|
||||||
})
|
})
|
||||||
|
|
||||||
const { data: backlinks, isLoading: backlinksLoading } = useQuery({
|
const { data: backlinks, isLoading: backlinksLoading } = useQuery({
|
||||||
queryKey: [`backlinks_for_${entryTitle}`],
|
queryKey: [`backlinks_for_${entryTitle}`],
|
||||||
queryFn: () => api.get(`/entries/backlinks/${entryTitle}`).then((res) => res.data),
|
queryFn: () => api.get(`/entries/backlinks/${entryTitle}`).then((res) => res.data),
|
||||||
})
|
})
|
||||||
|
|
||||||
const { data: outlinks, isLoading: outlinksLoading } = useQuery({
|
const { data: outlinks, isLoading: outlinksLoading } = useQuery({
|
||||||
queryKey: [`outlinks_for_${entryTitle}`],
|
queryKey: [`outlinks_for_${entryTitle}`],
|
||||||
queryFn: () => api.get(`/entries/outlinks/${entryTitle}`).then((res) => res.data),
|
queryFn: () => api.get(`/entries/outlinks/${entryTitle}`).then((res) => res.data),
|
||||||
})
|
})
|
||||||
|
|
||||||
console.log(backlinks)
|
return (
|
||||||
return (
|
<div className="w-full flex flex-row justify-between gap-3">
|
||||||
<div className="w-full flex flex-row justify-between gap-3">
|
<div className="w-full">
|
||||||
<div className="w-full">
|
<div className="flex flex row justify-between bg-sidebar">
|
||||||
<div className="flex flex row justify-between bg-sidebar">
|
<h3 className="font-medium text-sm p-1 ml-1">Tags</h3>
|
||||||
<h3 className="font-medium text-sm p-1 ml-1">Tags</h3>
|
<Badge variant="secondary" className="rounded-none">
|
||||||
<Badge variant="secondary" className="rounded-none">
|
{tags?.count}
|
||||||
{tags?.count}
|
</Badge>
|
||||||
</Badge>
|
</div>
|
||||||
</div>
|
<div className="mt-2 mb-3 pl-1">
|
||||||
<div className="mt-2 mb-3 pl-1">
|
{tags?.data.map((item, i) => (
|
||||||
{tags?.data.map((item, i) => (
|
<Link
|
||||||
<Link
|
key={i}
|
||||||
key={i}
|
to={`/tags/${item}`}
|
||||||
to={`/tags/${item}`}
|
className="text-foreground underline-offset-3 text-sm underline hover:text-gray-700 dark:hover:text-green-300 pr-2"
|
||||||
className="text-foreground underline-offset-3 text-sm underline hover:text-gray-700 dark:hover:text-green-300 pr-2"
|
>
|
||||||
>
|
{item}
|
||||||
{item}
|
</Link>
|
||||||
</Link>
|
))}
|
||||||
))}
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex flex row justify-between bg-sidebar">
|
<div className="flex flex row justify-between bg-sidebar">
|
||||||
<h3 className="font-medium text-sm p-1 ml-1">Incoming links</h3>
|
<h3 className="font-medium text-sm p-1 ml-1">Incoming links</h3>
|
||||||
<Badge variant="secondary" className="rounded-none">
|
<Badge variant="secondary" className="rounded-none">
|
||||||
{backlinks?.count}
|
{backlinks?.count}
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-2 mb-3 pl-1">
|
<div className="mt-2 mb-3 pl-1">
|
||||||
{backlinks &&
|
{backlinks &&
|
||||||
backlinks?.data.map((item, i) => (
|
backlinks?.data.map((item, i) => (
|
||||||
<Link
|
<Link
|
||||||
key={i}
|
key={i}
|
||||||
to={`/entries/${item}`}
|
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"
|
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, " ")}
|
{item.replace(/_/g, " ")}
|
||||||
</Link>
|
</Link>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
<div className="flex flex row justify-between bg-sidebar">
|
<div className="flex flex row justify-between bg-sidebar">
|
||||||
<h3 className="font-medium text-sm p-1 ml-1">Outgoing links</h3>
|
<h3 className="font-medium text-sm p-1 ml-1">Outgoing links</h3>
|
||||||
<Badge variant="secondary" className="rounded-none">
|
<Badge variant="secondary" className="rounded-none">
|
||||||
{outlinks?.count}
|
{outlinks?.count}
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-2 mb-3 pl-1">
|
<div className="mt-2 mb-3 pl-1">
|
||||||
{outlinks &&
|
{outlinks &&
|
||||||
outlinks?.data.map((item, i) => (
|
outlinks?.data.map((item, i) => (
|
||||||
<Link
|
<Link
|
||||||
key={i}
|
key={i}
|
||||||
to={`/entries/${item}`}
|
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"
|
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, " ")}
|
{item.replace(/_/g, " ")}
|
||||||
</Link>
|
</Link>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,10 @@
|
||||||
@import "tailwindcss";
|
@import "tailwindcss";
|
||||||
@import "tw-animate-css";
|
@import "tw-animate-css";
|
||||||
|
|
||||||
|
html {
|
||||||
|
overflow-y: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
@custom-variant dark (&:is(.dark *));
|
@custom-variant dark (&:is(.dark *));
|
||||||
|
|
||||||
@theme inline {
|
@theme inline {
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue