diff --git a/.eslintrc.json b/.eslintrc.json index 600c693b..1a579ced 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -45,6 +45,7 @@ "next.config.js", "next-i18next.config.js", "scripts/widget-client.ts", + "scripts/build-search-index.ts", "public/*" ] } diff --git a/.lintstagedrc.js b/.lintstagedrc.js index 35e23046..936c852f 100644 --- a/.lintstagedrc.js +++ b/.lintstagedrc.js @@ -1,10 +1,10 @@ -const path = require('path') +const path = require("path"); const buildEslintCommand = (filenames) => `next lint --fix --file ${filenames .map((f) => path.relative(process.cwd(), f)) - .join(' --file ')}` + .join(" --file ")}`; module.exports = { - '*.{js,jsx,ts,tsx}': [buildEslintCommand], -} + "*.{js,jsx,ts,tsx}": [buildEslintCommand], +}; diff --git a/components/DocSearch/DocSearch.module.scss b/components/DocSearch/DocSearch.module.scss new file mode 100644 index 00000000..dd147a0c --- /dev/null +++ b/components/DocSearch/DocSearch.module.scss @@ -0,0 +1,208 @@ +@import "styles/mixins/media"; + +.searchButton { + display: flex; + align-items: center; + width: 100%; + max-width: 24rem; + gap: 0.5rem; + padding: 0.5rem; + font-size: 0.875rem; + color: var(--blue-text); + background: var(--background-secondary); + border: 1px solid var(--blue-dark); + border-radius: 0.5rem; + transition: background-color 0.2s; + + &:hover { + background: var(--blue-dark); + } +} + +.shortcut { + display: none; + padding: 0.25rem 0.5rem; + margin-left: auto; + font-size: 0.75rem; + font-weight: 500; + color: var(--blue-text); + background: var(--blue-dark); + border-radius: 0.25rem; + + @include for-tablet-up { + display: inline-flex; + } +} +.dialog { + position: fixed; + top: 10%; + left: 50%; + transform: translateX(-50%); + width: min-content; + min-width: 32rem; + max-width: 90%; + max-height: calc(100vh - 20%); + background: var(--blue-darkest); + border: 1px solid var(--blue-dark); + border-radius: 30px; + padding: 20px; + box-shadow: 0 5px 10px rgba(0, 0, 0, 0.5); + z-index: 1000; + transition: + opacity 0.3s ease, + transform 0.3s ease; + overflow: auto; + + &.open { + opacity: 1; + transform: translateX(-50%) scale(1); + } + + &.close { + opacity: 0; + transform: translateX(-50%) scale(0.9); + } +} + +.overlay { + position: fixed; + top: 0; + right: 0; + bottom: 0; + left: 0; + background: var(--black-alpha-8); + z-index: 40; +} + +.dialogHeader { + padding: 1rem; + border-bottom: 1px solid var(--blue-dark); +} + +.dialogTitle { + font-family: "commuters_sans", sans-serif; + font-size: 1rem; + font-weight: 300; + color: var(--text-primary); +} + +.searchInput { + width: 100%; + padding: 0.75rem 1rem; + font-family: "proxima_nova", sans-serif; + font-size: 0.875rem; + color: var(--text-primary); + background: transparent; + border: none; + outline: none; + border-bottom: 1px solid var(--blue-dark); + + &::placeholder { + color: var(--blue-text); + } +} + +.resultsList { + max-height: 60vh; + overflow-y: auto; + padding: 0.5rem 0; + + a { + text-decoration: none !important; + } +} + +.noResults { + padding: 0.75rem 1rem; + color: var(--blue-text); + text-align: center; +} + +.resultGroup { + &:not(:last-child) { + margin-bottom: 0.5rem; + } +} + +.groupHeading { + padding: 0.5rem 1rem; + font-family: "commuters_sans", sans-serif; + font-size: 0.75rem; + font-weight: 300; + color: var(--blue-text); + text-transform: uppercase; + letter-spacing: 0.05em; +} + +.resultItem { + display: flex; + flex-direction: column; + padding: 0.75rem 1rem; + cursor: pointer; + transition: background-color 0.2s; + border-radius: 15px; + + &:hover { + background: var(--blue-dark); + } +} + +.resultHeader { + display: flex; + align-items: center; + width: 100%; + gap: 0.5rem; +} + +.resultTitle { + font-family: "proxima_nova", sans-serif; + font-weight: 300; + color: var(--text-primary); +} + +.resultSection { + margin-left: auto; + font-size: 0.75rem; + color: var(--aqua); +} + +.resultMatch { + margin-top: 0.25rem; + font-family: "proxima_nova", sans-serif; + font-size: 0.875rem; + font-weight: 300; + color: var(--blue-text); + overflow: hidden; + text-overflow: ellipsis; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; +} + +.matchHeading { + font-weight: 500; + color: var(--blue-text-light); +} + +.resultTag { + display: inline-flex; + margin-top: 0.5rem; + padding: 0.125rem 0.5rem; + font-size: 0.75rem; + border-radius: 9999px; + + &[data-color="aqua"] { + background: var(--aqua-dark); + color: var(--aqua); + } + + &[data-color="orange"] { + background: var(--orange-alpha-1); + color: var(--orange); + } + + &[data-color="violet"] { + background: var(--violet-alpha-1); + color: var(--violet); + } +} diff --git a/components/DocSearch/DocSearch.tsx b/components/DocSearch/DocSearch.tsx new file mode 100644 index 00000000..78a03de3 --- /dev/null +++ b/components/DocSearch/DocSearch.tsx @@ -0,0 +1,250 @@ +import React, { useState, useEffect } from "react"; +import { Search } from "lucide-react"; +import styles from "./DocSearch.module.scss"; +import Link from "next/link"; + +interface SearchResult { + label: string; + href: string; + content: string; + tabId: string; + tabLabel: string; + tabColor: string; + section: string; + score: number; + headings: { text: string; level: number }[]; + matches: { + text: string; + type: "content" | "heading"; + }[]; +} + +export const DocSearch: React.FC = () => { + const [open, setOpen] = useState(false); + const [query, setQuery] = useState(""); + const [results, setResults] = useState([]); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const [searchIndex, setSearchIndex] = useState([]); + + // Load search index + useEffect(() => { + fetch("/search-index.json") + .then((res) => res.json()) + .then(setSearchIndex) + .catch(console.error); + }, []); + + // Handle keyboard shortcut + useEffect(() => { + const down = (e: KeyboardEvent) => { + if (e.key === "k" && (e.metaKey || e.ctrlKey)) { + e.preventDefault(); + setOpen(true); + } + }; + document.addEventListener("keydown", down); + return () => document.removeEventListener("keydown", down); + }, []); + + // Handle escape key + useEffect(() => { + const handleEscape = (e: KeyboardEvent) => { + if (e.key === "Escape") { + setOpen(false); + } + }; + document.addEventListener("keydown", handleEscape); + return () => document.removeEventListener("keydown", handleEscape); + }, []); + + // Search logic with content matching + const search = (searchQuery: string) => { + if (!searchQuery) { + setResults([]); + return; + } + + const searchTerms = searchQuery.toLowerCase().split(" "); + + const results = searchIndex.map((item) => { + let score = 0; + const matches: { text: string; type: "content" | "heading" }[] = []; + + // Search in title + if ( + searchTerms.every((term) => item.label.toLowerCase().includes(term)) + ) { + score += 10; + } + + // Search in headings + item.headings.forEach((heading: { text: string; level: number }) => { + if ( + searchTerms.every((term) => heading.text.toLowerCase().includes(term)) + ) { + score += 5; + matches.push({ text: heading.text, type: "heading" }); + } + }); + + // Search in content + const contentLower = item.content.toLowerCase(); + const allTermsInContent = searchTerms.every((term) => + contentLower.includes(term) + ); + + if (allTermsInContent) { + score += 1; + + // Find the best matching content snippet + const snippetLength = 150; + let bestSnippetScore = 0; + let bestSnippet = ""; + + for (let i = 0; i < contentLower.length - snippetLength; i += 50) { + const snippet = item.content.slice(i, i + snippetLength); + const snippetLower = snippet.toLowerCase(); + let snippetScore = 0; + + searchTerms.forEach((term) => { + const count = (snippetLower.match(new RegExp(term, "g")) || []) + .length; + snippetScore += count; + }); + + if (snippetScore > bestSnippetScore) { + bestSnippetScore = snippetScore; + bestSnippet = snippet; + } + } + + if (bestSnippet) { + matches.push({ text: bestSnippet.trim(), type: "content" }); + } + } + + return { + ...item, + score, + matches, + }; + }); + + setResults( + results.filter((item) => item.score > 0).sort((a, b) => b.score - a.score) + ); + }; + + // Debounce search + useEffect(() => { + const timer = setTimeout(() => { + search(query); + }, 200); + return () => clearTimeout(timer); + }, [query, searchIndex]); + + // Group results by tab + const groupedResults = results.reduce( + (acc, item) => { + if (!acc[item.tabId]) { + acc[item.tabId] = { + label: item.tabLabel, + color: item.tabColor, + items: [], + }; + } + acc[item.tabId]?.items.push(item); + return acc; + }, + {} as Record< + string, + { label: string; color: string; items: SearchResult[] } + > + ); + + return ( + <> + + + {open && ( + <> +
setOpen(false)} + onKeyDown={(e) => { + if (e.key === "Enter" || e.key === " ") { + setOpen(false); + } + }} + role="button" + tabIndex={0} + /> +
+
+

Search Documentation

+
+ +
+ setQuery(e.target.value)} + autoFocus + /> + +
+ {results.length === 0 && query && ( +
No results found.
+ )} + + {Object.entries(groupedResults).map(([tabId, group]) => ( +
+
{group.label}
+ + {group.items.map((result) => ( + setOpen(false)} + > +
+ + {result.label} + + + {result.section} + +
+ + {result.matches.map((match, idx) => ( +
+ {match.type === "heading" ? ( + + {match.text} + + ) : ( + <>...{match.text}... + )} +
+ ))} + + ))} +
+ ))} +
+
+
+ + )} + + ); +}; + +export default DocSearch; diff --git a/components/DocsLayout/DocsLayout.tsx b/components/DocsLayout/DocsLayout.tsx index 6ce5bf6d..6d968e7f 100644 --- a/components/DocsLayout/DocsLayout.tsx +++ b/components/DocsLayout/DocsLayout.tsx @@ -11,6 +11,7 @@ import { useRouter } from "next/router"; import { useAuth } from "hooks/useAuth"; import Link from "next/link"; import { PERSON_FALLBACK_IMAGE_400_URL } from "utils/constants"; +import DocSearch from "components/DocSearch/DocSearch"; export function DocsLayout({ children, @@ -61,17 +62,9 @@ export function DocsLayout({
- {/*
-
-
- -
-
-
*/} +
+ +
{!user && } diff --git a/components/index.tsx b/components/index.tsx index aa3730b5..f751f616 100644 --- a/components/index.tsx +++ b/components/index.tsx @@ -57,10 +57,12 @@ import { RadioGroup } from "./RadioGroup/RadioGroup"; import { IndeterminateCheckbox } from "./IndeterminateCheckbox/IndeterminateCheckbox"; import { DocsLayout } from "./DocsLayout/DocsLayout"; import { OrganizationDashboardLink } from "./OrganizationDashboardLink"; +import { Modal } from "./Modal/Modal"; export type { EditVotingGuideCandidate, FlagColor, NavItem }; export { + Modal, OrganizationDashboardLink, DocsLayout, Avatar, diff --git a/package.json b/package.json index 7696c3e2..61e36f2f 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "format": "prettier --write \"**/*.+(ts|tsx)\"", "prebuild": "dotenv -c production -- graphql-codegen", "prepare": "husky install", + "build-search-index": "tsx scripts/build-search-index.ts", "test": "npx playwright test" }, "sideEffects": [ @@ -50,6 +51,7 @@ "gray-matter": "^4.0.3", "highlight.js": "^11.9.0", "little-state-machine": "^4.8.0", + "lucide-react": "^0.468.0", "next": "^14.2.4", "next-i18next": "14.0.3", "next-pwa": "^5.6.0", @@ -105,6 +107,7 @@ "stylelint": "^16.2.1", "stylelint-config-standard-scss": "^13.0.0", "ts-node": "^10.9.2", + "tsx": "^4.19.2", "typescript": "^5.4.2" }, "graphql": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2d3bfac8..712a2f9b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -101,6 +101,9 @@ importers: little-state-machine: specifier: ^4.8.0 version: 4.8.0(react@18.2.0) + lucide-react: + specifier: ^0.468.0 + version: 0.468.0(react@18.2.0) next: specifier: ^14.2.4 version: 14.2.5(@babel/core@7.24.0)(@playwright/test@1.42.1)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(sass@1.71.1) @@ -265,6 +268,9 @@ importers: ts-node: specifier: ^10.9.2 version: 10.9.2(@types/node@20.11.20)(typescript@5.4.2) + tsx: + specifier: ^4.19.2 + version: 4.19.2 typescript: specifier: ^5.4.2 version: 5.4.2 @@ -1151,138 +1157,282 @@ packages: '@emnapi/runtime@0.45.0': resolution: {integrity: sha512-Txumi3td7J4A/xTTwlssKieHKTGl3j4A1tglBx72auZ49YK7ePY6XZricgIg9mnZT4xPfA+UPCUdnhRuEFDL+w==} + '@esbuild/aix-ppc64@0.23.1': + resolution: {integrity: sha512-6VhYk1diRqrhBAqpJEdjASR/+WVRtfjpqKuNw11cLiaWpAT/Uu+nokB+UJnevzy/P9C/ty6AOe0dwueMrGh/iQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + '@esbuild/android-arm64@0.18.6': resolution: {integrity: sha512-pL0Ci8P9q1sWbtPx8CXbc8JvPvvYdJJQ+LO09PLFsbz3aYNdFBGWJjiHU+CaObO4Ames+GOFpXRAJZS2L3ZK/A==} engines: {node: '>=12'} cpu: [arm64] os: [android] + '@esbuild/android-arm64@0.23.1': + resolution: {integrity: sha512-xw50ipykXcLstLeWH7WRdQuysJqejuAGPd30vd1i5zSyKK3WE+ijzHmLKxdiCMtH1pHz78rOg0BKSYOSB/2Khw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + '@esbuild/android-arm@0.18.6': resolution: {integrity: sha512-J3lwhDSXBBppSzm/LC1uZ8yKSIpExc+5T8MxrYD9KNVZG81FOAu2VF2gXi/6A/LwDDQQ+b6DpQbYlo3VwxFepQ==} engines: {node: '>=12'} cpu: [arm] os: [android] + '@esbuild/android-arm@0.23.1': + resolution: {integrity: sha512-uz6/tEy2IFm9RYOyvKl88zdzZfwEfKZmnX9Cj1BHjeSGNuGLuMD1kR8y5bteYmwqKm1tj8m4cb/aKEorr6fHWQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + '@esbuild/android-x64@0.18.6': resolution: {integrity: sha512-hE2vZxOlJ05aY28lUpB0y0RokngtZtcUB+TVl9vnLEnY0z/8BicSvrkThg5/iI1rbf8TwXrbr2heEjl9fLf+EA==} engines: {node: '>=12'} cpu: [x64] os: [android] + '@esbuild/android-x64@0.23.1': + resolution: {integrity: sha512-nlN9B69St9BwUoB+jkyU090bru8L0NA3yFvAd7k8dNsVH8bi9a8cUAUSEcEEgTp2z3dbEDGJGfP6VUnkQnlReg==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + '@esbuild/darwin-arm64@0.18.6': resolution: {integrity: sha512-/tuyl4R+QhhoROQtuQj9E/yfJtZNdv2HKaHwYhhHGQDN1Teziem2Kh7BWQMumfiY7Lu9g5rO7scWdGE4OsQ6MQ==} engines: {node: '>=12'} cpu: [arm64] os: [darwin] + '@esbuild/darwin-arm64@0.23.1': + resolution: {integrity: sha512-YsS2e3Wtgnw7Wq53XXBLcV6JhRsEq8hkfg91ESVadIrzr9wO6jJDMZnCQbHm1Guc5t/CdDiFSSfWP58FNuvT3Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + '@esbuild/darwin-x64@0.18.6': resolution: {integrity: sha512-L7IQga2pDT+14Ti8HZwsVfbCjuKP4U213T3tuPggOzyK/p4KaUJxQFXJgfUFHKzU0zOXx8QcYRYZf0hSQtppkw==} engines: {node: '>=12'} cpu: [x64] os: [darwin] + '@esbuild/darwin-x64@0.23.1': + resolution: {integrity: sha512-aClqdgTDVPSEGgoCS8QDG37Gu8yc9lTHNAQlsztQ6ENetKEO//b8y31MMu2ZaPbn4kVsIABzVLXYLhCGekGDqw==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + '@esbuild/freebsd-arm64@0.18.6': resolution: {integrity: sha512-bq10jFv42V20Kk77NvmO+WEZaLHBKuXcvEowixnBOMkaBgS7kQaqTc77ZJDbsUpXU3KKNLQFZctfaeINmeTsZA==} engines: {node: '>=12'} cpu: [arm64] os: [freebsd] + '@esbuild/freebsd-arm64@0.23.1': + resolution: {integrity: sha512-h1k6yS8/pN/NHlMl5+v4XPfikhJulk4G+tKGFIOwURBSFzE8bixw1ebjluLOjfwtLqY0kewfjLSrO6tN2MgIhA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + '@esbuild/freebsd-x64@0.18.6': resolution: {integrity: sha512-HbDLlkDZqUMBQaiday0pJzB6/8Xx/10dI3xRebJBReOEeDSeS+7GzTtW9h8ZnfB7/wBCqvtAjGtWQLTNPbR2+g==} engines: {node: '>=12'} cpu: [x64] os: [freebsd] + '@esbuild/freebsd-x64@0.23.1': + resolution: {integrity: sha512-lK1eJeyk1ZX8UklqFd/3A60UuZ/6UVfGT2LuGo3Wp4/z7eRTRYY+0xOu2kpClP+vMTi9wKOfXi2vjUpO1Ro76g==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + '@esbuild/linux-arm64@0.18.6': resolution: {integrity: sha512-NMY9yg/88MskEZH2s4i6biz/3av+M8xY5ua4HE7CCz5DBz542cr7REe317+v7oKjnYBCijHpkzo5vU85bkXQmQ==} engines: {node: '>=12'} cpu: [arm64] os: [linux] + '@esbuild/linux-arm64@0.23.1': + resolution: {integrity: sha512-/93bf2yxencYDnItMYV/v116zff6UyTjo4EtEQjUBeGiVpMmffDNUyD9UN2zV+V3LRV3/on4xdZ26NKzn6754g==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + '@esbuild/linux-arm@0.18.6': resolution: {integrity: sha512-C+5kb6rgsGMmvIdUI7v1PPgC98A6BMv233e97aXZ5AE03iMdlILFD/20HlHrOi0x2CzbspXn9HOnlE4/Ijn5Kw==} engines: {node: '>=12'} cpu: [arm] os: [linux] + '@esbuild/linux-arm@0.23.1': + resolution: {integrity: sha512-CXXkzgn+dXAPs3WBwE+Kvnrf4WECwBdfjfeYHpMeVxWE0EceB6vhWGShs6wi0IYEqMSIzdOF1XjQ/Mkm5d7ZdQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + '@esbuild/linux-ia32@0.18.6': resolution: {integrity: sha512-AXazA0ljvQEp7cA9jscABNXsjodKbEcqPcAE3rDzKN82Vb3lYOq6INd+HOCA7hk8IegEyHW4T72Z7QGIhyCQEA==} engines: {node: '>=12'} cpu: [ia32] os: [linux] + '@esbuild/linux-ia32@0.23.1': + resolution: {integrity: sha512-VTN4EuOHwXEkXzX5nTvVY4s7E/Krz7COC8xkftbbKRYAl96vPiUssGkeMELQMOnLOJ8k3BY1+ZY52tttZnHcXQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + '@esbuild/linux-loong64@0.18.6': resolution: {integrity: sha512-JjBf7TwY7ldcPgHYt9UcrjZB03+WZqg/jSwMAfzOzM5ZG+tu5umUqzy5ugH/crGI4eoDIhSOTDp1NL3Uo/05Fw==} engines: {node: '>=12'} cpu: [loong64] os: [linux] + '@esbuild/linux-loong64@0.23.1': + resolution: {integrity: sha512-Vx09LzEoBa5zDnieH8LSMRToj7ir/Jeq0Gu6qJ/1GcBq9GkfoEAoXvLiW1U9J1qE/Y/Oyaq33w5p2ZWrNNHNEw==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + '@esbuild/linux-mips64el@0.18.6': resolution: {integrity: sha512-kATNsslryVxcH1sO3KP2nnyUWtZZVkgyhAUnyTVVa0OQQ9pmDRjTpHaE+2EQHoCM5wt/uav2edrAUqbwn3tkKQ==} engines: {node: '>=12'} cpu: [mips64el] os: [linux] + '@esbuild/linux-mips64el@0.23.1': + resolution: {integrity: sha512-nrFzzMQ7W4WRLNUOU5dlWAqa6yVeI0P78WKGUo7lg2HShq/yx+UYkeNSE0SSfSure0SqgnsxPvmAUu/vu0E+3Q==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + '@esbuild/linux-ppc64@0.18.6': resolution: {integrity: sha512-B+wTKz+8pi7mcWXFQV0LA79dJ+qhiut5uK9q0omoKnq8yRIwQJwfg3/vclXoqqcX89Ri5Y5538V0Se2v5qlcLA==} engines: {node: '>=12'} cpu: [ppc64] os: [linux] + '@esbuild/linux-ppc64@0.23.1': + resolution: {integrity: sha512-dKN8fgVqd0vUIjxuJI6P/9SSSe/mB9rvA98CSH2sJnlZ/OCZWO1DJvxj8jvKTfYUdGfcq2dDxoKaC6bHuTlgcw==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + '@esbuild/linux-riscv64@0.18.6': resolution: {integrity: sha512-h44RBLVXFUSjvhOfseE+5UxQ/r9LVeqK2S8JziJKOm9W7SePYRPDyn7MhzhNCCFPkcjIy+soCxfhlJXHXXCR0A==} engines: {node: '>=12'} cpu: [riscv64] os: [linux] + '@esbuild/linux-riscv64@0.23.1': + resolution: {integrity: sha512-5AV4Pzp80fhHL83JM6LoA6pTQVWgB1HovMBsLQ9OZWLDqVY8MVobBXNSmAJi//Csh6tcY7e7Lny2Hg1tElMjIA==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + '@esbuild/linux-s390x@0.18.6': resolution: {integrity: sha512-FlYpyr2Xc2AUePoAbc84NRV+mj7xpsISeQ36HGf9etrY5rTBEA+IU9HzWVmw5mDFtC62EQxzkLRj8h5Hq85yOQ==} engines: {node: '>=12'} cpu: [s390x] os: [linux] + '@esbuild/linux-s390x@0.23.1': + resolution: {integrity: sha512-9ygs73tuFCe6f6m/Tb+9LtYxWR4c9yg7zjt2cYkjDbDpV/xVn+68cQxMXCjUpYwEkze2RcU/rMnfIXNRFmSoDw==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + '@esbuild/linux-x64@0.18.6': resolution: {integrity: sha512-Mc4EUSYwzLci77u0Kao6ajB2WbTe5fNc7+lHwS3a+vJISC/oprwURezUYu1SdWAYoczbsyOvKAJwuNftoAdjjg==} engines: {node: '>=12'} cpu: [x64] os: [linux] + '@esbuild/linux-x64@0.23.1': + resolution: {integrity: sha512-EV6+ovTsEXCPAp58g2dD68LxoP/wK5pRvgy0J/HxPGB009omFPv3Yet0HiaqvrIrgPTBuC6wCH1LTOY91EO5hQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + '@esbuild/netbsd-x64@0.18.6': resolution: {integrity: sha512-3hgZlp7NqIM5lNG3fpdhBI5rUnPmdahraSmwAi+YX/bp7iZ7mpTv2NkypGs/XngdMtpzljICxnUG3uPfqLFd3w==} engines: {node: '>=12'} cpu: [x64] os: [netbsd] + '@esbuild/netbsd-x64@0.23.1': + resolution: {integrity: sha512-aevEkCNu7KlPRpYLjwmdcuNz6bDFiE7Z8XC4CPqExjTvrHugh28QzUXVOZtiYghciKUacNktqxdpymplil1beA==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.23.1': + resolution: {integrity: sha512-3x37szhLexNA4bXhLrCC/LImN/YtWis6WXr1VESlfVtVeoFJBRINPJ3f0a/6LV8zpikqoUg4hyXw0sFBt5Cr+Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + '@esbuild/openbsd-x64@0.18.6': resolution: {integrity: sha512-aEWTdZQHtSRROlDYn7ygB8yAqtnall/UnmoVIJVqccKitkAWVVSYocQUWrBOxLEFk8XdlRouVrLZe6WXszyviA==} engines: {node: '>=12'} cpu: [x64] os: [openbsd] + '@esbuild/openbsd-x64@0.23.1': + resolution: {integrity: sha512-aY2gMmKmPhxfU+0EdnN+XNtGbjfQgwZj43k8G3fyrDM/UdZww6xrWxmDkuz2eCZchqVeABjV5BpildOrUbBTqA==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + '@esbuild/sunos-x64@0.18.6': resolution: {integrity: sha512-uxk/5yAGpjKZUHOECtI9W+9IcLjKj+2m0qf+RG7f7eRBHr8wP6wsr3XbNbgtOD1qSpPapd6R2ZfSeXTkCcAo5g==} engines: {node: '>=12'} cpu: [x64] os: [sunos] + '@esbuild/sunos-x64@0.23.1': + resolution: {integrity: sha512-RBRT2gqEl0IKQABT4XTj78tpk9v7ehp+mazn2HbUeZl1YMdaGAQqhapjGTCe7uw7y0frDi4gS0uHzhvpFuI1sA==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + '@esbuild/win32-arm64@0.18.6': resolution: {integrity: sha512-oXlXGS9zvNCGoAT/tLHAsFKrIKye1JaIIP0anCdpaI+Dc10ftaNZcqfLzEwyhdzFAYInXYH4V7kEdH4hPyo9GA==} engines: {node: '>=12'} cpu: [arm64] os: [win32] + '@esbuild/win32-arm64@0.23.1': + resolution: {integrity: sha512-4O+gPR5rEBe2FpKOVyiJ7wNDPA8nGzDuJ6gN4okSA1gEOYZ67N8JPk58tkWtdtPeLz7lBnY6I5L3jdsr3S+A6A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + '@esbuild/win32-ia32@0.18.6': resolution: {integrity: sha512-qh7IcAHUvvmMBmoIG+V+BbE9ZWSR0ohF51e5g8JZvU08kZF58uDFL5tHs0eoYz31H6Finv17te3W3QB042GqVA==} engines: {node: '>=12'} cpu: [ia32] os: [win32] + '@esbuild/win32-ia32@0.23.1': + resolution: {integrity: sha512-BcaL0Vn6QwCwre3Y717nVHZbAa4UBEigzFm6VdsVdT/MbZ38xoj1X9HPkZhbmaBGUD1W8vxAfffbDe8bA6AKnQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + '@esbuild/win32-x64@0.18.6': resolution: {integrity: sha512-9UDwkz7Wlm4N9jnv+4NL7F8vxLhSZfEkRArz2gD33HesAFfMLGIGNVXRoIHtWNw8feKsnGly9Hq1EUuRkWl0zA==} engines: {node: '>=12'} cpu: [x64] os: [win32] + '@esbuild/win32-x64@0.23.1': + resolution: {integrity: sha512-BHpFFeslkWrXWyUPnbKm+xYYVYruCinGcftSBaa8zoF9hZO4BcSCFUvHVTtzpIY6YzUnYtuEhZ+C9iEXjxnasg==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + '@eslint-community/eslint-utils@4.4.0': resolution: {integrity: sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -3949,6 +4099,11 @@ packages: engines: {node: '>=12'} hasBin: true + esbuild@0.23.1: + resolution: {integrity: sha512-VVNz/9Sa0bs5SELtn3f7qhJCDPCF5oMEl5cO9/SSinpE9hbPVvxbd572HH5AKiP7WD8INO53GgfDDhRjkylHEg==} + engines: {node: '>=18'} + hasBin: true + escalade@3.1.2: resolution: {integrity: sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==} engines: {node: '>=6'} @@ -4360,6 +4515,9 @@ packages: get-tsconfig@4.7.3: resolution: {integrity: sha512-ZvkrzoUA0PQZM6fy6+/Hce561s+faD1rsNwhnO5FelNjyy7EMGJ3Rz1AQ8GYDWjhRs/7dBLOEJvhK8MiEJOAFg==} + get-tsconfig@4.8.1: + resolution: {integrity: sha512-k9PN+cFBmaLWtVz29SkUoqU5O0slLuHJXt/2P+tMVFT+phsSGXGkp9t3rQIqdz0e+06EHNGs3oM6ZX1s2zHxRg==} + glob-parent@5.1.2: resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} engines: {node: '>= 6'} @@ -5103,6 +5261,11 @@ packages: resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} engines: {node: '>=10'} + lucide-react@0.468.0: + resolution: {integrity: sha512-6koYRhnM2N0GGZIdXzSeiNwguv1gt/FAjZOiPl76roBi3xKEXa4WmfpxgQwTTL4KipXjefrnf3oV4IsYhi4JFA==} + peerDependencies: + react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0-rc + magic-string@0.25.9: resolution: {integrity: sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==} @@ -6572,6 +6735,11 @@ packages: tslib@2.6.2: resolution: {integrity: sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==} + tsx@4.19.2: + resolution: {integrity: sha512-pOUl6Vo2LUq/bSa8S5q7b91cgNSjctn9ugq/+Mvow99qW6x/UZYwzxy/3NmqoT66eHYfCVvFvACC58UBPFf28g==} + engines: {node: '>=18.0.0'} + hasBin: true + type-check@0.4.0: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} engines: {node: '>= 0.8.0'} @@ -8587,72 +8755,144 @@ snapshots: tslib: 2.6.2 optional: true + '@esbuild/aix-ppc64@0.23.1': + optional: true + '@esbuild/android-arm64@0.18.6': optional: true + '@esbuild/android-arm64@0.23.1': + optional: true + '@esbuild/android-arm@0.18.6': optional: true + '@esbuild/android-arm@0.23.1': + optional: true + '@esbuild/android-x64@0.18.6': optional: true + '@esbuild/android-x64@0.23.1': + optional: true + '@esbuild/darwin-arm64@0.18.6': optional: true + '@esbuild/darwin-arm64@0.23.1': + optional: true + '@esbuild/darwin-x64@0.18.6': optional: true + '@esbuild/darwin-x64@0.23.1': + optional: true + '@esbuild/freebsd-arm64@0.18.6': optional: true + '@esbuild/freebsd-arm64@0.23.1': + optional: true + '@esbuild/freebsd-x64@0.18.6': optional: true + '@esbuild/freebsd-x64@0.23.1': + optional: true + '@esbuild/linux-arm64@0.18.6': optional: true + '@esbuild/linux-arm64@0.23.1': + optional: true + '@esbuild/linux-arm@0.18.6': optional: true + '@esbuild/linux-arm@0.23.1': + optional: true + '@esbuild/linux-ia32@0.18.6': optional: true + '@esbuild/linux-ia32@0.23.1': + optional: true + '@esbuild/linux-loong64@0.18.6': optional: true + '@esbuild/linux-loong64@0.23.1': + optional: true + '@esbuild/linux-mips64el@0.18.6': optional: true + '@esbuild/linux-mips64el@0.23.1': + optional: true + '@esbuild/linux-ppc64@0.18.6': optional: true + '@esbuild/linux-ppc64@0.23.1': + optional: true + '@esbuild/linux-riscv64@0.18.6': optional: true + '@esbuild/linux-riscv64@0.23.1': + optional: true + '@esbuild/linux-s390x@0.18.6': optional: true + '@esbuild/linux-s390x@0.23.1': + optional: true + '@esbuild/linux-x64@0.18.6': optional: true + '@esbuild/linux-x64@0.23.1': + optional: true + '@esbuild/netbsd-x64@0.18.6': optional: true + '@esbuild/netbsd-x64@0.23.1': + optional: true + + '@esbuild/openbsd-arm64@0.23.1': + optional: true + '@esbuild/openbsd-x64@0.18.6': optional: true + '@esbuild/openbsd-x64@0.23.1': + optional: true + '@esbuild/sunos-x64@0.18.6': optional: true + '@esbuild/sunos-x64@0.23.1': + optional: true + '@esbuild/win32-arm64@0.18.6': optional: true + '@esbuild/win32-arm64@0.23.1': + optional: true + '@esbuild/win32-ia32@0.18.6': optional: true + '@esbuild/win32-ia32@0.23.1': + optional: true + '@esbuild/win32-x64@0.18.6': optional: true + '@esbuild/win32-x64@0.23.1': + optional: true + '@eslint-community/eslint-utils@4.4.0(eslint@8.57.0)': dependencies: eslint: 8.57.0 @@ -11946,6 +12186,33 @@ snapshots: '@esbuild/win32-ia32': 0.18.6 '@esbuild/win32-x64': 0.18.6 + esbuild@0.23.1: + optionalDependencies: + '@esbuild/aix-ppc64': 0.23.1 + '@esbuild/android-arm': 0.23.1 + '@esbuild/android-arm64': 0.23.1 + '@esbuild/android-x64': 0.23.1 + '@esbuild/darwin-arm64': 0.23.1 + '@esbuild/darwin-x64': 0.23.1 + '@esbuild/freebsd-arm64': 0.23.1 + '@esbuild/freebsd-x64': 0.23.1 + '@esbuild/linux-arm': 0.23.1 + '@esbuild/linux-arm64': 0.23.1 + '@esbuild/linux-ia32': 0.23.1 + '@esbuild/linux-loong64': 0.23.1 + '@esbuild/linux-mips64el': 0.23.1 + '@esbuild/linux-ppc64': 0.23.1 + '@esbuild/linux-riscv64': 0.23.1 + '@esbuild/linux-s390x': 0.23.1 + '@esbuild/linux-x64': 0.23.1 + '@esbuild/netbsd-x64': 0.23.1 + '@esbuild/openbsd-arm64': 0.23.1 + '@esbuild/openbsd-x64': 0.23.1 + '@esbuild/sunos-x64': 0.23.1 + '@esbuild/win32-arm64': 0.23.1 + '@esbuild/win32-ia32': 0.23.1 + '@esbuild/win32-x64': 0.23.1 + escalade@3.1.2: {} escape-html@1.0.3: {} @@ -12479,6 +12746,10 @@ snapshots: dependencies: resolve-pkg-maps: 1.0.0 + get-tsconfig@4.8.1: + dependencies: + resolve-pkg-maps: 1.0.0 + glob-parent@5.1.2: dependencies: is-glob: 4.0.3 @@ -13294,6 +13565,10 @@ snapshots: dependencies: yallist: 4.0.0 + lucide-react@0.468.0(react@18.2.0): + dependencies: + react: 18.2.0 + magic-string@0.25.9: dependencies: sourcemap-codec: 1.4.8 @@ -15112,6 +15387,13 @@ snapshots: tslib@2.6.2: {} + tsx@4.19.2: + dependencies: + esbuild: 0.23.1 + get-tsconfig: 4.8.1 + optionalDependencies: + fsevents: 2.3.3 + type-check@0.4.0: dependencies: prelude-ls: 1.2.1 diff --git a/public/search-index.json b/public/search-index.json new file mode 100644 index 00000000..0c3e495c --- /dev/null +++ b/public/search-index.json @@ -0,0 +1,220 @@ +[ + { + "label": "What is Populist?", + "href": "/docs/content/overview", + "content": "import { DocsLayout } from \"components\";\n\n## Populist helps you create civic content\n\n### What is Civic Content?\n\nCivic content is information that helps people understand and engage\nwith their communities. It can be news, events, or discussions about\nlocal issues. Civic content is important because it helps people make\ninformed decisions and participate in their communities.\n\nexport default function DocsContentOverview({ children }) {\n return {children};\n}\n", + "headings": [ + { + "text": "Populist helps you create civic content", + "level": 2 + }, + { + "text": "What is Civic Content?", + "level": 3 + } + ], + "tabId": "content", + "tabLabel": "Embed Content", + "tabColor": "aqua", + "section": "Overview" + }, + { + "label": "What is a conversation?", + "href": "/docs/conversations/overview", + "content": "import { DocsLayout } from \"components\";\n\n## What is a Conversation?\n\nPopulist's **Conversations** is an open-source platform that helps large groups share and understand their collective views. It is an implementation of a [Wikisurvey](https://en.wikipedia.org/wiki/Wiki_survey) and inspired by other platforms including [Pol.is](https://pol.is) with a few key improvements.\n\nWhen using Populist conversations, people write short comments and then vote on other people's comments by selecting support, oppose, or neutral. The system shows comments to voters in a semi-random way to ensure broad exposure of ideas.\n\nWhat makes this different from regular surveys:\n\n- Participants create the content themselves instead of answering preset questions\n- People can contribute meaningfully without having to vote on everything\n- The system works efficiently even with hundreds, thousands, or potentially millions of participants\n\n#### Participating in Conversations\n\nBrowse through the list of existing conversations to find topics that interest you. Click on a conversation to view the discussion and contribute your thoughts and opinions.\n\n#### Managing Conversations\n\nOrganization administrators have the ability to manage conversations, including editing or deleting conversations as needed. This ensures that discussions remain relevant and appropriate for the organization.\n\nexport default function DocsConversationsOverview({ children }) {\n return {children};\n}\n", + "headings": [ + { + "text": "What is a Conversation?", + "level": 2 + }, + { + "text": "Participating in Conversations", + "level": 4 + }, + { + "text": "Managing Conversations", + "level": 4 + } + ], + "tabId": "conversations", + "tabLabel": "Conversations", + "tabColor": "orange", + "section": "Overview" + }, + { + "label": "Quickstart", + "href": "/docs/conversations/quickstart", + "content": "import { ReactNode } from \"react\";\nimport { DocsLayout, OrganizationDashboardLink } from \"components\";\n\n# Quickstart\n\n1. Access the conversations feature by navigating to conversations on your .\n\n2. Click the \"Create New Conversation\" button to create a new conversation.\n\n3. Set a topic and optional description for the conversation.\n\n4. Optionally add seed statements to provide initial context or ideas for the discussion.\n\n5. Click on \"Manage\" then \"Copy Public URL\" to distribute a link to the conversation.\n\n6. From the \"Manage\" tab you'll be able to monitor the conversation in realtime.\n\n7. Use the \"Moderate\" tab to manage comments and seed statements.\n\nexport default function DocsConversationsQuickstart({ children }) {\n return {children};\n}\n", + "headings": [ + { + "text": "Quickstart", + "level": 1 + } + ], + "tabId": "conversations", + "tabLabel": "Conversations", + "tabColor": "orange", + "section": "Overview" + }, + { + "label": "Configuration", + "href": "/docs/conversations/configuration", + "content": "import { DocsLayout } from \"components\";\n\n## Configuring a Conversation\n\nThe conversation owner can configure the conversation settings to suit their needs. The following settings are available:\n\n#### Conversation Title\n\nThe title of the conversation is displayed at the top of the conversation page. Choose a title that is descriptive and engaging to attract participants.\n\n#### Conversation Description\n\nThe description provides additional context for the conversation. Use the description to explain the purpose of the conversation and provide guidelines for participants.\n\n#### Seed Statements\n\nSeed statements are initial statements or questions that provide context for the conversation. Add seed statements to prompt participants to engage with the conversation.\n\nexport default function DocsConversationsConfiguration({ children }) {\n return {children};\n}\n", + "headings": [ + { + "text": "Configuring a Conversation", + "level": 2 + }, + { + "text": "Conversation Title", + "level": 4 + }, + { + "text": "Conversation Description", + "level": 4 + }, + { + "text": "Seed Statements", + "level": 4 + } + ], + "tabId": "conversations", + "tabLabel": "Conversations", + "tabColor": "orange", + "section": "Usage" + }, + { + "label": "Distribution", + "href": "/docs/conversations/distribution", + "content": "import { DocsLayout, Divider } from \"components\";\n\n## Distributing a Conversation\n\nThe conversation owner can distribute the conversation to participants using the following methods:\n\n#### Direct Link\n\nShare the conversation link with participants via email, social media, or other communication channels. Participants can access the conversation by clicking the link.\n\n#### Embed Code (Coming Soon)\n\nEmbed the conversation on your website using the provided embed code. Participants can engage with the conversation directly on your website.\n\n\n\n## Marketing\n\nPromote the conversation to attract participants and increase engagement. Consider the following marketing strategies:\n\n#### Email list\n\nSend an email to your subscribers inviting them to participate in the conversation. Include a brief description of the conversation and a call-to-action to join.\n\n#### Social media\n\nShare the conversation link on your social media channels to reach a wider audience. Use engaging visuals and captions to attract participants.\n\n#### Paid Ads\n\nConsider running paid ads to promote the conversation to a targeted audience. Use platforms like Google Ads or Facebook Ads to reach potential participants.\n\nexport default function DocsConversationsDistribution({ children }) {\n return {children};\n}\n", + "headings": [ + { + "text": "Distributing a Conversation", + "level": 2 + }, + { + "text": "Direct Link", + "level": 4 + }, + { + "text": "Embed Code (Coming Soon)", + "level": 4 + }, + { + "text": "Marketing", + "level": 2 + }, + { + "text": "Email list", + "level": 4 + }, + { + "text": "Social media", + "level": 4 + }, + { + "text": "Paid Ads", + "level": 4 + } + ], + "tabId": "conversations", + "tabLabel": "Conversations", + "tabColor": "orange", + "section": "Usage" + }, + { + "label": "Moderation", + "href": "/docs/conversations/moderation", + "content": "import { DocsLayout } from \"components\";\n\n## Moderation\n\nThe conversation owner controls moderation through the admin interface.\nEach new comment can be approved, rejected, or left unmoderated. While\nConversations work without moderation, moderating effectively saves\nparticipants' time by removing irrelevant content.\n\n## Moderation Schemes\n\nStrict Moderation (Default) Works like a whitelist Comments only appear\nafter moderator approval. Recommended when embedding on your website to\nprevent inappropriate content. Permissive Moderation Works like a\nblacklist Comments appear immediately Can be removed later by moderators\nYou can switch between these in the conversation's admin configuration.\n\nexport default function DocsConversationsModeration({ children }) {\n return {children};\n}\n", + "headings": [ + { + "text": "Moderation", + "level": 2 + }, + { + "text": "Moderation Schemes", + "level": 2 + } + ], + "tabId": "conversations", + "tabLabel": "Conversations", + "tabColor": "orange", + "section": "Usage" + }, + { + "label": "Monitoring", + "href": "/docs/conversations/monitoring", + "content": "import { DocsLayout } from \"components\";\n\n## Monitoring\n\nYou can monitor a live conversation in real time using a conversations\n\"Manage\" page. Here you can see the number of participants, votes,\nstatements, and more.\n\nexport default function DocsConversationsMonitoring({ children }) {\n return {children};\n}\n", + "headings": [ + { + "text": "Monitoring", + "level": 2 + } + ], + "tabId": "conversations", + "tabLabel": "Conversations", + "tabColor": "orange", + "section": "Usage" + }, + { + "label": "Introduction", + "href": "/docs/api/introduction", + "content": "import { Divider, DocsLayout, CodeBlock } from \"components\";\n\n# API Reference\n\n\n## Introducing: The Populist public API\n\nThe Populist public API provides access to the Populist platform's\nfeatures and data. You can use the API to consume data about elections,\npoliticians, bills and more.\n\n### GraphQL\n\nThe Populist public API is built on GraphQL, a query language for your\nAPI. You can use GraphQL to query the Populist platform for the specific\ndata you need.\n\nA query to the Populist API could looks something like this:\n\n\n\nexport default function DocsApiIntroduction({ children }) {\n return {children};\n}\n", + "headings": [ + { + "text": "API Reference", + "level": 1 + }, + { + "text": "Introducing: The Populist public API", + "level": 2 + }, + { + "text": "GraphQL", + "level": 3 + } + ], + "tabId": "api", + "tabLabel": "API Reference", + "tabColor": "violet", + "section": "Overview" + }, + { + "label": "Quickstart", + "href": "/docs/api/quickstart", + "content": "import { Divider, DocsLayout, CodeBlock } from \"components\";\nimport styles from \"components/DocsLayout/DocsLayout.module.scss\";\n\n# API Reference\n\n\n## Quickstart\n\nMaking a request to the Populist API is easy! To get started quickly, copy this snippet into your terminal and run it:\n\n\n\nThis will return a list of upcoming elections in Colorado. You can modify the `filter` object to query elections in other states or filter by other fields. For more information on the available fields and filters, check out the [API Reference](/docs/api/reference). Your output should looks something like this:\n\n
\n  {`{\n  \"data\": {\n    \"elections\": [\n      {\n        \"title\": \"Colorado Primaries\",\n        \"description\": \"Primary races for the general election later this year on November 8, 2022.\",\n        \"electionDate\": \"2022-06-28\"\n      },\n      {\n        \"title\": \"General Election\",\n        \"description\": \"During this midterm election year, all 435 seats in the U.S. House of Representatives and 35 of the 100 seats in the U.S. Senate will be contested. This will be the first election affected by the redistricting following the 2020 census.\",\n        \"electionDate\": \"2022-11-08\"\n      },\n      {\n        \"title\": \"Municipal General Election\",\n        \"description\": \"\",\n        \"electionDate\": \"2023-04-04\"\n      },\n      {\n        \"title\": \"General Runoff Election\",\n        \"description\": \"Michael Johnston defeated Kelly Brough in the general runoff election for Mayor of Denver on June 6, 2023.\",\n        \"electionDate\": \"2023-06-06\"\n      },\n      {\n        \"title\": \"Primary & Special Elections\",\n        \"description\": null,\n        \"electionDate\": \"2023-08-08\"\n      },\n      {\n        \"title\": \"General Election\",\n        \"description\": \"This off-year election includes mostly local elections, with a few state level ones.\",\n        \"electionDate\": \"2023-11-07\"\n      },\n      {\n        \"title\": \"Colorado Primaries 2024\",\n        \"description\": \"Primary races in Colorado for the upcoming general election on November 5, 2024\",\n        \"electionDate\": \"2024-06-25\"\n      },\n      {\n        \"title\": \"General Election 2024\",\n        \"description\": \"This year's election includes the presidential election as well as all 435 seats in the U.S. House of Representatives and 33 seats in the U.S. Senate.\",\n        \"electionDate\": \"2024-11-05\"\n      }\n    ]\n  }\n}`}\n
\n\nexport default function DocsGuidesQuickStart({ children }) {\n return {children};\n}\n", + "headings": [ + { + "text": "API Reference", + "level": 1 + }, + { + "text": "Quickstart", + "level": 2 + } + ], + "tabId": "api", + "tabLabel": "API Reference", + "tabColor": "violet", + "section": "Overview" + }, + { + "label": "Authentication", + "href": "/docs/api/auth", + "content": "import { Divider } from \"components\";\nimport { DocsLayout } from \"components\";\n\n# API Reference\n\n\n## Authentication\n\nThe Populist public API requires an `authorization` header to be set with your request using your API\nkey like so:\n\n```json\n\"authorization\": \"Bearer YOUR_API_KEY\"\n```\n\nIf you do not have an API key, you can request one by contacting us at\n[info@populist.us](mailto:info@populist.us)\n\nexport default function DocsApiAuth({ children }) {\n return {children};\n}\n", + "headings": [ + { + "text": "API Reference", + "level": 1 + }, + { + "text": "Authentication", + "level": 2 + } + ], + "tabId": "api", + "tabLabel": "API Reference", + "tabColor": "violet", + "section": "Overview" + } +] \ No newline at end of file diff --git a/scripts/build-search-index.ts b/scripts/build-search-index.ts new file mode 100644 index 00000000..efe01a43 --- /dev/null +++ b/scripts/build-search-index.ts @@ -0,0 +1,89 @@ +import fs from "fs/promises"; +import path from "path"; +import matter from "gray-matter"; +import { navigationConfig } from "utils/navigationConfig"; + +interface SearchIndexItem { + label: string; + href: string; + content: string; + tabId: string; + tabLabel: string; + tabColor: string; + section: string; + headings: { text: string; level: number }[]; +} + +async function readMDXContent(filePath: string): Promise<{ + content: string; + headings: { text: string; level: number }[]; +}> { + const content = await fs.readFile(filePath, "utf-8"); + const { content: mdxContent } = matter(content); + + // Extract headings from MDX content + const headingRegex = /^#{1,6}\s+(.+)$/gm; + const headings: { text: string; level: number }[] = []; + let match; + + while ((match = headingRegex.exec(mdxContent)) !== null) { + const level = match[0].indexOf(" "); + headings.push({ + text: match[1] as string, + level, + }); + } + + return { + content: mdxContent, + headings, + }; +} + +async function buildSearchIndex() { + const searchIndex: SearchIndexItem[] = []; + const docsDir = path.join(process.cwd(), "pages/docs"); + + // Process each tab in the navigation config + for (const tab of navigationConfig.tabs) { + const tabContent = navigationConfig[tab.id]; + + if (tabContent?.sections) { + for (const section of tabContent.sections) { + for (const item of section.items) { + // Convert href to filesystem path + const mdxPath = path.join( + docsDir, + item.href.replace("/docs/", "") + ".mdx" + ); + + try { + const { content, headings } = await readMDXContent(mdxPath); + + searchIndex.push({ + ...item, + content, + headings, + tabId: tab.id, + tabLabel: tab.label, + tabColor: tab.color, + section: section.title, + }); + } catch (error) { + console.warn(`Warning: Could not read MDX file for ${item.href}`); + } + } + } + } + } + + // Write the search index to a JSON file + await fs.writeFile( + "public/search-index.json", + JSON.stringify(searchIndex, null, 2) + ); + + console.log(`Built search index with ${searchIndex.length} pages`); +} + +buildSearchIndex().catch(console.error); diff --git a/tsconfig.json b/tsconfig.json index eae22c1d..1e82feec 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -27,6 +27,10 @@ "pages/elections/browse/[state]", "pages/dashboard/[dashboardSlug]/embeds/candidate-guide/[id]", "deploy.mjs" -, "pages/docs/api/auth.mdx", "pages/docs/api/auth.mdx" ], - "exclude": ["node_modules", "./scripts/widget-client.ts"] + ], + "exclude": [ + "node_modules", + "./scripts/widget-client.ts", + "./scripts/build-search-index.ts" + ] }