add table of contents
This commit is contained in:
parent
beef34abfa
commit
70cbdd057a
|
@ -77,7 +77,6 @@
|
|||
}
|
||||
|
||||
.hunk {
|
||||
overflow: visible;
|
||||
margin-bottom: 0;
|
||||
margin-top: 0.15rem;
|
||||
}
|
||||
|
|
|
@ -59,7 +59,9 @@ export default function Comments ({ parentId, comments, ...props }) {
|
|||
useEffect(() => {
|
||||
const hash = window.location.hash
|
||||
if (hash) {
|
||||
document.querySelector(hash).scrollIntoView({ behavior: 'smooth' })
|
||||
try {
|
||||
document.querySelector(hash).scrollIntoView({ behavior: 'smooth' })
|
||||
} catch {}
|
||||
}
|
||||
}, [])
|
||||
const [getComments, { loading }] = useLazyQuery(COMMENTS_QUERY, {
|
||||
|
|
|
@ -83,7 +83,7 @@ function TopLevelItem ({ item, noReply, ...props }) {
|
|||
const ItemComponent = item.maxBid ? ItemJob : Item
|
||||
|
||||
return (
|
||||
<ItemComponent item={item} showFwdUser {...props}>
|
||||
<ItemComponent item={item} toc showFwdUser {...props}>
|
||||
{item.text && <ItemText item={item} />}
|
||||
{item.url && <ItemEmbed item={item} />}
|
||||
{!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 * as Yup from 'yup'
|
||||
import Briefcase from '../svgs/briefcase-4-fill.svg'
|
||||
import Toc from './table-of-contents'
|
||||
|
||||
function SearchTitle ({ title }) {
|
||||
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)
|
||||
|
||||
return (
|
||||
|
@ -52,12 +53,12 @@ export function ItemJob ({ item, rank, children }) {
|
|||
</a>
|
||||
</Link>
|
||||
{/* eslint-disable-next-line */}
|
||||
<a
|
||||
className={`${styles.link}`}
|
||||
target='_blank' href={(isEmail ? 'mailto:' : '') + item.url}
|
||||
>
|
||||
apply
|
||||
</a>
|
||||
<a
|
||||
className={`${styles.link}`}
|
||||
target='_blank' href={(isEmail ? 'mailto:' : '') + item.url}
|
||||
>
|
||||
apply
|
||||
</a>
|
||||
</div>
|
||||
<div className={`${styles.other}`}>
|
||||
{item.status !== 'NOSATS'
|
||||
|
@ -89,6 +90,7 @@ export function ItemJob ({ item, rank, children }) {
|
|||
</>}
|
||||
</div>
|
||||
</div>
|
||||
{toc && <Toc text={item.text} />}
|
||||
</div>
|
||||
{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 editThreshold = new Date(item.createdAt).getTime() + 10 * 60000
|
||||
const [canEdit, setCanEdit] =
|
||||
|
@ -202,6 +204,7 @@ export default function Item ({ item, rank, showFwdUser, children }) {
|
|||
</div>
|
||||
{showFwdUser && item.fwdUser && <FwdUser user={item.fwdUser} />}
|
||||
</div>
|
||||
{toc && <Toc text={item.text} />}
|
||||
</div>
|
||||
{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 GithubSlugger from 'github-slugger'
|
||||
import Link from '../svgs/link.svg'
|
||||
import {toString} from 'mdast-util-to-string'
|
||||
|
||||
function myRemarkPlugin () {
|
||||
return (tree) => {
|
||||
|
@ -29,16 +30,10 @@ function myRemarkPlugin () {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
function Heading ({ h, slugger, noFragments, topLevel, children, node, ...props }) {
|
||||
const id = noFragments
|
||||
? undefined
|
||||
: slugger.slug(children.reduce(
|
||||
(acc, cur) => {
|
||||
if (typeof cur !== 'string') {
|
||||
return acc
|
||||
}
|
||||
return acc + cur.replace(/[^\w\-\s]+/gi, '')
|
||||
}, ''))
|
||||
const id = noFragments ? undefined : slugger.slug(toString(node).replace(/[^\w\-\s]+/gi, ''))
|
||||
|
||||
return (
|
||||
<div className={styles.heading}>
|
||||
|
|
|
@ -28,6 +28,8 @@
|
|||
"graphql-type-json": "^0.3.2",
|
||||
"ln-service": "^52.8.0",
|
||||
"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-auth": "^3.29.3",
|
||||
"next-plausible": "^2.1.3",
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
$theme-colors: (
|
||||
"primary" : #FADA5E,
|
||||
"primary" : #FADA5E,
|
||||
"secondary" : #F6911D,
|
||||
"danger" : #c03221,
|
||||
"info" : #007cbe,
|
||||
|
@ -14,17 +14,17 @@ $border-radius: .4rem;
|
|||
$enable-transitions: false;
|
||||
$enable-gradients: false;
|
||||
$enable-shadows: false;
|
||||
$btn-transition: none;
|
||||
$btn-transition: none;
|
||||
$form-feedback-icon-valid: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='12' height='12'%3E%3Cpath fill='none' d='M0 0h24v24H0z'/%3E%3Cpath d='M2 9h3v12H2a1 1 0 0 1-1-1V10a1 1 0 0 1 1-1zm5.293-1.293l6.4-6.4a.5.5 0 0 1 .654-.047l.853.64a1.5 1.5 0 0 1 .553 1.57L14.6 8H21a2 2 0 0 1 2 2v2.104a2 2 0 0 1-.15.762l-3.095 7.515a1 1 0 0 1-.925.619H8a1 1 0 0 1-1-1V8.414a1 1 0 0 1 .293-.707z' fill='rgba(92, 128, 1, 1)'/%3E%3C/svg%3E");
|
||||
$form-feedback-icon-invalid: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='12' height='12'%3E%3Cpath fill='none' d='M0 0h24v24H0z'/%3E%3Cpath d='M22 15h-3V3h3a1 1 0 0 1 1 1v10a1 1 0 0 1-1 1zm-5.293 1.293l-6.4 6.4a.5.5 0 0 1-.654.047L8.8 22.1a1.5 1.5 0 0 1-.553-1.57L9.4 16H3a2 2 0 0 1-2-2v-2.104a2 2 0 0 1 .15-.762L4.246 3.62A1 1 0 0 1 5.17 3H16a1 1 0 0 1 1 1v11.586a1 1 0 0 1-.293.707z' fill='rgba(192,50,33,1)'/%3E%3C/svg%3E");
|
||||
$line-height-base: 1.75;
|
||||
$input-btn-padding-y: .42rem;
|
||||
$input-btn-padding-x: .84rem;
|
||||
$btn-padding-y: .42rem;
|
||||
$btn-padding-x: 1.1rem;
|
||||
$btn-padding-x: 1.1rem;
|
||||
$btn-font-weight: bold;
|
||||
$btn-focus-width: 0;
|
||||
$btn-border-width: 0;
|
||||
$btn-border-width: 0;
|
||||
$btn-focus-box-shadow: none;
|
||||
$alert-border-width: 0;
|
||||
$close-text-shadow: none;
|
||||
|
@ -38,13 +38,13 @@ $dropdown-border-color: #ced4da;
|
|||
$dropdown-link-active-color: inherit;
|
||||
$dropdown-link-hover-bg: transparent;
|
||||
$dropdown-link-active-bg: transparent;
|
||||
$dropdown-link-color: rgba(0, 0, 0, 0.7);
|
||||
$dropdown-link-color: rgba(0, 0, 0, 0.7);
|
||||
$dropdown-link-hover-color: rgba(0, 0, 0, 0.9);
|
||||
$container-max-widths: (
|
||||
sm: 540px,
|
||||
md: 720px,
|
||||
lg: 900px,
|
||||
) !default;
|
||||
) !default;
|
||||
$nav-link-padding-y: .1rem;
|
||||
$nav-tabs-link-active-bg: #fff;
|
||||
$nav-tabs-link-hover-border-color: transparent;
|
||||
|
@ -54,7 +54,9 @@ $tooltip-bg: #5c8001;
|
|||
@import "~bootstrap/scss/bootstrap";
|
||||
|
||||
@media screen and (min-width: 767px) {
|
||||
.table-sm th, .table-sm td {
|
||||
|
||||
.table-sm th,
|
||||
.table-sm td {
|
||||
padding: .3rem .75rem;
|
||||
}
|
||||
}
|
||||
|
@ -74,7 +76,7 @@ $tooltip-bg: #5c8001;
|
|||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.modal-close + .modal-body {
|
||||
.modal-close+.modal-body {
|
||||
padding-top: 0.5rem;
|
||||
}
|
||||
|
||||
|
@ -82,16 +84,20 @@ $tooltip-bg: #5c8001;
|
|||
color: var(--theme-grey) !important;
|
||||
}
|
||||
|
||||
ol, ul, dl {
|
||||
ol,
|
||||
ul,
|
||||
dl {
|
||||
padding-inline-start: 2rem;
|
||||
}
|
||||
|
||||
mark {
|
||||
background-color: var(--primary);
|
||||
padding: 0 0.2rem;
|
||||
background-color: var(--primary
|
||||
);
|
||||
padding: 0 0.2rem;
|
||||
}
|
||||
|
||||
.table-sm th, .table-sm td {
|
||||
.table-sm th,
|
||||
.table-sm td {
|
||||
line-height: 1.2rem;
|
||||
}
|
||||
|
||||
|
@ -114,7 +120,9 @@ mark {
|
|||
background-color: var(--theme-body);
|
||||
}
|
||||
|
||||
.table th, .table td, .table thead th {
|
||||
.table th,
|
||||
.table td,
|
||||
.table thead th {
|
||||
border-color: var(--theme-borderColor);
|
||||
}
|
||||
|
||||
|
@ -138,7 +146,8 @@ a:hover {
|
|||
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;
|
||||
background-color: var(--theme-inputBg);
|
||||
border-color: var(--theme-borderColor);
|
||||
|
@ -152,11 +161,12 @@ a:hover {
|
|||
}
|
||||
|
||||
.form-control:focus {
|
||||
background-color: var(--theme-inputBg);
|
||||
background-color: var(--theme-inputBg);
|
||||
color: var(--theme-color);
|
||||
}
|
||||
|
||||
.form-control:disabled, .form-control[readonly] {
|
||||
.form-control:disabled,
|
||||
.form-control[readonly] {
|
||||
background-color: var(--theme-inputDisabledBg);
|
||||
border-color: var(--theme-borderColor);
|
||||
opacity: 1;
|
||||
|
@ -190,7 +200,8 @@ a:hover {
|
|||
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;
|
||||
}
|
||||
|
||||
|
@ -205,6 +216,8 @@ a:hover {
|
|||
.dropdown-menu {
|
||||
background-color: var(--theme-inputBg);
|
||||
border: 1px solid var(--theme-borderColor);
|
||||
max-width: 90vw;
|
||||
overflow: scroll;
|
||||
}
|
||||
|
||||
.dropdown-item {
|
||||
|
@ -267,7 +280,13 @@ footer {
|
|||
}
|
||||
|
||||
@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;
|
||||
}
|
||||
}
|
||||
|
@ -420,17 +439,34 @@ textarea.form-control {
|
|||
}
|
||||
|
||||
@keyframes flash {
|
||||
from { filter: brightness(1);}
|
||||
2% { filter: brightness(2.3); }
|
||||
4% { filter: brightness(1.4); }
|
||||
8% { filter: brightness(3); }
|
||||
16% { filter: brightness(1); }
|
||||
to { filter: brightness(1);}
|
||||
from {
|
||||
filter: brightness(1);
|
||||
}
|
||||
|
||||
2% {
|
||||
filter: brightness(2.3);
|
||||
}
|
||||
|
||||
4% {
|
||||
filter: brightness(1.4);
|
||||
}
|
||||
|
||||
8% {
|
||||
filter: brightness(3);
|
||||
}
|
||||
|
||||
16% {
|
||||
filter: brightness(1);
|
||||
}
|
||||
|
||||
to {
|
||||
filter: brightness(1);
|
||||
}
|
||||
}
|
||||
|
||||
.clouds {
|
||||
background-image: url('/clouds.jpeg') !important;
|
||||
background-color:var(--theme-grey);
|
||||
background-color: var(--theme-grey);
|
||||
background-repeat: repeat;
|
||||
background-origin: content-box;
|
||||
background-size: cover;
|
||||
|
@ -443,8 +479,13 @@ textarea.form-control {
|
|||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(359deg); }
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: rotate(359deg);
|
||||
}
|
||||
}
|
||||
|
||||
.spin {
|
||||
|
@ -453,7 +494,7 @@ textarea.form-control {
|
|||
|
||||
.static {
|
||||
background: url('/giphy.gif');
|
||||
background-color:var(--theme-grey);
|
||||
background-color: var(--theme-grey);
|
||||
background-repeat: repeat;
|
||||
background-origin: content-box;
|
||||
background-size: cover;
|
||||
|
@ -461,12 +502,13 @@ textarea.form-control {
|
|||
opacity: .1;
|
||||
}
|
||||
|
||||
@keyframes flipX{
|
||||
@keyframes flipX {
|
||||
from {
|
||||
transform: rotateX(180deg);
|
||||
transform: rotateX(180deg);
|
||||
}
|
||||
|
||||
to {
|
||||
transform: rotateX(-180deg);
|
||||
transform: rotateX(-180deg);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -474,12 +516,13 @@ textarea.form-control {
|
|||
animation: flipX 2s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes flipY{
|
||||
@keyframes flipY {
|
||||
from {
|
||||
transform: rotateY(0deg);
|
||||
transform: rotateY(0deg);
|
||||
}
|
||||
|
||||
to {
|
||||
transform: rotateY(360deg);
|
||||
transform: rotateY(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -487,7 +530,8 @@ textarea.form-control {
|
|||
animation: flipY 4s linear infinite;
|
||||
}
|
||||
|
||||
@media (hover:none), (hover:on-demand) {
|
||||
@media (hover:none),
|
||||
(hover:on-demand) {
|
||||
.tooltip {
|
||||
visibility: hidden;
|
||||
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