import React from "react"; import { Link, useNavigate, useLocation } from "react-router-dom"; import useSWR from "swr"; import { WRITER_MODE, WRITER_MODE_HOSTNAMES } from "../env"; import { useLocale } from "../hooks"; import { Loading } from "../ui/atoms/loading"; import { MainContentContainer } from "../ui/atoms/page-content"; import "./index.scss"; import NoteCard from "../ui/molecules/notecards"; interface SearchIndexDoc { url: string; title: string; } export default function Sitemap() { const location = useLocation(); const navigate = useNavigate(); const locale = useLocale(); // Because you can load this app with something like `/en-us/_sitemap/Web/` // we have to pretend that didn't happen and force it to be `/en-US/_sitemap/Web` const pathname = location.pathname.endsWith("/") ? location.pathname.slice(0, -1) : location.pathname; // `pathname` is going to be something like `/en-US/_sitemap/Web/Foo`. // Transform that to be just `en-us/docs/web/foo`. const searchPathname = pathname .replace(`/${locale}/_sitemap`, `/${locale}/docs`) .toLowerCase(); React.useEffect(() => { document.title = "Sitemap"; }, []); const { data, error } = useSWR( `/${locale}/search-index.json`, async (url) => { const response = await fetch(url); if (!response.ok) { throw new Error(`${response.status} on ${response.url}`); } return (await response.json()) as SearchIndexDoc[]; }, { revalidateOnFocus: false, } ); const [docs, setDocs] = React.useState(null); React.useEffect(() => { if (data) { const theseDocs = [...data].sort((a, b) => a.url.localeCompare(b.url)); setDocs(theseDocs); } }, [data]); const [childCounts, setChildCounts] = React.useState>( new Map() ); React.useEffect(() => { const counts = new Map(); if (docs) { for (const { url } of docs) { const split = url.split("/"); const root = split.slice(0, 3); split.slice(3).forEach((portion, i) => { root.push(portion); const key = root.join("/"); counts.set(key, (counts.get(key) || 0) + 1); }); } setChildCounts(counts); } }, [docs]); const [thisDoc, setThisDoc] = React.useState(null); React.useEffect(() => { if (docs) { const newThisDoc = docs.find((doc) => { return doc.url.toLowerCase() === searchPathname; }); setThisDoc(newThisDoc || null); } }, [searchPathname, docs]); const [searchFilter, setSearchFilter] = React.useState(""); React.useEffect(() => { setSearchFilter(""); }, [pathname]); const [searchSubmitted, setSearchSubmitted] = React.useState(false); const [filtered, setFiltered] = React.useState(null); React.useEffect(() => { if (docs) { const depth = searchPathname.split("/").length; const newFiltered = docs.filter((doc) => { if ( doc.url.toLowerCase().startsWith(searchPathname) && depth + 1 === doc.url.split("/").length ) { const baseName = doc.url.split("/").slice(-1)[0].toLowerCase(); if (!baseName.startsWith(searchFilter.toLowerCase())) { return false; } return true; } return false; }); setFiltered(newFiltered); } }, [searchPathname, docs, searchFilter]); const [highlightIndex, setHighlightIndex] = React.useState(0); React.useEffect(() => { setHighlightIndex(0); }, [searchFilter]); React.useEffect(() => { if (searchSubmitted) { if (filtered && filtered.length >= 1) { const slug = filtered[highlightIndex].url.split("/").slice(3); setSearchFilter(""); setSearchSubmitted(false); navigate(`/${locale}/_sitemap/${slug.join("/")}`); } } }, [locale, filtered, searchSubmitted, navigate, highlightIndex]); function changeHighlight(direction: "up" | "down") { if (direction === "up") { let nextNumber = highlightIndex - 1; if (filtered) { nextNumber = ((nextNumber % filtered.length) + filtered.length) % filtered.length; } setHighlightIndex(nextNumber); } else { let nextNumber = highlightIndex + 1; if (filtered) { nextNumber = nextNumber % filtered.length; } setHighlightIndex(nextNumber); } } const [opening, setOpening] = React.useState(null); const [editorOpeningError, setEditorOpeningError] = React.useState(null); React.useEffect(() => { let unsetOpeningTimer: ReturnType; if (opening) { unsetOpeningTimer = setTimeout(() => { setOpening(null); }, 3000); } return () => { if (unsetOpeningTimer) { clearTimeout(unsetOpeningTimer); } }; }, [opening]); async function openInYourEditor(url: string) { console.log(`Going to try to open ${url} in your editor`); setOpening(url); const sp = new URLSearchParams(); sp.set("url", url); try { const response = await fetch(`/_open?${sp.toString()}`); if (!response.ok) { if (response.status >= 500) { setEditorOpeningError( new Error(`${response.status}: ${response.statusText}`) ); } else { const body = await response.text(); setEditorOpeningError(new Error(`${response.status}: ${body}`)); } } } catch (err: any) { setEditorOpeningError(err); } } return (
{error && (

Error

{error.toString()}

)} {editorOpeningError && (

Error opening in your editor

{editorOpeningError.toString()}

)} {!data && !error && }
{opening && ( <> Opening{" "} {opening.slice(opening.length - 50, opening.length)}{" "} in your editor... )}
{filtered && ( )} {filtered && ( { setSearchFilter(text); setSearchSubmitted(submitted); }} onGoUp={() => { // Navigate to the parent! ...if possible const split = pathname.split("/"); if (split.length >= 4) { const parentPathname = split.slice(0, -1); navigate(parentPathname.join("/")); } }} onChangeHighlight={changeHighlight} /> )} {filtered && filtered.length === 0 && (searchFilter ? ( nothing found ) : ( has no further sub-documents ))} {filtered && !searchFilter && } {filtered && filtered.length > 0 && ( )}

Note, this sitemap only shows documents. Not any other applications.

); } function GoBackUp({ pathname }: { pathname: string }) { const parentPath = pathname.split("/").slice(0, -1); if (parentPath.length <= 2) { return null; } const parentBasename = parentPath[parentPath.length - 1]; return (

↖️ Back up to{" "} {parentPath.length <= 3 ? root : {parentBasename}}

); } function FilterForm({ pathname, searchFilter, onUpdate, onGoUp, onChangeHighlight, }: { pathname: string; searchFilter: string; onUpdate: (text: string, submitted: boolean) => void; onGoUp: () => void; onChangeHighlight: (s: "up" | "down") => void; }) { const [hideTip, setHideTip] = React.useState(false); const [countBackspaces, setCountBackspaces] = React.useState(0); React.useEffect(() => { if (countBackspaces >= 2) { setCountBackspaces(0); onGoUp(); } }, [countBackspaces, onGoUp]); const inputRef = React.useRef(null); const focusSearch = React.useCallback( (event: KeyboardEvent) => { if (inputRef.current && event.target) { const target = event.target as HTMLElement; if (target === inputRef.current) { if (event.key === "ArrowDown") { onChangeHighlight("down"); } else if (event.key === "ArrowUp") { onChangeHighlight("up"); } } if (target.tagName === "INPUT" || target.tagName === "TEXTAREA") { if (event.key === "Backspace" && event.target === inputRef.current) { if (!searchFilter.trim()) { setCountBackspaces((s) => s + 1); } } else { setCountBackspaces(0); } if (event.key === "Escape") { inputRef.current.blur(); } } else { if (event.key === "T" || event.key === "t") { inputRef.current.focus(); setCountBackspaces(0); } } } }, [onChangeHighlight, searchFilter] ); React.useEffect(() => { window.document.addEventListener("keyup", focusSearch); return () => { window.document.removeEventListener("keyup", focusSearch); }; }, [focusSearch]); const prefixPathname = pathname.replace(`/_sitemap`, "/docs"); return (
{ event.preventDefault(); onUpdate(searchFilter.trim(), true); }} > {prefixPathname}/ { onUpdate(event.target.value, false); }} onFocus={() => { setHideTip(true); }} onBlur={() => { setHideTip(false); }} />{" "} {!hideTip && ( Tip! press t on your keyboard to focus on search filter )}
); } function Breadcrumb({ pathname, thisDoc, openInYourEditor, }: { pathname: string; thisDoc: SearchIndexDoc | null; openInYourEditor: (url: string) => void; }) { const locale = useLocale(); const split = pathname.split("/").slice(3); const root = pathname.split("/").slice(0, 2); root.push("_sitemap"); const isReadOnly = !WRITER_MODE_HOSTNAMES.includes(window.location.hostname); return ( <> ); } function ShowTree({ filtered, childCounts, highlightIndex, openInYourEditor, }: { filtered: SearchIndexDoc[]; childCounts: Map; highlightIndex: number; openInYourEditor: (url: string) => void; }) { const locale = useLocale(); const isReadOnly = !WRITER_MODE_HOSTNAMES.includes(window.location.hostname); return (
    {filtered.map((doc, i) => { const countChild = childCounts.get(doc.url) || 0; return (
  • {doc.url.replace(`/${locale}/docs`, "")} {" "} ( {countChild === 1 ? "1 document" : `${countChild.toLocaleString()} documents`} {" | "} View {!isReadOnly && " | "} {!isReadOnly && ( { event.preventDefault(); openInYourEditor(doc.url); }} > Edit )} )
  • ); })}
); }