├── src ├── App │ ├── utils │ │ ├── index.js │ │ ├── url-builder.js │ │ └── __test__ │ │ │ └── url-builder.test.js │ ├── libs │ │ ├── provider │ │ │ ├── index.js │ │ │ ├── namespaces.js │ │ │ ├── url-params.js │ │ │ ├── reducer.js │ │ │ └── provider.js │ │ ├── router │ │ │ ├── index.js │ │ │ ├── link.js │ │ │ ├── __test__ │ │ │ │ └── link.test.js │ │ │ ├── error-boundary.js │ │ │ ├── error-page.js │ │ │ ├── crash-page.js │ │ │ └── router.js │ │ ├── layout │ │ │ ├── header-logo.js │ │ │ ├── header-links │ │ │ │ ├── index.js │ │ │ │ └── link-theme.js │ │ │ ├── index.js │ │ │ └── index.module.css │ │ └── fetcher │ │ │ └── index.js │ ├── hooks │ │ ├── index.js │ │ ├── use-mobile.js │ │ └── use-custom-compare-memoize.js │ ├── components │ │ ├── icon │ │ │ ├── index.module.css │ │ │ └── index.js │ │ ├── containers │ │ │ ├── container.js │ │ │ ├── content │ │ │ │ ├── index.js │ │ │ │ └── index.module.css │ │ │ ├── message.js │ │ │ ├── base-alert.js │ │ │ └── loading-result.js │ │ └── index.js │ ├── styles │ │ ├── icons │ │ │ ├── index.js │ │ │ ├── fa-icons.js │ │ │ └── custom-icons.js │ │ ├── theme │ │ │ ├── default │ │ │ │ ├── button.js │ │ │ │ ├── list.js │ │ │ │ ├── table.js │ │ │ │ ├── index.js │ │ │ │ └── badge.js │ │ │ ├── index.js │ │ │ ├── color-scheme.js │ │ │ ├── variant.js │ │ │ ├── provider.js │ │ │ ├── common.js │ │ │ ├── resolver.js │ │ │ └── colors │ │ │ │ └── index.js │ │ ├── reset.css │ │ └── common.js │ ├── assets │ │ ├── index.js │ │ └── img │ │ │ ├── vespa-logo-black.svg │ │ │ └── vespa-logo-heather.svg │ ├── pages │ │ ├── search │ │ │ ├── search-container │ │ │ │ ├── index.js │ │ │ │ └── index.module.css │ │ │ ├── typography │ │ │ │ ├── index.js │ │ │ │ └── index.module.css │ │ │ ├── link-reference.js │ │ │ ├── abstract-container │ │ │ │ ├── abstract │ │ │ │ │ ├── use-consent.js │ │ │ │ │ ├── abstract-content.js │ │ │ │ │ ├── abstract-questions │ │ │ │ │ │ ├── index.module.css │ │ │ │ │ │ └── index.js │ │ │ │ │ ├── index.js │ │ │ │ │ ├── abstract-feedback.js │ │ │ │ │ ├── abstract-about.js │ │ │ │ │ ├── abstract-title.js │ │ │ │ │ └── abstract-disclaimer.js │ │ │ │ ├── index.js │ │ │ │ └── index.module.css │ │ │ ├── results-container │ │ │ │ ├── index.module.css │ │ │ │ ├── results │ │ │ │ │ ├── index.module.css │ │ │ │ │ └── index.js │ │ │ │ └── index.js │ │ │ ├── search-input │ │ │ │ ├── index.module.css │ │ │ │ └── index.js │ │ │ ├── search-classic.js │ │ │ ├── index.js │ │ │ ├── search-sources.js │ │ │ └── md-parser.js │ │ ├── home │ │ │ └── index.js │ │ ├── md │ │ │ └── index.js │ │ └── testcomp │ │ │ └── index.js │ └── index.js └── main.js ├── .prettierrc.cjs ├── .env ├── vespa-search.png ├── public └── favicon.ico ├── renovate.json ├── jsconfig.json ├── .gitignore ├── postcss.config.js ├── index.html ├── README.md ├── vite.config.js ├── .eslintrc.cjs ├── package.json ├── .github └── workflows │ └── deploy.yaml └── LICENSE /src/App/utils/index.js: -------------------------------------------------------------------------------- 1 | export * from './url-builder'; 2 | -------------------------------------------------------------------------------- /.prettierrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | singleQuote: true, 3 | }; 4 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | VITE_SITE_TITLE=Vespa Search 2 | VITE_ENDPOINT=https://api.search.vespa.ai 3 | -------------------------------------------------------------------------------- /vespa-search.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vespa-engine/vespa-search/HEAD/vespa-search.png -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vespa-engine/vespa-search/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /src/App/libs/provider/index.js: -------------------------------------------------------------------------------- 1 | export * from './namespaces'; 2 | export * from './provider'; 3 | -------------------------------------------------------------------------------- /src/App/hooks/index.js: -------------------------------------------------------------------------------- 1 | export * from './use-custom-compare-memoize'; 2 | export { useMobile } from 'App/hooks/use-mobile.js'; 3 | -------------------------------------------------------------------------------- /src/App/components/icon/index.module.css: -------------------------------------------------------------------------------- 1 | .box { 2 | &[data-disabled='true'] { 3 | pointer-events: none; 4 | opacity: var(--common-opacity); 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/App/styles/icons/index.js: -------------------------------------------------------------------------------- 1 | import customIcons from './custom-icons'; 2 | import * as faIcons from './fa-icons'; 3 | 4 | export const icons = [...Object.values(faIcons), ...customIcons]; 5 | -------------------------------------------------------------------------------- /src/App/assets/index.js: -------------------------------------------------------------------------------- 1 | export { default as VespaLogoBlack } from 'App/assets/img/vespa-logo-black.svg'; 2 | export { default as VespaLogoHeather } from 'App/assets/img/vespa-logo-heather.svg'; 3 | -------------------------------------------------------------------------------- /src/App/styles/theme/default/button.js: -------------------------------------------------------------------------------- 1 | import { Button as MantineButton } from '@mantine/core'; 2 | 3 | export const Button = MantineButton.extend({ 4 | defaultProps: { variant: 'filled' }, 5 | }); 6 | -------------------------------------------------------------------------------- /src/App/styles/theme/default/list.js: -------------------------------------------------------------------------------- 1 | import { List as MantineList } from '@mantine/core'; 2 | 3 | export const List = MantineList.extend({ 4 | styles: { 5 | itemWrapper: { display: 'inline' }, 6 | }, 7 | }); 8 | -------------------------------------------------------------------------------- /src/App/styles/theme/default/table.js: -------------------------------------------------------------------------------- 1 | import { Table as MantineTable } from '@mantine/core'; 2 | 3 | export const Table = MantineTable.extend({ 4 | defaultProps: { verticalSpacing: 'sm' }, 5 | styles: {}, 6 | }); 7 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "local>vespa-engine/renovate-config" 5 | ], 6 | "prHourlyLimit": 20, 7 | "prConcurrentLimit": 20 8 | } 9 | -------------------------------------------------------------------------------- /src/App/libs/router/index.js: -------------------------------------------------------------------------------- 1 | export { Router, Redirect } from './router'; 2 | export { Link } from './link'; 3 | export { ErrorPage } from './error-page'; 4 | export { ErrorBoundary } from './error-boundary'; 5 | export { CrashPage } from './crash-page.js'; 6 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom/client'; 3 | import App from 'App'; 4 | 5 | ReactDOM.createRoot(document.getElementById('root')).render( 6 | 7 | 8 | , 9 | ); 10 | -------------------------------------------------------------------------------- /src/App/styles/theme/default/index.js: -------------------------------------------------------------------------------- 1 | export { Badge } from 'App/styles/theme/default/badge'; 2 | export { Button } from 'App/styles/theme/default/button'; 3 | export { List } from 'App/styles/theme/default/list'; 4 | export { Table } from 'App/styles/theme/default/table'; 5 | -------------------------------------------------------------------------------- /src/App/styles/theme/index.js: -------------------------------------------------------------------------------- 1 | export { common } from 'App/styles/theme/common.js'; 2 | export { resolver } from 'App/styles/theme/resolver.js'; 3 | export { ThemeProvider } from 'App/styles/theme/provider.js'; 4 | export { ColorScheme } from 'App/styles/theme/color-scheme.js'; 5 | -------------------------------------------------------------------------------- /src/App/hooks/use-mobile.js: -------------------------------------------------------------------------------- 1 | import { useMediaQuery } from '@mantine/hooks'; 2 | import { breakpoints } from 'App/styles/common.js'; 3 | 4 | export function useMobile() { 5 | return useMediaQuery(`(max-width: ${breakpoints.sm})`, null, { 6 | getInitialValueInEffect: false, 7 | }); 8 | } 9 | -------------------------------------------------------------------------------- /src/App/styles/theme/color-scheme.js: -------------------------------------------------------------------------------- 1 | import { useMantineColorScheme } from '@mantine/core'; 2 | import { useHotkeys } from '@mantine/hooks'; 3 | 4 | export function ColorScheme() { 5 | const { toggleColorScheme } = useMantineColorScheme(); 6 | useHotkeys([['mod+J', toggleColorScheme]]); 7 | } 8 | -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true, 4 | "forceConsistentCasingInFileNames": true, 5 | "noFallthroughCasesInSwitch": true, 6 | "resolveJsonModule": true, 7 | "isolatedModules": true, 8 | "noEmit": true, 9 | "jsx": "react-jsx", 10 | "baseUrl": "src" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/App/pages/search/search-container/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Container } from 'App/components/index.js'; 3 | import classNames from 'App/pages/search/search-container/index.module.css'; 4 | 5 | export function SearchContainer(props) { 6 | return ; 7 | } 8 | -------------------------------------------------------------------------------- /src/App/pages/search/typography/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Stack } from '@mantine/core'; 3 | import Classnames from 'App/pages/search/typography/index.module.css'; 4 | 5 | export function Typography(props) { 6 | const { typography } = Classnames; 7 | return ; 8 | } 9 | -------------------------------------------------------------------------------- /src/App/pages/search/search-container/index.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | gap: var(--mantine-spacing-md); 3 | 4 | grid-template-columns: 5 | minmax(0, var(--search-result-width)) 6 | minmax(0, var(--search-abstract-width)); 7 | 8 | @media (max-width: $mantine-breakpoint-sm) { 9 | grid-template-columns: revert; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/App/components/containers/container.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Box } from '@mantine/core'; 3 | import { mergeStyles } from 'App/styles/common'; 4 | 5 | export function Container({ style, ...props }) { 6 | return ( 7 | 11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | build 15 | 16 | # Editor directories and files 17 | .vscode/* 18 | !.vscode/extensions.json 19 | .idea 20 | .DS_Store 21 | *.suo 22 | *.ntvs* 23 | *.njsproj 24 | *.sln 25 | *.sw? 26 | -------------------------------------------------------------------------------- /src/App/pages/search/link-reference.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useSearchContext } from 'App/libs/provider'; 3 | import { Link } from 'App/libs/router'; 4 | 5 | export function LinkReference({ token }) { 6 | const selectHit = useSearchContext((ctx) => ctx.selectHit); 7 | return ( 8 | selectHit(parseInt(token.text))}>{token.text} 9 | ); 10 | } 11 | -------------------------------------------------------------------------------- /src/App/pages/search/abstract-container/abstract/use-consent.js: -------------------------------------------------------------------------------- 1 | import { useLocalStorage } from '@mantine/hooks'; 2 | 3 | export function useConsent() { 4 | const [value, setValue] = useLocalStorage({ 5 | key: 'consent', 6 | getInitialValueInEffect: false, 7 | }); 8 | return { 9 | value: value === 'true', 10 | setValue: (value) => setValue(value.toString()), 11 | }; 12 | } 13 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | 'postcss-preset-mantine': {}, 4 | 'postcss-simple-vars': { 5 | variables: { 6 | 'mantine-breakpoint-xs': '36em', 7 | 'mantine-breakpoint-sm': '48em', 8 | 'mantine-breakpoint-md': '62em', 9 | 'mantine-breakpoint-lg': '75em', 10 | 'mantine-breakpoint-xl': '88em', 11 | }, 12 | }, 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /src/App/components/index.js: -------------------------------------------------------------------------------- 1 | export { Container } from 'App/components/containers/container.js'; 2 | export { Content } from 'App/components/containers/content'; 3 | export { Error } from 'App/components/containers/base-alert.js'; 4 | export { Icon } from 'App/components/icon'; 5 | export { LoadingResult } from 'App/components/containers/loading-result.js'; 6 | export { Message } from 'App/components/containers/message.js'; 7 | -------------------------------------------------------------------------------- /src/App/pages/search/abstract-container/abstract/abstract-content.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Stack } from '@mantine/core'; 3 | import { useSearchContext } from 'App/libs/provider/index.js'; 4 | import { parseMarkdown } from 'App/pages/search/md-parser.js'; 5 | 6 | export function AbstractContent() { 7 | const summary = useSearchContext((ctx) => ctx.summary.raw); 8 | return {parseMarkdown(summary)}; 9 | } 10 | -------------------------------------------------------------------------------- /src/App/pages/search/abstract-container/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { ScrollArea, Stack } from '@mantine/core'; 3 | import classNames from 'App/pages/search/abstract-container/index.module.css'; 4 | 5 | export function AbstractContainer(props) { 6 | return ( 7 | 8 | 9 | 10 | ); 11 | } 12 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vespa Search 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/App/components/containers/content/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Box, Stack } from '@mantine/core'; 3 | import classNames from 'App/components/containers/content/index.module.css'; 4 | 5 | export function Content({ withBorder, selected, ...props }) { 6 | const { box } = classNames; 7 | return ( 8 | 9 | 10 | 11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /src/App/components/containers/message.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Box } from '@mantine/core'; 3 | import { mergeStyles } from 'App/styles/common'; 4 | 5 | export function Message({ style, ...props }) { 6 | return ( 7 | 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /src/App/components/containers/base-alert.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Alert } from '@mantine/core'; 3 | import { Icon, Container } from '..'; 4 | 5 | function BaseAlert({ message, icon, color, ...props }) { 6 | return ( 7 | 8 | } color={color} {...props}> 9 | {message} 10 | 11 | 12 | ); 13 | } 14 | 15 | export const Error = (props) => ; 16 | -------------------------------------------------------------------------------- /src/App/pages/search/abstract-container/abstract/abstract-questions/index.module.css: -------------------------------------------------------------------------------- 1 | .item { 2 | border: 1px solid var(--subtle-border-and-separator-blue); 3 | background-color: var(--app-background); 4 | border-radius: var(--mantine-radius-xl); 5 | padding: var(--mantine-spacing-sm); 6 | margin-bottom: var(--mantine-spacing-xs); 7 | 8 | @mixin hover { 9 | border-color: var(--hovered-ui-element-border-blue); 10 | } 11 | } 12 | 13 | .link { 14 | &:hover { 15 | text-decoration: none; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/App/components/containers/content/index.module.css: -------------------------------------------------------------------------------- 1 | .box { 2 | position: relative; 3 | 4 | &[data-with-border='true'] { 5 | border: 1px solid var(--subtle-border-and-separator); 6 | border-radius: var(--mantine-radius-xs); 7 | } 8 | 9 | &[data-selected='true'] { 10 | border-color: var(--ui-element-border-and-focus-blue); 11 | 12 | @mixin hover { 13 | border-color: var(--hovered-ui-element-border-blue); 14 | } 15 | } 16 | 17 | @mixin hover { 18 | border-color: var(--hovered-ui-element-border); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/App/pages/search/results-container/index.module.css: -------------------------------------------------------------------------------- 1 | .scrollArea { 2 | height: var(--search-scrollarea-height); 3 | 4 | @media (max-width: $mantine-breakpoint-sm) { 5 | height: calc( 6 | 67vh - var(--app-shell-header-height, 0px) - 7 | (2 * var(--mantine-spacing-md)) 8 | ); 9 | } 10 | } 11 | 12 | .stack { 13 | max-width: calc(var(--search-result-width) - (4 * var(--mantine-spacing-md))); 14 | 15 | @media (max-width: $mantine-breakpoint-sm) { 16 | max-width: calc(100vw - (2 * var(--mantine-spacing-md))); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/App/hooks/use-custom-compare-memoize.js: -------------------------------------------------------------------------------- 1 | import { isEqual } from 'lodash'; 2 | import { useCallback, useRef } from 'react'; 3 | 4 | export function useCustomCompareMemoize(deps, depsEqual = isEqual) { 5 | const ref = useRef(); 6 | if (!ref.current || !depsEqual(ref.current, deps)) ref.current = deps; 7 | return ref.current; 8 | } 9 | 10 | export function useCustomCompareCallback(callback, deps, depsEqual) { 11 | // eslint-disable-next-line react-hooks/exhaustive-deps 12 | return useCallback(callback, useCustomCompareMemoize(deps, depsEqual)); 13 | } 14 | -------------------------------------------------------------------------------- /src/App/libs/layout/header-logo.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Image, useComputedColorScheme } from '@mantine/core'; 3 | import { Link } from 'react-router-dom'; 4 | import { VespaLogoBlack, VespaLogoHeather } from 'App/assets'; 5 | 6 | export function HeaderLogo() { 7 | const computedColorScheme = useComputedColorScheme('light'); 8 | const logo = 9 | computedColorScheme === 'dark' ? VespaLogoHeather : VespaLogoBlack; 10 | return ( 11 | 12 | 13 | 14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /src/App/components/containers/loading-result.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Skeleton, Stack } from '@mantine/core'; 3 | import { Content } from 'App/components'; 4 | 5 | export function LoadingResult() { 6 | return ( 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /src/App/pages/search/results-container/results/index.module.css: -------------------------------------------------------------------------------- 1 | .titleResult { 2 | color: var(--high-contrast-text); 3 | } 4 | 5 | .titleResult:hover .iconExternal { 6 | visibility: revert; 7 | } 8 | 9 | .iconExternal { 10 | visibility: hidden; 11 | } 12 | 13 | .spoilerControl { 14 | font-size: var(--mantine-font-size-sm); 15 | color: var(--high-contrast-text); 16 | background-color: var(--ui-element-background); 17 | text-align: center; 18 | width: 100%; 19 | 20 | @mixin hover { 21 | text-decoration: none; 22 | background-color: var(--hovered-ui-element-background); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/App/styles/theme/default/badge.js: -------------------------------------------------------------------------------- 1 | import { Badge as MantineBadge } from '@mantine/core'; 2 | 3 | export const Badge = MantineBadge.extend({ 4 | vars: (theme, { variant, color }) => { 5 | const _color = color || theme.primaryColor; 6 | if (variant === 'light') { 7 | return { 8 | root: { 9 | color: `var(--low-contrast-text-${_color})`, 10 | background: `var(--ui-element-background-${_color})`, 11 | hover: `var(--hovered-ui-element-background-${_color})`, 12 | }, 13 | }; 14 | } 15 | }, 16 | defaultProps: {}, 17 | styles: {}, 18 | }); 19 | -------------------------------------------------------------------------------- /src/App/pages/search/search-input/index.module.css: -------------------------------------------------------------------------------- 1 | .input { 2 | overflow-y: hidden; 3 | 4 | &[data-expanded='true'][data-suggestions='true'] { 5 | border-radius: var(--mantine-radius-lg) var(--mantine-radius-lg) 0 0; 6 | 7 | &:focus, 8 | &:focus-within { 9 | border-bottom-color: transparent; 10 | } 11 | } 12 | } 13 | 14 | .dropdown { 15 | border-bottom-left-radius: var(--mantine-radius-lg); 16 | border-bottom-right-radius: var(--mantine-radius-lg); 17 | border-color: var(--solid-background-green); 18 | border-top: none; 19 | overflow: hidden; 20 | margin-top: -10px; 21 | } 22 | -------------------------------------------------------------------------------- /src/App/styles/theme/variant.js: -------------------------------------------------------------------------------- 1 | import { defaultVariantColorsResolver, parseThemeColor } from '@mantine/core'; 2 | 3 | export const variantColorResolver = (input) => { 4 | const defaultResolvedColors = defaultVariantColorsResolver(input); 5 | const parsedColor = parseThemeColor({ 6 | color: input.color || input.theme.primaryColor, 7 | theme: input.theme, 8 | }); 9 | 10 | if (parsedColor.isThemeColor && input.variant === 'filled') { 11 | return { 12 | ...defaultResolvedColors, 13 | color: 'var(--vespa-color-rock)', 14 | hoverColor: 'var(--vespa-color-rock)', 15 | }; 16 | } 17 | 18 | return defaultResolvedColors; 19 | }; 20 | -------------------------------------------------------------------------------- /src/App/styles/icons/fa-icons.js: -------------------------------------------------------------------------------- 1 | export { 2 | faDocker, 3 | faGithub, 4 | faLinux, 5 | faPython, 6 | faSlack, 7 | } from '@fortawesome/free-brands-svg-icons'; 8 | export { 9 | faAdd, 10 | faBug, 11 | faCloud, 12 | faSun, 13 | faMoon, 14 | faArrowRight, 15 | faMagnifyingGlass, 16 | faBook, 17 | faBlog, 18 | faThumbsUp, 19 | faThumbsDown, 20 | faVial, 21 | faExpand, 22 | faExternalLink, 23 | faExternalLinkSquare, 24 | faCode, 25 | faCheck, 26 | } from '@fortawesome/free-solid-svg-icons'; 27 | export { 28 | faClipboard, 29 | faClock as faClockRegular, 30 | faFileLines, 31 | faCircleQuestion, 32 | } from '@fortawesome/free-regular-svg-icons'; 33 | -------------------------------------------------------------------------------- /src/App/pages/search/results-container/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { ScrollArea, Stack } from '@mantine/core'; 3 | import classNames from 'App/pages/search/results-container/index.module.css'; 4 | import { useMobile } from 'App/hooks'; 5 | 6 | export function ResultsContainer({ viewportRef, ...props }) { 7 | const isMobile = useMobile(); 8 | 9 | return ( 10 | 11 | 18 | 19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /src/App/libs/provider/namespaces.js: -------------------------------------------------------------------------------- 1 | export const ALL_NAMESPACES = Object.freeze([ 2 | { id: 'all', name: 'All', icon: 'check' }, 3 | { id: 'open-p', name: 'Documentation', icon: 'book' }, 4 | { id: 'cloud-p', name: 'Cloud Documentation', icon: 'cloud' }, 5 | { id: 'vespaapps-p', name: 'Sample Apps', icon: 'vial' }, 6 | { id: 'blog-p', name: 'Blog', icon: 'blog' }, 7 | { id: 'pyvespa-p', name: 'PyVespa', icon: 'fab-python' }, 8 | { id: 'code-p', name: 'Sample Schemas', icon: 'code' }, 9 | ]); 10 | 11 | export const NAMESPACES_BY_ID = Object.freeze( 12 | ALL_NAMESPACES.reduce( 13 | (object, namespace) => ({ ...object, [namespace.id]: namespace }), 14 | {}, 15 | ), 16 | ); 17 | -------------------------------------------------------------------------------- /src/App/libs/router/link.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link as RouterLink } from 'react-router-dom'; 3 | 4 | export const isInternalLink = (link) => { 5 | if (!link) return false; 6 | return !/^[a-z]+:\/\//.test(link); 7 | }; 8 | 9 | export function Link({ to, api = false, ...props }) { 10 | const internal = !api && isInternalLink(to); 11 | if (!props.download && to && internal) 12 | return ; 13 | 14 | const fixedProps = Object.assign( 15 | to ? { href: (api ? window.config.api : '') + to } : {}, 16 | to && !internal && { target: '_blank', rel: 'noopener noreferrer' }, 17 | props, 18 | ); 19 | return ; 20 | } 21 | -------------------------------------------------------------------------------- /src/App/pages/search/abstract-container/index.module.css: -------------------------------------------------------------------------------- 1 | .scrollArea { 2 | height: var(--search-scrollarea-height); 3 | 4 | @media (max-width: $mantine-breakpoint-sm) { 5 | height: calc(33vh - (1 * var(--mantine-spacing-md))); 6 | background-color: var(--ui-element-background-green); 7 | padding-top: var(--mantine-spacing-sm); 8 | padding-left: var(--mantine-spacing-sm); 9 | padding-right: var(--mantine-spacing-sm); 10 | } 11 | } 12 | 13 | .stack { 14 | max-width: calc( 15 | var(--search-abstract-width) - (1 * var(--mantine-spacing-md)) 16 | ); 17 | 18 | @media (max-width: $mantine-breakpoint-sm) { 19 | max-width: calc(100vw - (2 * var(--mantine-spacing-md))); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/App/libs/router/__test__/link.test.js: -------------------------------------------------------------------------------- 1 | import { expect, test } from 'vitest'; 2 | import { isInternalLink } from '../link'; 3 | 4 | test('external links', () => { 5 | expect(isInternalLink('http://www.vg.no')).toBeFalsy(); 6 | expect(isInternalLink('gopher://gopher.floodgap.com/1/world')).toBeFalsy(); 7 | expect( 8 | isInternalLink('slack://channel?team=T025DU6HX&id=C6KT1FC9L'), 9 | ).toBeFalsy(); 10 | }); 11 | 12 | test('invalid links', () => { 13 | expect(isInternalLink()).toBeFalsy(); 14 | expect(isInternalLink(null)).toBeFalsy(); 15 | expect(isInternalLink('')).toBeFalsy(); 16 | }); 17 | 18 | test('internal links', () => { 19 | expect(isInternalLink('/search')).toBeTruthy(); 20 | expect(isInternalLink('/')).toBeTruthy(); 21 | }); 22 | -------------------------------------------------------------------------------- /src/App/styles/reset.css: -------------------------------------------------------------------------------- 1 | #root { 2 | height: 100%; 3 | isolation: isolate; 4 | } 5 | 6 | html { 7 | height: 100%; 8 | } 9 | 10 | * { 11 | margin: 0; 12 | } 13 | 14 | *, 15 | *::before, 16 | *::after { 17 | box-sizing: border-box; 18 | } 19 | 20 | body { 21 | -webkit-font-smoothing: antialiased; 22 | height: 100%; 23 | } 24 | 25 | img, 26 | picture, 27 | video, 28 | canvas, 29 | svg { 30 | display: block; 31 | } 32 | 33 | input, 34 | button, 35 | textarea, 36 | select { 37 | font: inherit; 38 | } 39 | 40 | p, 41 | h1, 42 | h2, 43 | h3, 44 | h4, 45 | h5, 46 | h6 { 47 | overflow-wrap: break-word; 48 | } 49 | 50 | a { 51 | color: var(--vespa-color-anchor); 52 | cursor: pointer; 53 | text-decoration: none; 54 | &:hover { 55 | text-decoration: underline; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/App/pages/search/typography/index.module.css: -------------------------------------------------------------------------------- 1 | .typography { 2 | color: var(--low-contrast-text); 3 | font-size: var(--mantine-font-size-sm); 4 | 5 | & ul, 6 | & ol { 7 | & li { 8 | font-size: var(--mantine-font-size-sm); 9 | } 10 | } 11 | 12 | & blockquote { 13 | font-size: var(--mantine-font-size-sm); 14 | border-top-right-radius: var(--mantine-radius-sm); 15 | border-bottom-right-radius: var(--mantine-radius-sm); 16 | color: var(--low-contrast-text); 17 | border-left: rem(5) solid var(--mantine-color-default-border); 18 | 19 | & cite { 20 | display: block; 21 | font-size: var(--mantine-font-size-sm); 22 | margin-top: var(--mantine-spacing-xs); 23 | overflow: hidden; 24 | text-overflow: ellipsis; 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/App/pages/home/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Container, Group, Space } from '@mantine/core'; 3 | import { SearchInput } from 'App/pages/search/search-input/index.js'; 4 | import { Icon } from 'App/components/index.js'; 5 | import { Link } from 'App/libs/router/index.js'; 6 | 7 | export function Home() { 8 | return ( 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | Browse documentation 21 | 22 | 23 |
24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /src/App/pages/search/search-classic.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Anchor, Group } from '@mantine/core'; 3 | import { useSearchContext } from 'App/libs/provider/index.js'; 4 | import { Icon } from 'App/components/index.js'; 5 | import { useMobile } from 'App/hooks'; 6 | 7 | export function SearchClassic() { 8 | const query = useSearchContext((ctx) => ctx.query); 9 | const isMobile = useMobile(); 10 | 11 | return ( 12 | 13 | 20 | 21 | 22 | classic search 23 | 24 | 25 | 26 | ); 27 | } 28 | -------------------------------------------------------------------------------- /src/App/libs/layout/header-links/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Group } from '@mantine/core'; 3 | import { LinkTheme } from 'App/libs/layout/header-links/link-theme'; 4 | import { Icon } from 'App/components'; 5 | import { Link } from 'App/libs/router'; 6 | 7 | export function HeaderLinks() { 8 | return ( 9 | 10 | 11 | 17 | 18 | 19 | 25 | 26 | 27 | 28 | ); 29 | } 30 | -------------------------------------------------------------------------------- /src/App/libs/layout/header-links/link-theme.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | ActionIcon, 4 | Tooltip, 5 | useComputedColorScheme, 6 | useMantineColorScheme, 7 | } from '@mantine/core'; 8 | import { Icon } from 'App/components'; 9 | 10 | export function LinkTheme() { 11 | const { toggleColorScheme } = useMantineColorScheme(); 12 | const computedColorScheme = useComputedColorScheme('light'); 13 | const isDarkMode = computedColorScheme === 'dark'; 14 | const color = isDarkMode ? 'yellow' : 'var(--header-links)'; 15 | const iconName = isDarkMode ? 'sun' : 'moon'; 16 | 17 | return ( 18 | 19 | toggleColorScheme()} 21 | variant="transparent" 22 | color={color} 23 | > 24 | 25 | 26 | 27 | ); 28 | } 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ![Vespa Search screenshot](vespa-search.png) 4 | 5 | # Vespa Search 6 | 7 | This is a Vespa Application for searching and exploring documentation, blogs, 8 | sample applications and other resources useful when working on Vespa. 9 | 10 | 11 | 12 | 13 | 14 | Install and start: 15 | 16 | yarn install 17 | yarn dev 18 | 19 | Alternatively, use Docker to start it without installing node: 20 | 21 | docker run -v `pwd`:/w -w /w --publish 3000:3000 node sh -c 'yarn install && yarn dev --host' 22 | 23 | When started, open [http://127.0.0.1:3000/](http://127.0.0.1:3000/). 24 | 25 | ## License 26 | 27 | Code licensed under the Apache 2.0 license. See [LICENSE](LICENSE) for terms. 28 | -------------------------------------------------------------------------------- /src/App/styles/common.js: -------------------------------------------------------------------------------- 1 | import { em, rem } from '@mantine/core'; 2 | 3 | // maximum width for a content container 4 | export const maxWidth = 1920; 5 | 6 | // default border radius for all elements 7 | export const borderRadius = rem(2); 8 | 9 | // default opacity for disabled elements 10 | export const opacity = 0.34; 11 | 12 | // Default font weights for all text elements 13 | export const fontWeightLight = 300; 14 | export const fontWeightRegular = 400; 15 | export const fontWeightBold = 600; 16 | 17 | export const breakpoints = { 18 | xs: em(576), 19 | sm: em(768), 20 | md: em(992), 21 | lg: em(1200), 22 | xl: em(1400), 23 | }; 24 | 25 | export const mergeStyles = (a = {}, b = {}) => { 26 | if (typeof a !== 'function' && typeof b !== 'function') return { ...a, ...b }; 27 | return (theme) => ({ 28 | ...(typeof a === 'function' ? a(theme) : a), 29 | ...(typeof b === 'function' ? b(theme) : b), 30 | }); 31 | }; 32 | -------------------------------------------------------------------------------- /vite.config.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import fs from 'fs/promises'; 3 | import { defineConfig } from 'vite'; 4 | import react from '@vitejs/plugin-react'; 5 | 6 | // https://vitejs.dev/config/ 7 | export default defineConfig({ 8 | build: { 9 | sourcemap: 'hidden', 10 | }, 11 | esbuild: { 12 | loader: 'jsx', 13 | include: /src\/.*\.jsx?$/, 14 | exclude: [], 15 | }, 16 | optimizeDeps: { 17 | esbuildOptions: { 18 | plugins: [ 19 | { 20 | name: 'load-js-files-as-jsx', 21 | setup(build) { 22 | build.onLoad({ filter: /src\/.*\.js$/ }, async (args) => ({ 23 | loader: 'jsx', 24 | contents: await fs.readFile(args.path, 'utf8'), 25 | })); 26 | }, 27 | }, 28 | ], 29 | }, 30 | }, 31 | plugins: [react()], 32 | resolve: { 33 | alias: { 34 | App: path.resolve(__dirname, 'src/App'), 35 | }, 36 | }, 37 | }); 38 | -------------------------------------------------------------------------------- /src/App/styles/theme/provider.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { library } from '@fortawesome/fontawesome-svg-core'; 3 | import { ColorSchemeScript, createTheme, MantineProvider } from '@mantine/core'; 4 | import { common, resolver } from 'App/styles/theme'; 5 | import { icons } from 'App/styles/icons'; 6 | import * as components from 'App/styles/theme/default'; 7 | import { variantColorResolver } from 'App/styles/theme/variant'; 8 | 9 | const theme = createTheme({ 10 | ...common, 11 | components, 12 | variantColorResolver, 13 | }); 14 | 15 | export function ThemeProvider({ children }) { 16 | icons.forEach((icon) => library.add(icon)); 17 | return ( 18 | <> 19 | 20 | 25 | {children} 26 | 27 | 28 | ); 29 | } 30 | -------------------------------------------------------------------------------- /src/App/libs/router/error-boundary.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export class ErrorBoundary extends React.Component { 4 | constructor(props) { 5 | super(props); 6 | this.state = {}; 7 | this.crashPage = props.crashPage; 8 | if (typeof this.crashPage !== 'function') 9 | throw new Error("Required prop 'crashPage' is not a valid React element"); 10 | } 11 | 12 | componentDidCatch(exception, errorInfo) { 13 | const error = Object.getOwnPropertyNames(exception).reduce( 14 | (acc, key) => { 15 | acc[key] = exception[key]; 16 | return acc; 17 | }, 18 | { ...errorInfo }, 19 | ); 20 | const meta = { 21 | location: window?.location?.href, 22 | time: new Date().toISOString(), 23 | error, 24 | }; 25 | this.setState({ error: meta }); 26 | } 27 | 28 | render() { 29 | if (this.state.error) return this.crashPage({ error: this.state.error }); 30 | return this.props.children; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/App/styles/theme/common.js: -------------------------------------------------------------------------------- 1 | import { rem } from '@mantine/core'; 2 | 3 | export const common = { 4 | defaultRadius: 'xs', 5 | cursorType: 'pointer', 6 | fontFamily: 'Inter, sans-serif', 7 | primaryColor: 'green', 8 | spacing: { 9 | xs: rem(5), 10 | sm: rem(8), 11 | md: rem(13), 12 | lg: rem(21), 13 | xl: rem(34), 14 | }, 15 | lineHeights: { 16 | xs: '1.62', 17 | sm: '1.62', 18 | md: '1.62', 19 | lg: '1.62', 20 | xl: '1.62', 21 | }, 22 | headings: { 23 | fontFamily: 'Inter, sans-serif', 24 | sizes: { 25 | h1: { fontSize: '1.3333rem', lineHeight: 1, fontWeight: 600 }, 26 | h2: { fontSize: '1.1875rem', lineHeight: 1, fontWeight: 400 }, 27 | h3: { fontSize: '1.1042rem', lineHeight: 1, fontWeight: 400 }, 28 | h4: { fontSize: '1.0417rem', lineHeight: 1, fontWeight: 600 }, 29 | h5: { fontSize: '1rem', lineHeight: 1, fontWeight: 600 }, 30 | h6: { fontSize: '0.9375rem', lineHeight: 1, fontWeight: 600 }, 31 | }, 32 | }, 33 | }; 34 | -------------------------------------------------------------------------------- /src/App/libs/router/error-page.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useLocation } from 'react-router-dom'; 3 | import { Text, Title } from '@mantine/core'; 4 | import { Message } from 'App/components'; 5 | 6 | const getMessage = (code, location) => { 7 | const numberCode = 8 | parseInt(code || new URLSearchParams(location?.search).get('code')) || 404; 9 | 10 | switch (numberCode) { 11 | case 403: 12 | return 'Sorry, you are not authorized to view this page.'; 13 | case 404: 14 | return 'Sorry, the page you were looking for does not exist.'; 15 | case 500: 16 | return 'Oops... something went wrong.'; 17 | default: 18 | return `Unknown error (${code}) - really, I have no idea what is going on here.`; 19 | } 20 | }; 21 | 22 | export function ErrorPage({ code }) { 23 | const location = useLocation(); 24 | const message = getMessage(code, location); 25 | return ( 26 | 27 | 28 | <Text fw={400} inherit> 29 | {message} 30 | </Text> 31 | 32 | 33 | ); 34 | } 35 | -------------------------------------------------------------------------------- /src/App/libs/router/crash-page.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Anchor, Space, Stack, Text, Title } from '@mantine/core'; 3 | import { Icon } from 'App/components'; 4 | 5 | export function CrashPage({ error }) { 6 | return ( 7 | 8 | 9 | 10 | You encountered a bug 11 | This is our fault. 12 | 13 | Not much you can do about it, but here are three suggestions anyway 14 | 15 | Reload page - you never know 16 | 20 | Email us a bug report, please include the information below 21 | 22 |