Skip to content

Commit

Permalink
Merge pull request #153 from openstax/K12-46/implement-search-ui-comp…
Browse files Browse the repository at this point in the history
…onent

Implement search UI component
  • Loading branch information
MReyna12 authored Apr 19, 2024
2 parents d485be1 + b0e74b7 commit b6cbb44
Show file tree
Hide file tree
Showing 7 changed files with 420 additions and 3 deletions.
145 changes: 145 additions & 0 deletions src/components/SearchBlock.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
import { Formik, Form, Field } from 'formik'
import { ENV } from '../lib/env'
import { useState } from 'react'

interface SearchBlockProps {
versionId: string
filter?: string
}

interface HitValue {
value: number
}

interface HitSource {
section: string
activity_name: string
lesson_page: string
teacher_only: boolean
}

interface HitHighlight {
lesson_page?: string[]
visible_content?: string[]
activity_name?: string[]
}

interface Hit {
_id: string
_source: HitSource
highlight: HitHighlight
}

interface Hits {
total: HitValue
hits: Hit[]
}

interface SearchResults {
hits: Hits
}

export const SearchBlock = ({ versionId, filter }: SearchBlockProps): JSX.Element => {
const [query, setQuery] = useState('')
const [searchResults, setSearchResults] = useState<SearchResults | undefined>(undefined)
const [errorMessage, setErrorMessage] = useState('')
const fetchContent = async (): Promise<void> => {
try {
const response = filter !== undefined
? await fetch(`${ENV.OS_RAISE_SEARCHAPI_URL_PREFIX}/v1/search?q=${query}&version=${versionId}&filter=${filter}`)
: await fetch(`${ENV.OS_RAISE_SEARCHAPI_URL_PREFIX}/v1/search?q=${query}&version=${versionId}`)

if (!response.ok) {
throw new Error('Failed to get search results')
}

const data: SearchResults = await response.json()
setSearchResults(data)
} catch (error) {
setErrorMessage('Failed to get search results, please try again.')
console.error('Error fetching search results:', error)
}
}

const handleSubmit = async (): Promise<void> => {
if (query.trim() === '') {
setErrorMessage('Input cannot be empty')
return
}
setSearchResults(undefined)
setErrorMessage('')
await fetchContent()
}
return (
<div>
<Formik
initialValues={{ response: '' }}
onSubmit={handleSubmit}
>
{({ setFieldValue, isSubmitting }) => (
<Form className='os-search-form'>
<Field
name="response"
type='text'
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setQuery(e.target.value)
setErrorMessage('')
void setFieldValue('response', e.target.value)
}}
disabled={isSubmitting}
/>
{isSubmitting
? <div className="os-raise-bootstrap">
<div className="text-center">
<div className="spinner-border mt-3 text-success" role="status">
<span className="visually-hidden">Searching...</span>
</div>
</div>
</div>
: <div className='os-raise-bootstrap os-text-center mt-4'>
<button type="submit" disabled={isSubmitting} className="os-btn btn-outline-success">Search</button>
</div>
}
{errorMessage !== '' && <p className='os-search-error-message'>{errorMessage}</p>}
</Form>
)}
</Formik>
{searchResults !== undefined && searchResults.hits.total.value !== 0 &&
<div className='os-search-results-container'>
<p>Total search results: {searchResults.hits.total.value}</p>
<p>Total search results displayed: {searchResults.hits.hits.length}</p>
<ul className='os-search-results-list'>
{searchResults.hits.hits.map((hit) => (
<li className='os-search-results-list-item' key={hit._id}>
<div>
<h3>Location</h3>
<p>{hit._source.section}</p>
<p>{hit._source.activity_name}</p>
<p>{hit._source.lesson_page !== '' && hit._source.lesson_page}</p>
</div>
<div>
{/* The keys for each item below are generated using the item's index in the array */}
<h3>{hit._source.teacher_only ? 'Teacher Content' : 'Content'}</h3>
{hit.highlight.visible_content?.map((content: string) => (
<p className='os-search-results-highlights' dangerouslySetInnerHTML={{ __html: content }}></p>
))}
{hit.highlight.lesson_page?.map((page: string) => (
<p className='os-search-results-highlights' dangerouslySetInnerHTML={{ __html: page }}></p>
))}
{hit.highlight.activity_name?.map((activity: string) => (
<p className='os-search-results-highlights' dangerouslySetInnerHTML={{ __html: activity }}></p>
))}
</div>
</li>
))}
</ul>
</div>
}
{searchResults !== undefined && searchResults.hits.total.value === 0 &&
<div>
<p className='os-search-no-results-message'>Your query did not produce any results. Please try again.</p>
</div>
}
</div>
)
}
15 changes: 15 additions & 0 deletions src/lib/blocks.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@ import {
PROBLEM_TYPE_MULTISELECT
} from '../components/ProblemSetBlock'
import { UserInputBlock } from '../components/UserInputBlock'
import { SearchBlock } from '../components/SearchBlock'
import { queueIbPsetProblemAttemptedV1Event, queueIbInputSubmittedV1Event } from './events'
import { getVersionId } from './utils'

export const OS_RAISE_IB_EVENT_PREFIX = 'os-raise-ib-event'
export const OS_RAISE_IB_CONTENT_CLASS = 'os-raise-ib-content'
Expand All @@ -21,6 +23,7 @@ export const CTA_CONTENT_CLASS = 'os-raise-ib-cta-content'
export const CTA_PROMPT_CLASS = 'os-raise-ib-cta-prompt'
export const OS_RAISE_IB_INPUT_CLASS = 'os-raise-ib-input'
export const OS_RAISE_IB_DESMOS_CLASS = 'os-raise-ib-desmos-gc'
export const OS_RAISE_SEARCH_CLASS = 'os-raise-search'
const INPUT_CONTENT_CLASS = 'os-raise-ib-input-content'
const INPUT_PROMPT_CLASS = 'os-raise-ib-input-prompt'
const INPUT_ACK_CLASS = 'os-raise-ib-input-ack'
Expand Down Expand Up @@ -365,3 +368,15 @@ export const parseDesmosBlock = (element: HTMLElement): JSX.Element | null => {

/>
}

export const parseSearchBlock = (element: HTMLElement): JSX.Element | null => {
if (!element.classList.contains(OS_RAISE_SEARCH_CLASS)) {
return null
}
const maybeFilter = element.dataset.filter

return <SearchBlock
versionId={getVersionId()}
filter={maybeFilter}
/>
}
1 change: 1 addition & 0 deletions src/lib/env.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export const ENV = {
OS_RAISE_CONTENT_URL_PREFIX: import.meta.env.MODE === 'production' ? 'https://k12.openstax.org/contents/raise' : 'http://localhost:8800/contents',
OS_RAISE_SEARCHAPI_URL_PREFIX: import.meta.env.MODE === 'development' ? 'http://localhost:9400' : 'https://search.raiselearning.org',
OS_RAISE_EVENTSAPI_URL_MAP: {},
EVENT_FLUSH_PERIOD: 60000
}
Expand Down
8 changes: 7 additions & 1 deletion src/lib/render-moodle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,13 @@ import {
OS_RAISE_IB_INPUT_CLASS,
OS_RAISE_IB_PSET_CLASS,
OS_RAISE_IB_DESMOS_CLASS,
OS_RAISE_SEARCH_CLASS,
parseContentOnlyBlock,
parseCTABlock,
parseUserInputBlock,
parseProblemSetBlock,
parseDesmosBlock
parseDesmosBlock,
parseSearchBlock
} from './blocks'

const replaceElementWithBlock = (element: HTMLElement, component: JSX.Element): void => {
Expand Down Expand Up @@ -103,3 +105,7 @@ export const renderProblemSetBlocks = (element: HTMLElement): void => {
export const renderDesmosBlocks = (element: HTMLElement): void => {
renderContentBlocksByClass(element, OS_RAISE_IB_DESMOS_CLASS, parseDesmosBlock)
}

export const renderSearchBlocks = (element: HTMLElement): void => {
renderContentBlocksByClass(element, OS_RAISE_SEARCH_CLASS, parseSearchBlock)
}
5 changes: 4 additions & 1 deletion src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ import {
renderCTABlocks,
renderProblemSetBlocks,
renderUserInputBlocks,
renderDesmosBlocks
renderDesmosBlocks,
renderSearchBlocks
} from './lib/render-moodle'
import { renderContentElements } from './lib/content'
import { tooltipify } from './lib/tooltip'
Expand All @@ -25,6 +26,8 @@ const processPage = (): void => {
renderProblemSetBlocks(document.body)
renderDesmosBlocks(document.body)
}

renderSearchBlocks(document.body)
}

processPage()
43 changes: 42 additions & 1 deletion src/styles/interactives.scss
Original file line number Diff line number Diff line change
Expand Up @@ -172,4 +172,45 @@ math-field::part(menu-toggle) {
// Textarea Styles
.os-textarea-disabled.os-textarea-disabled {
box-shadow: none;
}
}

// Search Styles
.os-search-form {
display: flex;
flex-direction: column;
background-color: #00504721;
border: 1px solid #00504721;
border-radius: 5px;
padding: 1rem
}

.os-search-results-container {
margin-top: 1rem;
}

.os-search-results-list {
list-style: none;
padding: 0;
}

.os-search-results-list-item {
background-color: #00504721;
border-radius: .5rem;
margin-bottom: .5rem;
padding: .5rem;
}

.os-search-results-highlights {
border-bottom: .1rem solid #d9d9d9;
padding-bottom: .5rem;
}

.os-search-error-message {
color: $os-wrong-answer-border-color;
text-align: center;
}

.os-search-no-results-message {
color: $os-selected-focus-border-color;
text-align: center;
}
Loading

0 comments on commit b6cbb44

Please sign in to comment.