[ENG-303] Search on Docs (#1735)

* implement search

* fix pnpm lock

* indexer action and added some alerts

* test action

* oops

* styles pls

* disable input ring
This commit is contained in:
Utku 2023-11-05 23:15:24 +03:00 committed by GitHub
parent e4cfc30915
commit b9707db139
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 282 additions and 174 deletions

24
.github/workflows/search-index.yml vendored Normal file
View file

@ -0,0 +1,24 @@
name: Trigger Algolia Crawler
on:
workflow_dispatch:
push:
paths:
- 'docs/**'
branches:
- main
pull_request:
paths:
- '.github/workflows/search-index.yml'
jobs:
trigger_crawler:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Trigger Algolia Crawler
run: |
curl -X POST --user ${{secrets.CRAWLER_USER_ID}}:${{secrets.CRAWLER_API_KEY}} "https://crawler.algolia.com/api/1/crawlers/${{secrets.CRAWLER_ID}}/reindex"

View file

@ -10,6 +10,7 @@
"typecheck": "contentlayer build && tsc -b"
},
"dependencies": {
"@docsearch/react": "3",
"@octokit/webhooks": "^12.0.3",
"@phosphor-icons/react": "^2.0.13",
"@react-three/drei": "^9.88.4",

View file

@ -2,6 +2,7 @@
import { CaretRight } from '@phosphor-icons/react';
import { useParams } from 'next/navigation';
import { Fragment } from 'react';
import { toTitleCase } from '~/utils/util';
export function Breadcrumbs() {
@ -11,10 +12,10 @@ export function Breadcrumbs() {
return (
<div className="flex flex-row items-center gap-1">
{slug.map((item, index) => (
<>
<Fragment key={index}>
{index > 0 && <CaretRight className="h-4 w-4" />}
<span className="px-1 text-sm">{toTitleCase(item)}</span>
</>
</Fragment>
))}
</div>
);

View file

@ -23,7 +23,7 @@ export function MobileSidebarProvider({ children }: PropsWithChildren) {
return <MenuContext.Provider value={{ open, setOpen }}>{children}</MenuContext.Provider>;
}
function useMenuContext() {
export function useMenuContext() {
const ctx = useContext(MenuContext);
if (!ctx) throw new Error('useMenuContext must be used within a MenuProvider');

View file

@ -1,14 +1,24 @@
'use client';
import { SearchInput } from '@sd/ui';
import { DocSearch } from '@docsearch/react';
import { useMenuContext } from './MobileSidebar';
export function SearchBar() {
const menu = useMenuContext();
return (
<div onClick={() => alert('Search coming soon...')} className="mb-5">
<SearchInput
placeholder="Search..."
disabled
right={<span className="pr-2 text-xs font-semibold text-gray-400">K</span>}
<div
className="mb-5"
onClick={() => {
menu.open && menu.setOpen(false);
}}
>
<DocSearch
appId="O2QT1W1OHH"
apiKey="765d32dcfd1971b2b21cea6cc343e259"
indexName="spacedrivedocs"
placeholder="Search"
/>
</div>
);

View file

@ -8,6 +8,9 @@ import {
} from './Sidebar/MobileSidebar';
import 'katex/dist/katex.min.css';
import '@docsearch/css';
// This must be imported after the docsearch css
import '~/styles/search.scss';
import { Breadcrumbs } from './Breadcrumbs';

View file

@ -1,66 +0,0 @@
import { CaretRight, List, X } from '@phosphor-icons/react/dist/ssr';
import { PropsWithChildren, useState } from 'react';
import { slide as Menu } from 'react-burger-menu';
import { Button } from '@sd/ui';
import { DocsNavigation } from '~/utils/contentlayer';
import { toTitleCase } from '~/utils/util';
import DocsSidebar from './DocsSidebar';
type DocsLayoutProps = {
docUrl?: string;
navigation: DocsNavigation;
};
export default function DocsLayout(props: PropsWithChildren<DocsLayoutProps>) {
const [menuOpen, setMenuOpen] = useState(false);
return (
<div className="flex w-full flex-col items-start sm:flex-row">
<Menu
onClose={() => setMenuOpen(false)}
customBurgerIcon={false}
isOpen={menuOpen}
pageWrapId="page-container"
className="shadow-2xl shadow-black"
>
<div className="custom-scroll doc-sidebar-scroll visible h-screen overflow-x-hidden bg-gray-650 px-7 pb-20 pt-7 sm:invisible">
<Button
onClick={() => setMenuOpen(!menuOpen)}
className="-ml-0.5 mb-3 !border-none !px-1"
>
<X weight="bold" className="h-6 w-6" />
</Button>
<DocsSidebar activePath={props.docUrl} navigation={props.navigation} />
</div>
</Menu>
<aside className="sticky top-32 mb-20 ml-2 mr-0 mt-32 hidden px-5 sm:inline lg:mr-4">
<DocsSidebar activePath={props.docUrl} navigation={props.navigation} />
</aside>
<div className="flex w-full flex-col sm:flex-row" id="page-container">
<div className="mt-[65px] flex h-12 w-full items-center border-y border-gray-600 px-5 sm:hidden">
<div className="flex sm:hidden">
<Button
onClick={() => setMenuOpen(!menuOpen)}
className="ml-1 !border-none !px-2"
>
<List weight="bold" className="h-6 w-6" />
</Button>
</div>
{props.docUrl?.split('/').map((item, index) => {
if (index === 2) return null;
return (
<div key={index} className="ml-2 flex flex-row items-center">
<span className="px-1 text-sm">{toTitleCase(item)}</span>
{index < 1 && <CaretRight className="-mr-2 ml-1 h-4 w-4" />}
</div>
);
})}
</div>
<div className="mx-4 overflow-x-hidden sm:mx-auto">{props.children}</div>
<div className="w-0 sm:w-32 lg:w-64" />
</div>
</div>
);
}

View file

@ -1,89 +0,0 @@
import clsx from 'clsx';
import Link from 'next/link';
import { SearchInput } from '@sd/ui';
import { DocsNavigation, iconConfig } from '~/utils/contentlayer';
import { toTitleCase } from '~/utils/util';
interface DocsSidebarProps {
navigation: DocsNavigation;
activePath?: string;
}
export default function DocsSidebar(props: DocsSidebarProps) {
const activeSection = props.activePath?.split('/')[2] || props.navigation[0]?.slug;
const activeSectionData = props.navigation.find((section) => section.slug === activeSection);
return (
<nav className="mr-8 flex w-full flex-col sm:w-52">
<div onClick={() => alert('Search coming soon...')} className="mb-5">
<SearchInput
placeholder="Search..."
disabled
right={<span className="pr-2 text-xs font-semibold text-gray-400">K</span>}
/>
</div>
<div className="mb-6 flex flex-col">
{props.navigation.map((section) => {
const isActive = section.slug === activeSection;
const Icon = iconConfig[section.slug];
return (
<Link
// Use the first page in the section as the link
href={section.categories[0]?.docs[0]?.url}
key={section.slug}
className={clsx(
`doc-sidebar-button flex items-center py-1.5 text-[14px] font-semibold`,
section.slug,
isActive && 'nav-active'
)}
>
<div
className={clsx(
`mr-4 rounded-lg border-t border-gray-400/20 bg-gray-500 p-1`
)}
>
<Icon weight="bold" className="h-4 w-4 text-white opacity-80" />
</div>
{toTitleCase(section.slug)}
</Link>
);
})}
</div>
{activeSectionData?.categories.map((category) => {
return (
<div className="mb-5" key={category.title}>
<h2 className="font-semibold no-underline">{category.title}</h2>
<ul className="mt-3">
{category.docs.map((doc) => {
const active = props.activePath === doc.url;
return (
<li
className={clsx(
'flex border-l border-gray-600',
active && 'border-l-2 border-primary'
)}
key={doc.title}
>
<Link
href={doc.url}
className={clsx(
'w-full rounded px-3 py-1 text-[14px] font-normal text-gray-350 no-underline hover:text-gray-50',
active && '!font-medium !text-white '
)}
>
{doc.title}
</Link>
{/* this fixes the links no joke */}
{active && <div />}
</li>
);
})}
</ul>
</div>
);
})}
</nav>
);
}

View file

@ -0,0 +1,71 @@
@tailwind utilities;
// Set docsearch to dark mode and set primary color to our primary color
:root {
--docsearch-primary-color: rgb(37 153 255);
--docsearch-text-color: rgb(245, 246, 247);
--docsearch-container-background: rgba(9, 10, 17, 0.8);
--docsearch-modal-background: rgb(21, 23, 42);
--docsearch-modal-shadow: inset 1px 1px 0 0 rgb(44, 46, 64), 0 3px 8px 0 rgb(0, 3, 9);
--docsearch-searchbox-background: rgb(9, 10, 17);
--docsearch-searchbox-focus-background: #000;
--docsearch-hit-color: rgb(190, 195, 201);
--docsearch-hit-shadow: none;
--docsearch-hit-background: rgb(9, 10, 17);
--docsearch-key-gradient: linear-gradient(-26.5deg, rgb(86, 88, 114) 0%, rgb(49, 53, 91) 100%);
--docsearch-key-shadow: inset 0 -2px 0 0 rgb(40, 45, 85), inset 0 0 1px 1px rgb(81, 87, 125),
0 2px 2px 0 rgba(3, 4, 9, 0.3);
--docsearch-key-pressed-shadow: inset 0 -2px 0 0 #282d55, inset 0 0 1px 1px #51577d,
0 1px 1px 0 #0304094d;
--docsearch-footer-background: rgb(30, 33, 54);
--docsearch-footer-shadow: inset 0 1px 0 0 rgba(73, 76, 106, 0.5), 0 -4px 8px 0 rgba(0, 0, 0, 0.2);
--docsearch-logo-color: rgb(255, 255, 255);
--docsearch-muted-color: rgb(127, 132, 151);
}
.DocSearch-Button {
@apply m-0 flex h-[34px] w-full rounded-md border border-app-line bg-app-input text-sm leading-4 shadow-sm outline-none;
}
.DocSearch-Button:hover,
.DocSearch-Button:focus,
.DocSearch-Button:active {
background: none;
box-shadow: none;
@apply border-primary bg-app-hover text-ink-faint;
}
.DocSearch-Button-Key--pressed {
transform: none;
}
.DocSearch-Button .DocSearch-Search-Icon {
@apply mr-2 py-0.5 text-gray-350;
}
.DocSearch-Button-Placeholder {
@apply flex-1 border-none bg-transparent px-3 pl-0 pr-0 text-sm text-ink-faint outline-none;
}
.DocSearch-Search-Icon {
@apply h-5 w-5;
}
.DocSearch-Button-Keys {
min-width: auto;
}
.DocSearch-Button-Key {
background: none;
@apply top-0 w-5 bg-app-button p-0 text-sm text-ink shadow;
}
@media (max-width: 768px) {
.DocSearch-Button-Placeholder {
display: block;
}
}
.DocSearch-Input:focus {
@apply ring-0;
}

View file

@ -2,7 +2,7 @@ import { EjectSimple } from '@phosphor-icons/react';
import clsx from 'clsx';
import { useMemo, useState } from 'react';
import { useBridgeQuery, useLibraryQuery } from '@sd/client';
import { Button, tw } from '@sd/ui';
import { Button, toast, tw } from '@sd/ui';
import { Icon, IconName } from '~/components';
import { usePlatform } from '~/util/Platform';
@ -12,8 +12,13 @@ import SeeMore from './SeeMore';
const Name = tw.span`truncate`;
// TODO: This eject button does nothing!
const EjectButton = ({ className }: { className?: string }) => (
<Button className={clsx('absolute right-[2px] !p-[5px]', className)} variant="subtle">
<Button
className={clsx('absolute right-[2px] !p-[5px]', className)}
variant="subtle"
onClick={() => toast.info('Eject button coming soon')}
>
<EjectSimple weight="fill" size={18} className="h-3 w-3 opacity-70" />
</Button>
);

View file

@ -1,4 +1,3 @@
import { EjectSimple } from '@phosphor-icons/react';
import clsx from 'clsx';
import { useEffect, useState } from 'react';
import { Link, NavLink } from 'react-router-dom';
@ -41,12 +40,6 @@ type TriggeredContextItem =
tagId: number;
};
const EjectButton = ({ className }: { className?: string }) => (
<Button className={clsx('absolute right-[2px] !p-[5px]', className)} variant="subtle">
<EjectSimple weight="bold" size={18} className="h-3 w-3 opacity-70" />
</Button>
);
export const LibrarySection = () => {
const node = useBridgeQuery(['nodeState']);
const locationsQuery = useLibraryQuery(['locations.list'], { keepPreviousData: true });

View file

@ -225,7 +225,7 @@ const EditLocationForm = () => {
<FlexCol>
<div>
<Button
onClick={() => alert('Archiving locations is coming soon...')}
onClick={() => toast.info('Archiving locations is coming soon...')}
size="sm"
variant="outline"
className=""

View file

@ -148,6 +148,9 @@ importers:
apps/landing:
dependencies:
'@docsearch/react':
specifier: '3'
version: 3.0.0(@algolia/client-search@4.20.0)(@types/react@18.2.34)(react-dom@18.2.0)(react@18.2.0)
'@octokit/webhooks':
specifier: ^12.0.3
version: 12.0.3
@ -1102,6 +1105,117 @@ packages:
resolution: {integrity: sha512-wi9JjgKLYS7U/z8PPbco+PvTb/nRWjeoFlJ1Qer83k/3C5PHQi28hiVdeE2kHXmIL99mQFawx8qt/JPjZilJ8Q==}
dev: false
/@algolia/autocomplete-core@1.5.2:
resolution: {integrity: sha512-DY0bhyczFSS1b/CqJlTE/nQRtnTAHl6IemIkBy0nEWnhDzRDdtdx4p5Uuk3vwAFxwEEgi1WqKwgSSMx6DpNL4A==}
dependencies:
'@algolia/autocomplete-shared': 1.5.2
dev: false
/@algolia/autocomplete-preset-algolia@1.5.2(@algolia/client-search@4.20.0)(algoliasearch@4.20.0):
resolution: {integrity: sha512-3MRYnYQFJyovANzSX2CToS6/5cfVjbLLqFsZTKcvF3abhQzxbqwwaMBlJtt620uBUOeMzhdfasKhCc40+RHiZw==}
peerDependencies:
'@algolia/client-search': ^4.9.1
algoliasearch: ^4.9.1
dependencies:
'@algolia/autocomplete-shared': 1.5.2
'@algolia/client-search': 4.20.0
algoliasearch: 4.20.0
dev: false
/@algolia/autocomplete-shared@1.5.2:
resolution: {integrity: sha512-ylQAYv5H0YKMfHgVWX0j0NmL8XBcAeeeVQUmppnnMtzDbDnca6CzhKj3Q8eF9cHCgcdTDdb5K+3aKyGWA0obug==}
dev: false
/@algolia/cache-browser-local-storage@4.20.0:
resolution: {integrity: sha512-uujahcBt4DxduBTvYdwO3sBfHuJvJokiC3BP1+O70fglmE1ShkH8lpXqZBac1rrU3FnNYSUs4pL9lBdTKeRPOQ==}
dependencies:
'@algolia/cache-common': 4.20.0
dev: false
/@algolia/cache-common@4.20.0:
resolution: {integrity: sha512-vCfxauaZutL3NImzB2G9LjLt36vKAckc6DhMp05An14kVo8F1Yofb6SIl6U3SaEz8pG2QOB9ptwM5c+zGevwIQ==}
dev: false
/@algolia/cache-in-memory@4.20.0:
resolution: {integrity: sha512-Wm9ak/IaacAZXS4mB3+qF/KCoVSBV6aLgIGFEtQtJwjv64g4ePMapORGmCyulCFwfePaRAtcaTbMcJF+voc/bg==}
dependencies:
'@algolia/cache-common': 4.20.0
dev: false
/@algolia/client-account@4.20.0:
resolution: {integrity: sha512-GGToLQvrwo7am4zVkZTnKa72pheQeez/16sURDWm7Seyz+HUxKi3BM6fthVVPUEBhtJ0reyVtuK9ArmnaKl10Q==}
dependencies:
'@algolia/client-common': 4.20.0
'@algolia/client-search': 4.20.0
'@algolia/transporter': 4.20.0
dev: false
/@algolia/client-analytics@4.20.0:
resolution: {integrity: sha512-EIr+PdFMOallRdBTHHdKI3CstslgLORQG7844Mq84ib5oVFRVASuuPmG4bXBgiDbcsMLUeOC6zRVJhv1KWI0ug==}
dependencies:
'@algolia/client-common': 4.20.0
'@algolia/client-search': 4.20.0
'@algolia/requester-common': 4.20.0
'@algolia/transporter': 4.20.0
dev: false
/@algolia/client-common@4.20.0:
resolution: {integrity: sha512-P3WgMdEss915p+knMMSd/fwiHRHKvDu4DYRrCRaBrsfFw7EQHon+EbRSm4QisS9NYdxbS04kcvNoavVGthyfqQ==}
dependencies:
'@algolia/requester-common': 4.20.0
'@algolia/transporter': 4.20.0
dev: false
/@algolia/client-personalization@4.20.0:
resolution: {integrity: sha512-N9+zx0tWOQsLc3K4PVRDV8GUeOLAY0i445En79Pr3zWB+m67V+n/8w4Kw1C5LlbHDDJcyhMMIlqezh6BEk7xAQ==}
dependencies:
'@algolia/client-common': 4.20.0
'@algolia/requester-common': 4.20.0
'@algolia/transporter': 4.20.0
dev: false
/@algolia/client-search@4.20.0:
resolution: {integrity: sha512-zgwqnMvhWLdpzKTpd3sGmMlr4c+iS7eyyLGiaO51zDZWGMkpgoNVmltkzdBwxOVXz0RsFMznIxB9zuarUv4TZg==}
dependencies:
'@algolia/client-common': 4.20.0
'@algolia/requester-common': 4.20.0
'@algolia/transporter': 4.20.0
dev: false
/@algolia/logger-common@4.20.0:
resolution: {integrity: sha512-xouigCMB5WJYEwvoWW5XDv7Z9f0A8VoXJc3VKwlHJw/je+3p2RcDXfksLI4G4lIVncFUYMZx30tP/rsdlvvzHQ==}
dev: false
/@algolia/logger-console@4.20.0:
resolution: {integrity: sha512-THlIGG1g/FS63z0StQqDhT6bprUczBI8wnLT3JWvfAQDZX5P6fCg7dG+pIrUBpDIHGszgkqYEqECaKKsdNKOUA==}
dependencies:
'@algolia/logger-common': 4.20.0
dev: false
/@algolia/requester-browser-xhr@4.20.0:
resolution: {integrity: sha512-HbzoSjcjuUmYOkcHECkVTwAelmvTlgs48N6Owt4FnTOQdwn0b8pdht9eMgishvk8+F8bal354nhx/xOoTfwiAw==}
dependencies:
'@algolia/requester-common': 4.20.0
dev: false
/@algolia/requester-common@4.20.0:
resolution: {integrity: sha512-9h6ye6RY/BkfmeJp7Z8gyyeMrmmWsMOCRBXQDs4mZKKsyVlfIVICpcSibbeYcuUdurLhIlrOUkH3rQEgZzonng==}
dev: false
/@algolia/requester-node-http@4.20.0:
resolution: {integrity: sha512-ocJ66L60ABSSTRFnCHIEZpNHv6qTxsBwJEPfYaSBsLQodm0F9ptvalFkHMpvj5DfE22oZrcrLbOYM2bdPJRHng==}
dependencies:
'@algolia/requester-common': 4.20.0
dev: false
/@algolia/transporter@4.20.0:
resolution: {integrity: sha512-Lsii1pGWOAISbzeyuf+r/GPhvHMPHSPrTDWNcIzOE1SG1inlJHICaVe2ikuoRjcpgxZNU54Jl+if15SUCsaTUg==}
dependencies:
'@algolia/cache-common': 4.20.0
'@algolia/logger-common': 4.20.0
'@algolia/requester-common': 4.20.0
dev: false
/@alloc/quick-lru@5.2.0:
resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==}
engines: {node: '>=10'}
@ -2970,6 +3084,28 @@ packages:
engines: {node: '>=10.0.0'}
dev: true
/@docsearch/css@3.0.0:
resolution: {integrity: sha512-1kkV7tkAsiuEd0shunYRByKJe3xQDG2q7wYg24SOw1nV9/2lwEd4WrUYRJC/ukGTl2/kHeFxsaUvtiOy0y6fFA==}
dev: false
/@docsearch/react@3.0.0(@algolia/client-search@4.20.0)(@types/react@18.2.34)(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-yhMacqS6TVQYoBh/o603zszIb5Bl8MIXuOc6Vy617I74pirisDzzcNh0NEaYQt50fVVR3khUbeEhUEWEWipESg==}
peerDependencies:
'@types/react': '>= 16.8.0 < 18.0.0'
react: '>= 16.8.0 < 18.0.0'
react-dom: '>= 16.8.0 < 18.0.0'
dependencies:
'@algolia/autocomplete-core': 1.5.2
'@algolia/autocomplete-preset-algolia': 1.5.2(@algolia/client-search@4.20.0)(algoliasearch@4.20.0)
'@docsearch/css': 3.0.0
'@types/react': 18.2.34
algoliasearch: 4.20.0
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
transitivePeerDependencies:
- '@algolia/client-search'
dev: false
/@effect-ts/core@0.60.5:
resolution: {integrity: sha512-qi1WrtJA90XLMnj2hnUszW9Sx4dXP03ZJtCc5DiUBIOhF4Vw7plfb65/bdBySPoC9s7zy995TdUX1XBSxUkl5w==}
dependencies:
@ -8938,6 +9074,25 @@ packages:
uri-js: 4.4.1
dev: false
/algoliasearch@4.20.0:
resolution: {integrity: sha512-y+UHEjnOItoNy0bYO+WWmLWBlPwDjKHW6mNHrPi0NkuhpQOOEbrkwQH/wgKFDLh7qlKjzoKeiRtlpewDPDG23g==}
dependencies:
'@algolia/cache-browser-local-storage': 4.20.0
'@algolia/cache-common': 4.20.0
'@algolia/cache-in-memory': 4.20.0
'@algolia/client-account': 4.20.0
'@algolia/client-analytics': 4.20.0
'@algolia/client-common': 4.20.0
'@algolia/client-personalization': 4.20.0
'@algolia/client-search': 4.20.0
'@algolia/logger-common': 4.20.0
'@algolia/logger-console': 4.20.0
'@algolia/requester-browser-xhr': 4.20.0
'@algolia/requester-common': 4.20.0
'@algolia/requester-node-http': 4.20.0
'@algolia/transporter': 4.20.0
dev: false
/amdefine@1.0.1:
resolution: {integrity: sha512-S2Hw0TtNkMJhIabBwIojKL9YHO5T0n5eNqWJ7Lrlel/zDbftQpxpapi8tZs3X1HWa+u+QeydGmzzNU0m09+Rcg==}
engines: {node: '>=0.4.2'}