From 70cbdd057a63841afd83fc7eb51284c3b2488087 Mon Sep 17 00:00:00 2001 From: keyan Date: Mon, 18 Jul 2022 16:24:28 -0500 Subject: [PATCH] add table of contents --- components/comment.module.css | 1 - components/comments.js | 4 +- components/item-full.js | 2 +- components/item.js | 19 +++--- components/table-of-contents.js | 91 +++++++++++++++++++++++++ components/text.js | 13 ++-- package.json | 2 + styles/globals.scss | 114 ++++++++++++++++++++++---------- svgs/list-unordered.svg | 1 + 9 files changed, 192 insertions(+), 55 deletions(-) create mode 100644 components/table-of-contents.js create mode 100644 svgs/list-unordered.svg diff --git a/components/comment.module.css b/components/comment.module.css index cd205557..6e80226f 100644 --- a/components/comment.module.css +++ b/components/comment.module.css @@ -77,7 +77,6 @@ } .hunk { - overflow: visible; margin-bottom: 0; margin-top: 0.15rem; } diff --git a/components/comments.js b/components/comments.js index 283d3414..ed99b8a2 100644 --- a/components/comments.js +++ b/components/comments.js @@ -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, { diff --git a/components/item-full.js b/components/item-full.js index 60c5576a..cd402bff 100644 --- a/components/item-full.js +++ b/components/item-full.js @@ -83,7 +83,7 @@ function TopLevelItem ({ item, noReply, ...props }) { const ItemComponent = item.maxBid ? ItemJob : Item return ( - + {item.text && } {item.url && } {!noReply && } diff --git a/components/item.js b/components/item.js index ae734dbe..a77383a2 100644 --- a/components/item.js +++ b/components/item.js @@ -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 }) { {/* eslint-disable-next-line */} - - apply - + + apply +
{item.status !== 'NOSATS' @@ -89,6 +90,7 @@ export function ItemJob ({ item, rank, children }) { }
+ {toc && } {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 }) {
{showFwdUser && item.fwdUser && } + {toc && } {children && (
diff --git a/components/table-of-contents.js b/components/table-of-contents.js new file mode 100644 index 00000000..eb6f3f12 --- /dev/null +++ b/components/table-of-contents.js @@ -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 ( + + + + + + + {toc.map(v => { + return ( + {v.heading} + + ) + })} + + + ) +} + +const CustomToggle = React.forwardRef(({ children, onClick }, ref) => ( + { + e.preventDefault() + onClick(e) + }} + > + {children} + +)) + +// 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 ( +
+ setValue(e.target.value)} + value={value} + /> +
    + {React.Children.toArray(children).filter( + (child) => + !value || child.props.children.toLowerCase().includes(value) + )} +
+
+ ) + } +) diff --git a/components/text.js b/components/text.js index 80495d12..ba3f9c80 100644 --- a/components/text.js +++ b/components/text.js @@ -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 (
diff --git a/package.json b/package.json index 44e5a229..7cd276a1 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/styles/globals.scss b/styles/globals.scss index 4b05c904..4fcff96a 100644 --- a/styles/globals.scss +++ b/styles/globals.scss @@ -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; diff --git a/svgs/list-unordered.svg b/svgs/list-unordered.svg new file mode 100644 index 00000000..702a11e7 --- /dev/null +++ b/svgs/list-unordered.svg @@ -0,0 +1 @@ + \ No newline at end of file