Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

update search component #156

Merged
merged 15 commits into from
May 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
178 changes: 145 additions & 33 deletions src/components/SearchBlock.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { Formik, Form, Field } from 'formik'
import { ENV } from '../lib/env'
import { useState } from 'react'
import { mathifyElement } from '../lib/math'
import { useState, useCallback } from 'react'
import { v4 as uuidv4 } from 'uuid'

interface SearchBlockProps {
versionId: string
Expand Down Expand Up @@ -39,10 +41,18 @@
hits: Hits
}

type UnitHits = Record<string, Hit[]>

export const SearchBlock = ({ versionId, filter }: SearchBlockProps): JSX.Element => {
const [query, setQuery] = useState('')
const [searchResults, setSearchResults] = useState<SearchResults | undefined>(undefined)
const [errorMessage, setErrorMessage] = useState('')
const [teacherContentOnly, setTeacherContentOnly] = useState(false)
const [searchTerm, setSearchTerm] = useState('')
const [totalTeacherHitsOnly, setTotalTeacherHitsOnly] = useState(0)
const [groupedStudentTeacherHits, setGroupedStudentTeacherHits] = useState<UnitHits | undefined>(undefined)
const [groupedTeacherHits, setGroupedTeacherHits] = useState<UnitHits | undefined>(undefined)
const [sortedHits, setSortedHits] = useState<UnitHits | undefined>(undefined)
const fetchContent = async (): Promise<void> => {
try {
const response = filter !== undefined
Expand All @@ -55,19 +65,69 @@

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

const contentRefCallback = useCallback((node: HTMLParagraphElement | null): void => {
if (node != null) {
mathifyElement(node)
}
}, [])

const calculateTotalTeacherOnlyHits = (searchResults: SearchResults): void => {
rnathuji marked this conversation as resolved.
Show resolved Hide resolved
let totalTeacherOnlyHits: number = 0
searchResults.hits.hits.forEach((hit) => {
if (hit._source.teacher_only) {
totalTeacherOnlyHits += 1
}
})
setTotalTeacherHitsOnly(totalTeacherOnlyHits)
}

const groupHitsByUnit = (searchResults: SearchResults): void => {
if (searchResults === undefined) {
return

Check warning on line 94 in src/components/SearchBlock.tsx

View check run for this annotation

Codecov / codecov/patch

src/components/SearchBlock.tsx#L94

Added line #L94 was not covered by tests
}
const unitStudentTeacherHits: UnitHits = {}
const unitTeacherHits: UnitHits = {}

searchResults.hits.hits.forEach((hit) => {
const unitName = hit._source.section

if (hit._source.teacher_only) {
if (unitName in unitTeacherHits) {
unitTeacherHits[unitName].push(hit)

Check warning on line 104 in src/components/SearchBlock.tsx

View check run for this annotation

Codecov / codecov/patch

src/components/SearchBlock.tsx#L104

Added line #L104 was not covered by tests
} else {
unitTeacherHits[unitName] = [hit]
}
}

if (unitName in unitStudentTeacherHits) {
unitStudentTeacherHits[unitName].push(hit)
} else {
unitStudentTeacherHits[unitName] = [hit]
}
})

setGroupedStudentTeacherHits(unitStudentTeacherHits)
setGroupedTeacherHits(unitTeacherHits)
teacherContentOnly ? setSortedHits(unitTeacherHits) : setSortedHits(unitStudentTeacherHits)
}

const handleSubmit = async (): Promise<void> => {
if (query.trim() === '') {
setErrorMessage('Input cannot be empty')
return
}
setSearchResults(undefined)
setErrorMessage('')
setSearchTerm(query)
setTeacherContentOnly(false)
await fetchContent()
}
return (
Expand All @@ -91,53 +151,105 @@
{isSubmitting
? <div className="os-raise-bootstrap">
<div className="text-center">
<div className="spinner-border mt-3 text-success" role="status">
<div className="spinner-border mt-3 text-primary" 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 className='os-raise-bootstrap'>
<div className='os-text-center mt-4'>
<button type="submit" disabled={isSubmitting} className="os-btn btn-outline-primary">Search</button>
</div>
</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 className='os-search-results-count-container'>
<h3 className='os-search-heading'>Search Results</h3>
<div>
<p className='os-search-magnifying-glass os-search-results-text'>
Displaying {teacherContentOnly ? totalTeacherHitsOnly : searchResults.hits.hits.length} of out {searchResults.hits.total.value} results for <span className='os-raise-text-bold'>{searchTerm}</span>
</p>
{filter === undefined &&
<div className='os-raise-d-flex-nowrap os-raise-justify-content-evenly os-search-teacher-content-toggle-container'>
<p className='os-raise-mb-0 os-search-results-text'>Teacher Content Only</p>
<div className='os-raise-bootstrap'>
<div className="form-check form-switch">
<input
className="form-check-input"
type="checkbox"
id="flexSwitchCheckDefault"
onChange={() => {
setTeacherContentOnly(!teacherContentOnly)
!teacherContentOnly ? setSortedHits(groupedTeacherHits) : setSortedHits(groupedStudentTeacherHits)
}}
/>
</div>
</div>
</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>
</div>
{sortedHits !== undefined &&
<div className='os-search-results-container'>
<div className='os-raise-bootstrap'>
<div className="accordion" id="teacherContentAccordion">
{Object.keys(sortedHits).sort().map((unitName) => {
const uniqueId = `auto-${uuidv4()}`
return (
<div className="accordion-item" key={unitName}>
<h3 className="accordion-header">
<button
className="accordion-button collapsed os-raise-text-bold"
type="button" data-bs-toggle="collapse"
data-bs-target={`#${uniqueId}`}
aria-expanded="false"
aria-controls={uniqueId}
>
{unitName}
</button>
</h3>
<div id={uniqueId} className="accordion-collapse collapse" data-bs-parent="#teacherContentAccordion">
<div className="accordion-body">
{sortedHits[unitName].map((hit: Hit) => {
return (
<div className='os-search-hit' key={hit._id}>
<p className='os-raise-text-bold os-search-results-text'>{hit._source.activity_name}</p>
<p className='os-raise-text-bold os-search-results-text'>{hit._source.lesson_page}</p>
<p className='os-search-content-type'>{hit._source.teacher_only ? 'Teacher Content' : 'Student Content'}</p>
rnathuji marked this conversation as resolved.
Show resolved Hide resolved
{hit.highlight.visible_content?.map((content: string) => (
<p ref={contentRefCallback} className='os-search-results-highlights' dangerouslySetInnerHTML={{ __html: content }}></p>
))}
{hit.highlight.lesson_page?.map((page: string) => (
<p ref={contentRefCallback} className='os-search-results-highlights' dangerouslySetInnerHTML={{ __html: page }}></p>
))}
{hit.highlight.activity_name?.map((activity: string) => (
<p ref={contentRefCallback} className='os-search-results-highlights' dangerouslySetInnerHTML={{ __html: activity }}></p>
))}
</div>
)
})}
</div>
</div>
</div>
)
})}
</div>
</li>
))}
</ul>
</div>
</div>
</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 className='os-search-results-count-container'>

Check warning on line 248 in src/components/SearchBlock.tsx

View check run for this annotation

Codecov / codecov/patch

src/components/SearchBlock.tsx#L248

Added line #L248 was not covered by tests
<h3 className='os-search-heading'>Search Results</h3>
<div>
<p className='os-search-magnifying-glass os-search-results-text'>No results found for <span className='os-raise-text-bold'>{searchTerm}</span></p>
</div>
</div>
}
</div>
Expand Down
2 changes: 2 additions & 0 deletions src/styles/bootstrap.scss
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@
@import "bootstrap/scss/utilities";
@import "bootstrap/scss/utilities/api";
@import "bootstrap/scss/spinners";
@import "bootstrap/scss/accordion";
rnathuji marked this conversation as resolved.
Show resolved Hide resolved
@import "bootstrap/scss/transitions";

// The following are copied from bootstrap.css where the definitions under
// :root are otherwise not applied when namespaced so manually defined here
Expand Down
72 changes: 55 additions & 17 deletions src/styles/interactives.scss
Original file line number Diff line number Diff line change
Expand Up @@ -175,42 +175,80 @@ math-field::part(menu-toggle) {
}

// Search Styles
%os-search-container-border {
border: 1px solid $os-light-gray;
border-radius: 5px;
}

.os-search-form {
@extend %os-search-container-border;

display: flex;
flex-direction: column;
background-color: #00504721;
border: 1px solid #00504721;
border-radius: 5px;
background-color: #FFF;
padding: 1rem
}

.os-search-form input {
@extend %os-search-container-border;

padding: .5rem;
}

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

.os-search-results-list {
list-style: none;
padding: 0;
.os-search-results-count-container {
@extend %os-search-container-border;

padding: 1rem;
margin-top: 1rem;
}

.os-search-results-list-item {
background-color: #00504721;
border-radius: .5rem;
margin-bottom: .5rem;
.os-search-magnifying-glass {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='1em' height='1em' viewBox='0 0 24 24'%3E%3Cpath fill='%23000' d='M9.5 3A6.5 6.5 0 0 1 16 9.5c0 1.61-.59 3.09-1.56 4.23l.27.27h.79l5 5l-1.5 1.5l-5-5v-.79l-.27-.27A6.52 6.52 0 0 1 9.5 16A6.5 6.5 0 0 1 3 9.5A6.5 6.5 0 0 1 9.5 3m0 2C7 5 5 7 5 9.5S7 14 9.5 14S14 12 14 9.5S12 5 9.5 5'/%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: 0 50%;
background-size: 1.5rem;
padding-left: 2rem;
}

.os-search-teacher-content-toggle-container {
@extend %os-search-container-border;

padding: .5rem;
}

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

.os-search-error-message {
color: $os-wrong-answer-border-color;
text-align: center;
.os-search-hit:not(:first-child) {
margin-top: .5rem;
}

.os-search-results-highlights {
margin-bottom: .5rem;
font-size: .875rem;
color: #444;
}

.os-search-content-type {
font-size: 1rem;
font-weight: 700;
}

.os-search-heading {
font-size: 1rem;
font-weight: 700;
}

.os-search-no-results-message {
color: $os-selected-focus-border-color;
.os-search-results-text {
font-size: .875rem;
}

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