add table of contents
This commit is contained in:
parent
beef34abfa
commit
70cbdd057a
|
@ -77,7 +77,6 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.hunk {
|
.hunk {
|
||||||
overflow: visible;
|
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
margin-top: 0.15rem;
|
margin-top: 0.15rem;
|
||||||
}
|
}
|
||||||
|
|
|
@ -59,7 +59,9 @@ export default function Comments ({ parentId, comments, ...props }) {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const hash = window.location.hash
|
const hash = window.location.hash
|
||||||
if (hash) {
|
if (hash) {
|
||||||
|
try {
|
||||||
document.querySelector(hash).scrollIntoView({ behavior: 'smooth' })
|
document.querySelector(hash).scrollIntoView({ behavior: 'smooth' })
|
||||||
|
} catch {}
|
||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
const [getComments, { loading }] = useLazyQuery(COMMENTS_QUERY, {
|
const [getComments, { loading }] = useLazyQuery(COMMENTS_QUERY, {
|
||||||
|
|
|
@ -83,7 +83,7 @@ function TopLevelItem ({ item, noReply, ...props }) {
|
||||||
const ItemComponent = item.maxBid ? ItemJob : Item
|
const ItemComponent = item.maxBid ? ItemJob : Item
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ItemComponent item={item} showFwdUser {...props}>
|
<ItemComponent item={item} toc showFwdUser {...props}>
|
||||||
{item.text && <ItemText item={item} />}
|
{item.text && <ItemText item={item} />}
|
||||||
{item.url && <ItemEmbed item={item} />}
|
{item.url && <ItemEmbed item={item} />}
|
||||||
{!noReply && <Reply parentId={item.id} meComments={item.meComments} replyOpen />}
|
{!noReply && <Reply parentId={item.id} meComments={item.meComments} replyOpen />}
|
||||||
|
|
|
@ -10,6 +10,7 @@ import reactStringReplace from 'react-string-replace'
|
||||||
import { formatSats } from '../lib/format'
|
import { formatSats } from '../lib/format'
|
||||||
import * as Yup from 'yup'
|
import * as Yup from 'yup'
|
||||||
import Briefcase from '../svgs/briefcase-4-fill.svg'
|
import Briefcase from '../svgs/briefcase-4-fill.svg'
|
||||||
|
import Toc from './table-of-contents'
|
||||||
|
|
||||||
function SearchTitle ({ title }) {
|
function SearchTitle ({ title }) {
|
||||||
return reactStringReplace(title, /:high\[([^\]]+)\]/g, (match, i) => {
|
return reactStringReplace(title, /:high\[([^\]]+)\]/g, (match, i) => {
|
||||||
|
@ -17,7 +18,7 @@ function SearchTitle ({ title }) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ItemJob ({ item, rank, children }) {
|
export function ItemJob ({ item, toc, rank, children }) {
|
||||||
const isEmail = Yup.string().email().isValidSync(item.url)
|
const isEmail = Yup.string().email().isValidSync(item.url)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -89,6 +90,7 @@ export function ItemJob ({ item, rank, children }) {
|
||||||
</>}
|
</>}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{toc && <Toc text={item.text} />}
|
||||||
</div>
|
</div>
|
||||||
{children && (
|
{children && (
|
||||||
<div className={`${styles.children}`}>
|
<div className={`${styles.children}`}>
|
||||||
|
@ -110,7 +112,7 @@ function FwdUser ({ user }) {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Item ({ item, rank, showFwdUser, children }) {
|
export default function Item ({ item, rank, showFwdUser, toc, children }) {
|
||||||
const mine = item.mine
|
const mine = item.mine
|
||||||
const editThreshold = new Date(item.createdAt).getTime() + 10 * 60000
|
const editThreshold = new Date(item.createdAt).getTime() + 10 * 60000
|
||||||
const [canEdit, setCanEdit] =
|
const [canEdit, setCanEdit] =
|
||||||
|
@ -202,6 +204,7 @@ export default function Item ({ item, rank, showFwdUser, children }) {
|
||||||
</div>
|
</div>
|
||||||
{showFwdUser && item.fwdUser && <FwdUser user={item.fwdUser} />}
|
{showFwdUser && item.fwdUser && <FwdUser user={item.fwdUser} />}
|
||||||
</div>
|
</div>
|
||||||
|
{toc && <Toc text={item.text} />}
|
||||||
</div>
|
</div>
|
||||||
{children && (
|
{children && (
|
||||||
<div className={styles.children}>
|
<div className={styles.children}>
|
||||||
|
|
|
@ -0,0 +1,91 @@
|
||||||
|
import React, { useState } from 'react'
|
||||||
|
import { Dropdown, FormControl } from 'react-bootstrap'
|
||||||
|
import TocIcon from '../svgs/list-unordered.svg'
|
||||||
|
import { fromMarkdown } from 'mdast-util-from-markdown'
|
||||||
|
import { visit } from 'unist-util-visit'
|
||||||
|
import { toString } from 'mdast-util-to-string'
|
||||||
|
import GithubSlugger from 'github-slugger'
|
||||||
|
|
||||||
|
export default function Toc ({ text }) {
|
||||||
|
if (!text || text.length === 0) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const tree = fromMarkdown(text)
|
||||||
|
const toc = []
|
||||||
|
const slugger = new GithubSlugger()
|
||||||
|
visit(tree, 'heading', (node, position, parent) => {
|
||||||
|
const str = toString(node)
|
||||||
|
toc.push({ heading: str, slug: slugger.slug(str.replace(/[^\w\-\s]+/gi, '')), depth: node.depth })
|
||||||
|
})
|
||||||
|
|
||||||
|
if (toc.length === 0) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dropdown alignRight>
|
||||||
|
<Dropdown.Toggle as={CustomToggle} id='dropdown-custom-components'>
|
||||||
|
<TocIcon className='mx-2 fill-grey theme' />
|
||||||
|
</Dropdown.Toggle>
|
||||||
|
|
||||||
|
<Dropdown.Menu as={CustomMenu}>
|
||||||
|
{toc.map(v => {
|
||||||
|
return (
|
||||||
|
<Dropdown.Item
|
||||||
|
className={v.depth === 1 ? 'font-weight-bold' : ''}
|
||||||
|
style={{
|
||||||
|
marginLeft: `${(v.depth - 1) * 5}px`
|
||||||
|
}}
|
||||||
|
key={v.slug} href={`#${v.slug}`}
|
||||||
|
>{v.heading}
|
||||||
|
</Dropdown.Item>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</Dropdown.Menu>
|
||||||
|
</Dropdown>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const CustomToggle = React.forwardRef(({ children, onClick }, ref) => (
|
||||||
|
<a
|
||||||
|
href=''
|
||||||
|
ref={ref}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault()
|
||||||
|
onClick(e)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</a>
|
||||||
|
))
|
||||||
|
|
||||||
|
// forwardRef again here!
|
||||||
|
// Dropdown needs access to the DOM of the Menu to measure it
|
||||||
|
const CustomMenu = React.forwardRef(
|
||||||
|
({ children, style, className, 'aria-labelledby': labeledBy }, ref) => {
|
||||||
|
const [value, setValue] = useState('')
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
style={style}
|
||||||
|
className={className}
|
||||||
|
aria-labelledby={labeledBy}
|
||||||
|
>
|
||||||
|
<FormControl
|
||||||
|
className='mx-3 my-2 w-auto'
|
||||||
|
placeholder='filter'
|
||||||
|
onChange={(e) => setValue(e.target.value)}
|
||||||
|
value={value}
|
||||||
|
/>
|
||||||
|
<ul className='list-unstyled'>
|
||||||
|
{React.Children.toArray(children).filter(
|
||||||
|
(child) =>
|
||||||
|
!value || child.props.children.toLowerCase().includes(value)
|
||||||
|
)}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
|
@ -11,6 +11,7 @@ import reactStringReplace from 'react-string-replace'
|
||||||
import React, { useEffect, useState } from 'react'
|
import React, { useEffect, useState } from 'react'
|
||||||
import GithubSlugger from 'github-slugger'
|
import GithubSlugger from 'github-slugger'
|
||||||
import Link from '../svgs/link.svg'
|
import Link from '../svgs/link.svg'
|
||||||
|
import {toString} from 'mdast-util-to-string'
|
||||||
|
|
||||||
function myRemarkPlugin () {
|
function myRemarkPlugin () {
|
||||||
return (tree) => {
|
return (tree) => {
|
||||||
|
@ -29,16 +30,10 @@ function myRemarkPlugin () {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
function Heading ({ h, slugger, noFragments, topLevel, children, node, ...props }) {
|
function Heading ({ h, slugger, noFragments, topLevel, children, node, ...props }) {
|
||||||
const id = noFragments
|
const id = noFragments ? undefined : slugger.slug(toString(node).replace(/[^\w\-\s]+/gi, ''))
|
||||||
? undefined
|
|
||||||
: slugger.slug(children.reduce(
|
|
||||||
(acc, cur) => {
|
|
||||||
if (typeof cur !== 'string') {
|
|
||||||
return acc
|
|
||||||
}
|
|
||||||
return acc + cur.replace(/[^\w\-\s]+/gi, '')
|
|
||||||
}, ''))
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.heading}>
|
<div className={styles.heading}>
|
||||||
|
|
|
@ -28,6 +28,8 @@
|
||||||
"graphql-type-json": "^0.3.2",
|
"graphql-type-json": "^0.3.2",
|
||||||
"ln-service": "^52.8.0",
|
"ln-service": "^52.8.0",
|
||||||
"mdast-util-find-and-replace": "^1.1.1",
|
"mdast-util-find-and-replace": "^1.1.1",
|
||||||
|
"mdast-util-from-markdown": "^1.2.0",
|
||||||
|
"mdast-util-to-string": "^3.1.0",
|
||||||
"next": "^11.1.2",
|
"next": "^11.1.2",
|
||||||
"next-auth": "^3.29.3",
|
"next-auth": "^3.29.3",
|
||||||
"next-plausible": "^2.1.3",
|
"next-plausible": "^2.1.3",
|
||||||
|
|
|
@ -54,7 +54,9 @@ $tooltip-bg: #5c8001;
|
||||||
@import "~bootstrap/scss/bootstrap";
|
@import "~bootstrap/scss/bootstrap";
|
||||||
|
|
||||||
@media screen and (min-width: 767px) {
|
@media screen and (min-width: 767px) {
|
||||||
.table-sm th, .table-sm td {
|
|
||||||
|
.table-sm th,
|
||||||
|
.table-sm td {
|
||||||
padding: .3rem .75rem;
|
padding: .3rem .75rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -82,16 +84,20 @@ $tooltip-bg: #5c8001;
|
||||||
color: var(--theme-grey) !important;
|
color: var(--theme-grey) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
ol, ul, dl {
|
ol,
|
||||||
|
ul,
|
||||||
|
dl {
|
||||||
padding-inline-start: 2rem;
|
padding-inline-start: 2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
mark {
|
mark {
|
||||||
background-color: var(--primary);
|
background-color: var(--primary
|
||||||
|
);
|
||||||
padding: 0 0.2rem;
|
padding: 0 0.2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.table-sm th, .table-sm td {
|
.table-sm th,
|
||||||
|
.table-sm td {
|
||||||
line-height: 1.2rem;
|
line-height: 1.2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -114,7 +120,9 @@ mark {
|
||||||
background-color: var(--theme-body);
|
background-color: var(--theme-body);
|
||||||
}
|
}
|
||||||
|
|
||||||
.table th, .table td, .table thead th {
|
.table th,
|
||||||
|
.table td,
|
||||||
|
.table thead th {
|
||||||
border-color: var(--theme-borderColor);
|
border-color: var(--theme-borderColor);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -138,7 +146,8 @@ a:hover {
|
||||||
color: var(--theme-linkHover);
|
color: var(--theme-linkHover);
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-tabs .nav-link.active, .nav-tabs .nav-item.show .nav-link {
|
.nav-tabs .nav-link.active,
|
||||||
|
.nav-tabs .nav-item.show .nav-link {
|
||||||
color: inherit;
|
color: inherit;
|
||||||
background-color: var(--theme-inputBg);
|
background-color: var(--theme-inputBg);
|
||||||
border-color: var(--theme-borderColor);
|
border-color: var(--theme-borderColor);
|
||||||
|
@ -156,7 +165,8 @@ a:hover {
|
||||||
color: var(--theme-color);
|
color: var(--theme-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-control:disabled, .form-control[readonly] {
|
.form-control:disabled,
|
||||||
|
.form-control[readonly] {
|
||||||
background-color: var(--theme-inputDisabledBg);
|
background-color: var(--theme-inputDisabledBg);
|
||||||
border-color: var(--theme-borderColor);
|
border-color: var(--theme-borderColor);
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
|
@ -190,7 +200,8 @@ a:hover {
|
||||||
color: var(--theme-navLink) !important;
|
color: var(--theme-navLink) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-link:not(.text-success):hover, .nav-link:not(.text-success):focus {
|
.nav-link:not(.text-success):hover,
|
||||||
|
.nav-link:not(.text-success):focus {
|
||||||
color: var(--theme-navLinkFocus) !important;
|
color: var(--theme-navLinkFocus) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -205,6 +216,8 @@ a:hover {
|
||||||
.dropdown-menu {
|
.dropdown-menu {
|
||||||
background-color: var(--theme-inputBg);
|
background-color: var(--theme-inputBg);
|
||||||
border: 1px solid var(--theme-borderColor);
|
border: 1px solid var(--theme-borderColor);
|
||||||
|
max-width: 90vw;
|
||||||
|
overflow: scroll;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dropdown-item {
|
.dropdown-item {
|
||||||
|
@ -267,7 +280,13 @@ footer {
|
||||||
}
|
}
|
||||||
|
|
||||||
@media screen and (max-width: 767px) {
|
@media screen and (max-width: 767px) {
|
||||||
input, select, textarea, .form-control, .form-control:focus, .input-group-text {
|
|
||||||
|
input,
|
||||||
|
select,
|
||||||
|
textarea,
|
||||||
|
.form-control,
|
||||||
|
.form-control:focus,
|
||||||
|
.input-group-text {
|
||||||
font-size: 1rem !important;
|
font-size: 1rem !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -420,12 +439,29 @@ textarea.form-control {
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes flash {
|
@keyframes flash {
|
||||||
from { filter: brightness(1);}
|
from {
|
||||||
2% { filter: brightness(2.3); }
|
filter: brightness(1);
|
||||||
4% { filter: brightness(1.4); }
|
}
|
||||||
8% { filter: brightness(3); }
|
|
||||||
16% { filter: brightness(1); }
|
2% {
|
||||||
to { filter: brightness(1);}
|
filter: brightness(2.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
4% {
|
||||||
|
filter: brightness(1.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
8% {
|
||||||
|
filter: brightness(3);
|
||||||
|
}
|
||||||
|
|
||||||
|
16% {
|
||||||
|
filter: brightness(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
filter: brightness(1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.clouds {
|
.clouds {
|
||||||
|
@ -443,8 +479,13 @@ textarea.form-control {
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes spin {
|
@keyframes spin {
|
||||||
0% { transform: rotate(0deg); }
|
0% {
|
||||||
100% { transform: rotate(359deg); }
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
transform: rotate(359deg);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.spin {
|
.spin {
|
||||||
|
@ -465,6 +506,7 @@ textarea.form-control {
|
||||||
from {
|
from {
|
||||||
transform: rotateX(180deg);
|
transform: rotateX(180deg);
|
||||||
}
|
}
|
||||||
|
|
||||||
to {
|
to {
|
||||||
transform: rotateX(-180deg);
|
transform: rotateX(-180deg);
|
||||||
}
|
}
|
||||||
|
@ -478,6 +520,7 @@ textarea.form-control {
|
||||||
from {
|
from {
|
||||||
transform: rotateY(0deg);
|
transform: rotateY(0deg);
|
||||||
}
|
}
|
||||||
|
|
||||||
to {
|
to {
|
||||||
transform: rotateY(360deg);
|
transform: rotateY(360deg);
|
||||||
}
|
}
|
||||||
|
@ -487,7 +530,8 @@ textarea.form-control {
|
||||||
animation: flipY 4s linear infinite;
|
animation: flipY 4s linear infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (hover:none), (hover:on-demand) {
|
@media (hover:none),
|
||||||
|
(hover:on-demand) {
|
||||||
.tooltip {
|
.tooltip {
|
||||||
visibility: hidden;
|
visibility: hidden;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path fill="none" d="M0 0h24v24H0z"/><path d="M8 4h13v2H8V4zM4.5 6.5a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3zm0 7a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3zm0 6.9a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3zM8 11h13v2H8v-2zm0 7h13v2H8v-2z"/></svg>
|
After Width: | Height: | Size: 312 B |
Loading…
Reference in New Issue