tag and folder pages

This commit is contained in:
Jacky Zhao
2023-07-01 00:03:01 -07:00
parent 24348b24a9
commit ba9f243728
25 changed files with 586 additions and 123 deletions

View File

@ -1,31 +0,0 @@
import { QuartzComponentConstructor, QuartzComponentProps } from "./types"
import { Fragment, jsx, jsxs } from 'preact/jsx-runtime'
import { toJsxRuntime } from "hast-util-to-jsx-runtime"
// @ts-ignore
import popoverScript from './scripts/popover.inline'
import popoverStyle from './styles/popover.scss'
interface Options {
enablePopover: boolean
}
const defaultOptions: Options = {
enablePopover: true
}
export default ((opts?: Partial<Options>) => {
function Content({ tree }: QuartzComponentProps) {
// @ts-ignore (preact makes it angry)
const content = toJsxRuntime(tree, { Fragment, jsx, jsxs, elementAttributeNameCase: 'html' })
return <article>{content}</article>
}
const enablePopover = opts?.enablePopover ?? defaultOptions.enablePopover
if (enablePopover) {
Content.afterDOMLoaded = popoverScript
Content.css = popoverStyle
}
return Content
}) satisfies QuartzComponentConstructor

View File

@ -0,0 +1,12 @@
interface Props {
date: Date
}
export function Date({ date }: Props) {
const formattedDate = date.toLocaleDateString('en-US', {
year: "numeric",
month: "short",
day: '2-digit'
})
return <>{formattedDate}</>
}

View File

@ -0,0 +1,27 @@
import { QuartzComponentConstructor } from "./types"
import style from "./styles/footer.scss"
interface Options {
authorName: string,
links: Record<string, string>
}
export default ((opts?: Options) => {
function Footer() {
const year = new Date().getFullYear()
const name = opts?.authorName ?? "someone"
const links = opts?.links ?? []
return <>
<hr />
<footer>
<p>Made by {name} using <a>Quartz</a>, © {year}</p>
<ul>{Object.entries(links).map(([text, link]) => <li>
<a href={link}>{text}</a>
</li>)}</ul>
</footer>
</>
}
Footer.css = style
return Footer
}) satisfies QuartzComponentConstructor

View File

@ -0,0 +1,53 @@
import { relativeToRoot } from "../path"
import { QuartzPluginData } from "../plugins/vfile"
import { Date } from "./Date"
import { stripIndex } from "./scripts/util"
import { QuartzComponentProps } from "./types"
function byDateAndAlphabetical(f1: QuartzPluginData, f2: QuartzPluginData): number {
if (f1.dates && f2.dates) {
// sort descending by last modified
return f2.dates.modified.getTime() - f1.dates.modified.getTime()
} else if (f1.dates && !f2.dates) {
// prioritize files with dates
return -1
} else if (!f1.dates && f2.dates) {
return 1
}
// otherwise, sort lexographically by title
const f1Title = f1.frontmatter?.title.toLowerCase() ?? ""
const f2Title = f2.frontmatter?.title.toLowerCase() ?? ""
return f1Title.localeCompare(f2Title)
}
export function PageList({ fileData, allFiles }: QuartzComponentProps) {
const slug = fileData.slug!
return <ul class="section-ul">
{allFiles.sort(byDateAndAlphabetical).map(page => {
const title = page.frontmatter?.title
const pageSlug = page.slug!
const tags = page.frontmatter?.tags ?? []
return <li class="section-li">
<div class="section">
{page.dates && <p class="meta">
<Date date={page.dates.modified} />
</p>}
<div class="desc">
<h3><a href={stripIndex(relativeToRoot(slug, pageSlug))} class="internal">{title}</a></h3>
</div>
<div class="spacer"></div>
<ul class="tags">
{tags.map(tag => <li><a href={relativeToRoot(slug, `tags/${tag}`)}>#{tag}</a></li>)}
</ul>
</div>
</li>
})}
</ul>
}
PageList.css = `
.section h3 {
margin: 0;
}
`

View File

@ -1,5 +1,7 @@
import ArticleTitle from "./ArticleTitle"
import Content from "./Content"
import Content from "./pages/Content"
import TagContent from "./pages/TagContent"
import FolderContent from "./pages/FolderContent"
import Darkmode from "./Darkmode"
import Head from "./Head"
import PageTitle from "./PageTitle"
@ -10,10 +12,13 @@ import TagList from "./TagList"
import Graph from "./Graph"
import Backlinks from "./Backlinks"
import Search from "./Search"
import Footer from "./Footer"
export {
ArticleTitle,
Content,
TagContent,
FolderContent,
Darkmode,
Head,
PageTitle,
@ -23,5 +28,6 @@ export {
TagList,
Graph,
Backlinks,
Search
Search,
Footer
}

View File

@ -0,0 +1,11 @@
import { QuartzComponentConstructor, QuartzComponentProps } from "../types"
import { Fragment, jsx, jsxs } from 'preact/jsx-runtime'
import { toJsxRuntime } from "hast-util-to-jsx-runtime"
function Content({ tree }: QuartzComponentProps) {
// @ts-ignore (preact makes it angry)
const content = toJsxRuntime(tree, { Fragment, jsx, jsxs, elementAttributeNameCase: 'html' })
return <article>{content}</article>
}
export default (() => Content) satisfies QuartzComponentConstructor

View File

@ -0,0 +1,37 @@
import { QuartzComponentConstructor, QuartzComponentProps } from "../types"
import { Fragment, jsx, jsxs } from 'preact/jsx-runtime'
import { toJsxRuntime } from "hast-util-to-jsx-runtime"
import path from "path"
import style from '../styles/listPage.scss'
import { PageList } from "../PageList"
function TagContent(props: QuartzComponentProps) {
const { tree, fileData, allFiles } = props
const folderSlug = fileData.slug!
const allPagesInFolder = allFiles.filter(file => {
const fileSlug = file.slug ?? ""
const prefixed = fileSlug.startsWith(folderSlug)
const folderParts = folderSlug.split(path.posix.sep)
const fileParts = fileSlug.split(path.posix.sep)
const isDirectChild = fileParts.length === folderParts.length + 1
return prefixed && isDirectChild
})
const listProps = {
...props,
allFiles: allPagesInFolder
}
// @ts-ignore
const content = toJsxRuntime(tree, { Fragment, jsx, jsxs, elementAttributeNameCase: 'html' })
return <div>
<article>{content}</article>
<div>
<PageList {...listProps} />
</div>
</div>
}
TagContent.css = style + PageList.css
export default (() => TagContent) satisfies QuartzComponentConstructor

View File

@ -0,0 +1,33 @@
import { QuartzComponentConstructor, QuartzComponentProps } from "../types"
import { Fragment, jsx, jsxs } from 'preact/jsx-runtime'
import { toJsxRuntime } from "hast-util-to-jsx-runtime"
import style from '../styles/listPage.scss'
import { PageList } from "../PageList"
function TagContent(props: QuartzComponentProps) {
const { tree, fileData, allFiles } = props
const slug = fileData.slug
if (slug?.startsWith("tags/")) {
const tag = slug.slice("tags/".length)
const allPagesWithTag = allFiles.filter(file => (file.frontmatter?.tags ?? []).includes(tag))
const listProps = {
...props,
allFiles: allPagesWithTag
}
// @ts-ignore
const content = toJsxRuntime(tree, { Fragment, jsx, jsxs, elementAttributeNameCase: 'html' })
return <div>
<article>{content}</article>
<div>
<PageList {...listProps} />
</div>
</div>
} else {
throw `Component "TagContent" tried to render a non-tag page: ${slug}`
}
}
TagContent.css = style + PageList.css
export default (() => TagContent) satisfies QuartzComponentConstructor

View File

@ -0,0 +1,63 @@
import { render } from "preact-render-to-string";
import { QuartzComponent, QuartzComponentProps } from "./types";
import HeaderConstructor from "./Header"
import BodyConstructor from "./Body"
import { JSResourceToScriptElement, StaticResources } from "../resources";
import { resolveToRoot } from "../path";
interface RenderComponents {
head: QuartzComponent
header: QuartzComponent[],
beforeBody: QuartzComponent[],
pageBody: QuartzComponent,
left: QuartzComponent[],
right: QuartzComponent[],
footer: QuartzComponent,
}
export function pageResources(slug: string, staticResources: StaticResources): StaticResources {
const baseDir = resolveToRoot(slug)
return {
css: [baseDir + "/index.css", ...staticResources.css],
js: [
{ src: baseDir + "/prescript.js", loadTime: "beforeDOMReady", contentType: "external" },
...staticResources.js,
{ src: baseDir + "/postscript.js", loadTime: "afterDOMReady", moduleType: 'module', contentType: "external" }
]
}
}
export function renderPage(slug: string, componentData: QuartzComponentProps, components: RenderComponents, pageResources: StaticResources): string {
const { head: Head, header, beforeBody, pageBody: Content, left, right, footer: Footer } = components
const Header = HeaderConstructor()
const Body = BodyConstructor()
const doc = <html>
<Head {...componentData} />
<body data-slug={slug}>
<div id="quartz-root" class="page">
<Header {...componentData} >
{header.map(HeaderComponent => <HeaderComponent {...componentData} />)}
</Header>
<div class="popover-hint">
{beforeBody.map(BodyComponent => <BodyComponent {...componentData} />)}
</div>
<Body {...componentData}>
<div class="left">
{left.map(BodyComponent => <BodyComponent {...componentData} />)}
</div>
<div class="center popover-hint">
<Content {...componentData} />
</div>
<div class="right">
{right.map(BodyComponent => <BodyComponent {...componentData} />)}
</div>
</Body>
<Footer {...componentData} />
</div>
</body>
{pageResources.js.filter(resource => resource.loadTime === "afterDOMReady").map(res => JSResourceToScriptElement(res))}
</html>
return "<!DOCTYPE html>\n" + render(doc)
}

View File

@ -13,7 +13,19 @@ type LinkData = {
target: string
}
const localStorageKey = "graph-visited"
function getVisited(): Set<string> {
return new Set(JSON.parse(localStorage.getItem(localStorageKey) ?? "[]"))
}
function addToVisited(slug: string) {
const visited = getVisited()
visited.add(slug)
localStorage.setItem(localStorageKey, JSON.stringify([...visited]))
}
async function renderGraph(container: string, slug: string) {
const visited = getVisited()
const graph = document.getElementById(container)
if (!graph) return
removeAllChildren(graph)
@ -106,7 +118,13 @@ async function renderGraph(container: string, slug: string) {
// calculate radius
const color = (d: NodeData) => {
const isCurrent = d.id === slug
return isCurrent ? "var(--secondary)" : "var(--gray)"
if (isCurrent) {
return "var(--secondary)"
} else if (visited.has(d.id)) {
return "var(--tertiary)"
} else {
return "var(--gray)"
}
}
const drag = (simulation: d3.Simulation<NodeData, LinkData>) => {
@ -267,9 +285,15 @@ function renderGlobalGraph() {
document.addEventListener("nav", async (e: unknown) => {
const slug = (e as CustomEventMap["nav"]).detail.url
addToVisited(slug)
await renderGraph("graph-container", slug)
const containerIcon = document.getElementById("global-graph-icon")
containerIcon?.removeEventListener("click", renderGlobalGraph)
containerIcon?.addEventListener("click", renderGlobalGraph)
})
window.addEventListener('resize', async () => {
const slug = document.body.dataset["slug"]!
await renderGraph("graph-container", slug)
})

View File

@ -0,0 +1,13 @@
footer {
text-align: left;
opacity: 0.8;
& ul {
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-direction: row;
gap: 1rem;
margin-top: -1rem;
}
}

View File

@ -9,7 +9,6 @@
border: 1px solid var(--lightgray);
box-sizing: border-box;
height: 250px;
width: 300px;
margin: 0.5em 0;
position: relative;

View File

@ -0,0 +1,36 @@
ul.section-ul {
list-style: none;
margin-top: 2em;
padding-left: 0;
}
li.section-li {
margin-bottom: 1em;
& > .section {
display: flex;
align-items: center;
@media all and (max-width: 600px) {
& .tags {
display: none;
}
}
& h3 > a {
font-weight: 700;
margin: 0;
background-color: transparent;
}
& p {
margin: 0;
padding-right: 1em;
flex-basis: 6em;
}
}
& .meta {
opacity: 0.6;
}
}

View File

@ -26,6 +26,7 @@
font-weight: initial;
line-height: initial;
font-size: initial;
font-family: var(--bodyFont);
border: 1px solid var(--gray);
background-color: var(--light);
border-radius: 5px;

View File

@ -102,6 +102,7 @@
& .highlight {
color: var(--secondary);
font-weight: 700;
}
&:hover, &:focus {