Skip to content

Commit

Permalink
Migration: Restructure Search Page
Browse files Browse the repository at this point in the history
  • Loading branch information
reglim committed Dec 19, 2022
1 parent 94c61f6 commit 9e29bdf
Show file tree
Hide file tree
Showing 3 changed files with 129 additions and 91 deletions.
120 changes: 120 additions & 0 deletions web/src/components/SearchResults.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import { uniqueId } from 'lodash'
import React, { useEffect, useState } from 'react'
import { Link } from 'react-router-dom'
import { ApiSearchResponse } from '../models/SearchResult'

import styles from '../style/components/SearchResults.module.css'

interface Props {
searchQuery: string
loading: boolean
results: ApiSearchResponse
}

export default function SearchResults (props: Props): JSX.Element {
const [loading, setLoading] = useState<boolean>(props.loading)
const [resultElements, setResultElements] = useState<JSX.Element[]>([])

useEffect(() => {
setLoading(props.loading)
}, [props.loading])

useEffect(() => {
setLoading(true)
setResultElements(createSearchResultElements(props.results))
setLoading(false)
}, [props.results])

/**
* Used to create the result elements before updating the UI,
* as this can lag if there are many results.
* @param res ApiSearchResponse
* @returns Array of JSX.Element
*/
const createSearchResultElements = (
res: ApiSearchResponse
): JSX.Element[] => {
const projects = res.projects.map((p) => (
<Link
className={styles['search-result']}
key={`project-${p.name}`}
to={`/${p.name}`}
dangerouslySetInnerHTML={{
__html: highlighedText(p.name)
}}
></Link>
))

const versions = res.versions.map((v) => (
<Link
className={styles['search-result']}
key={`version-${v.project}-${v.version}`}
to={`/${v.project}/${v.version}`}
dangerouslySetInnerHTML={{
__html: highlighedText(`${v.project} v. ${v.version}`)
}}
></Link>
))

const files = res.files.map((f) => (
<Link
className={styles['search-result']}
key={`file-${f.project}-${f.version}-${f.path}`}
to={`/${f.project}/${f.version}/${f.path}`}
dangerouslySetInnerHTML={{
__html: highlighedText(`${f.project} v. ${f.version} - ${f.path}`)
}}
></Link>
))

return [...projects, ...versions, ...files]
}

/**
* Used to replace any unsafe characters in the displayed name
* with their html counterparts because we need to set the text as
* html to be able to highlight the search query.
* @param text content to be escaped
* @returns excaped text
*/
const escapeHtml = (text: string): string => {
return text
.replaceAll('&', '&amp;')
.replaceAll('<', '&lt;')
.replaceAll('>', '&gt;')
.replaceAll('"', '&quot;')
.replaceAll("'", '&#039;')
}

/**
* Used to highlight the query in the displayed link text.
* @param text link text
* @returns html string with highlighted query
*/
const highlighedText = (text: string): string => {
text = escapeHtml(text)
return text.replaceAll(
props.searchQuery,
`<span class="${styles.highlighted}" key="highlighted-${uniqueId()}">${
props.searchQuery
}</span>`
)
}

if (loading) {
return <div className="loading-spinner"></div>
}

if (resultElements.length === 0) {
if (props.searchQuery.trim().length === 0) {
return <div className={styles['no-results']}>No results</div>
}
return (
<div className={styles['no-results']}>
No results for &quot;{props.searchQuery}&quot;
</div>
)
}

return <div className={styles['search-results']}>{resultElements}</div>
}
100 changes: 9 additions & 91 deletions web/src/pages/Search.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
import { TextField } from '@mui/material'
import React, { useEffect, useMemo, useState } from 'react'
import { Link, useSearchParams } from 'react-router-dom'
import { useSearchParams } from 'react-router-dom'
import PageLayout from '../components/PageLayout'
import { ApiSearchResponse } from '../models/SearchResult'
import ProjectRepository from '../repositories/ProjectRepository'
import LoadingPage from './LoadingPage'
import { debounce, uniqueId } from 'lodash'
import { debounce } from 'lodash'

import styles from '../style/pages/Search.module.css'
import SearchResults from '../components/SearchResults'

export default function Search (): JSX.Element {
const NO_RESULTS: ApiSearchResponse = {
Expand All @@ -17,11 +16,12 @@ export default function Search (): JSX.Element {
}
const queryParam =
useSearchParams()[0].get('query') ?? ''
const debounceMs = 1000

const [loading, setLoading] = useState<boolean>(false)
const [errorMsg, setErrorMsg] = useState<string | null>(null)
const [searchQuery, setSearchQuery] = useState<string>(queryParam)
const [results, setResults] = useState<ApiSearchResponse>(NO_RESULTS)
const [searchQuery, setSearchQuery] = useState<string>(queryParam)
// used to prevent the search from being triggered immediately when the query is changed
const [displayedSearchQuery, setDisplayedSearchQuery] =
useState<string>(queryParam)
Expand All @@ -36,6 +36,7 @@ export default function Search (): JSX.Element {
return
}

setLoading(true)
ProjectRepository.search(searchQuery)
.then((res) => {
setResults(res)
Expand All @@ -49,7 +50,7 @@ export default function Search (): JSX.Element {
setLoading(false)
setTimeout(() => setErrorMsg(null), 5000)
})
}, 500),
}, debounceMs),
[searchQuery]
)

Expand All @@ -59,44 +60,6 @@ export default function Search (): JSX.Element {
searchDebounced()
}, [searchQuery])

/**
* Used to replace any unsafe characters in the displayed name
* with their html counterparts because we need to set the text as
* html to be able to highlight the search query.
* @param text content to be escaped
* @returns excaped text
*/
const escapeHtml = (text: string): string => {
return text
.replaceAll('&', '&amp;')
.replaceAll('<', '&lt;')
.replaceAll('>', '&gt;')
.replaceAll('"', '&quot;')
.replaceAll("'", '&#039;')
}

const highlighedText = (text: string): string => {
text = escapeHtml(text)
return text.replaceAll(
searchQuery,
`<span class="${
styles.highlighted
}" key="highlighted-${uniqueId()}">${searchQuery}</span>`
)
}

const hasProjects = (): boolean => {
return (
results.projects.length > 0 ||
results.versions.length > 0 ||
results.files.length > 0
)
}

if (loading) {
return <LoadingPage />
}

return (
<PageLayout
title="Search"
Expand All @@ -111,56 +74,11 @@ export default function Search (): JSX.Element {
value={displayedSearchQuery}
onChange={(e) => {
setDisplayedSearchQuery(e.target.value)
debounce(() => setSearchQuery(e.target.value), 500)()
debounce(() => setSearchQuery(e.target.value), debounceMs)()
}}
/>

<>
{hasProjects() && (
<div className={styles['search-results']}>
{results.projects.map((p) => (
<Link
className={styles['search-result']}
key={`project-${p.name}`}
to={`/${p.name}`}
dangerouslySetInnerHTML={{
__html: highlighedText(p.name)
}}
></Link>
))}
{results.versions.map((v) => (
<Link
className={styles['search-result']}
key={`version-${v.project}-${v.version}`}
to={`/${v.project}/${v.version}`}
dangerouslySetInnerHTML={{
__html: highlighedText(`${v.project} v. ${v.version}`)
}}
></Link>
))}
{results.files.map((f) => (
<Link
className={styles['search-result']}
key={`file-${f.project}-${f.version}-${f.path}`}
to={`/${f.project}/${f.version}/${f.path}`}
dangerouslySetInnerHTML={{
__html: highlighedText(
`${f.project} v. ${f.version} - ${f.path}`
)
}}
></Link>
))}
</div>
)}
{!hasProjects() && displayedSearchQuery.trim().length > 0 && (
<div className={styles['no-results']}>
No results found for &quot;{displayedSearchQuery}&quot;
</div>
)}
{!hasProjects() && displayedSearchQuery.trim().length === 0 && (
<div className={styles['no-results']}>No results</div>
)}
</>
<SearchResults searchQuery={searchQuery} loading={loading} results={results} />
</PageLayout>
)
}
File renamed without changes.

0 comments on commit 9e29bdf

Please sign in to comment.