add table of contents

This commit is contained in:
keyan 2022-07-18 16:24:28 -05:00
parent beef34abfa
commit 70cbdd057a
9 changed files with 192 additions and 55 deletions

View File

@ -77,7 +77,6 @@
}
.hunk {
overflow: visible;
margin-bottom: 0;
margin-top: 0.15rem;
}

View File

@ -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, {

View File

@ -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 />}

View File

@ -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}>

View File

@ -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>
)
}
)

View File

@ -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}>

View File

@ -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",

View File

@ -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;

1
svgs/list-unordered.svg Normal file
View File

@ -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