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 13 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
201 changes: 170 additions & 31 deletions src/components/SearchBlock.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
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'

interface SearchBlockProps {
versionId: string
Expand Down Expand Up @@ -39,10 +40,16 @@
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 [totalHitsDisplayed, setTotalHitsDisplayed] = useState(0)
rnathuji marked this conversation as resolved.
Show resolved Hide resolved
const [groupedHits, setGroupedHits] = useState<UnitHits | undefined>(undefined)
const fetchContent = async (): Promise<void> => {
try {
const response = filter !== undefined
Expand All @@ -55,19 +62,56 @@

const data: SearchResults = await response.json()
setSearchResults(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
}
})
setTotalHitsDisplayed(totalTeacherOnlyHits)
}

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

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

View check run for this annotation

Codecov / codecov/patch

src/components/SearchBlock.tsx#L90

Added line #L90 was not covered by tests
}
const unitHits: UnitHits = {}

searchResults.hits.hits.forEach((hit) => {
const unitName = hit._source.section
if (unitName in unitHits) {
unitHits[unitName].push(hit)
} else {
unitHits[unitName] = [hit]
}
})

setGroupedHits(unitHits)
}

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,54 +135,149 @@
{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 ? totalHitsDisplayed : 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)
calculateTotalTeacherOnlyHits(searchResults)
}}
/>
</div>
</div>
</div>
}
</div>
</div>
{groupedHits !== undefined &&
<div className='os-search-results-container'>
{teacherContentOnly
? <div className='os-raise-bootstrap'>
<div className="accordion" id="teacherContentAccordion">
{Object.keys(groupedHits).sort().map((unitName) => {
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={`#${unitName.slice(0, 6).replace(/\s/g, '')}`}
rnathuji marked this conversation as resolved.
Show resolved Hide resolved
aria-expanded="false"
aria-controls={`${unitName.slice(0, 6).replace(/\s/g, '')}`}
>
{unitName}
</button>
</h3>
<div id={`${unitName.slice(0, 6).replace(/\s/g, '')}`} className="accordion-collapse collapse" data-bs-parent="#teacherContentAccordion">
<div className="accordion-body">
{groupedHits[unitName].map((hit: Hit) => {
return (
<div key={hit._id}>
{hit._source.teacher_only && hit._source.lesson_page === '' && <p className='os-raise-text-bold os-search-results-text'>{hit._source.activity_name}</p>}
{hit._source.teacher_only && hit._source.lesson_page !== '' && <p className='os-raise-text-bold os-search-results-text'>{`${hit._source.activity_name}; ${hit._source.lesson_page}`}</p>}
{hit._source.teacher_only && hit.highlight.visible_content?.map((content: string) => (
<p ref={contentRefCallback} className='os-search-results-highlights' dangerouslySetInnerHTML={{ __html: content }}></p>
))}
{hit._source.teacher_only && hit.highlight.lesson_page?.map((page: string) => (
<p ref={contentRefCallback} className='os-search-results-highlights' dangerouslySetInnerHTML={{ __html: page }}></p>
))}
{hit._source.teacher_only && hit.highlight.activity_name?.map((activity: string) => (
<p ref={contentRefCallback} className='os-search-results-highlights' dangerouslySetInnerHTML={{ __html: activity }}></p>
))}
</div>
)
})}
</div>
</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 className='os-raise-bootstrap'>
<div className="accordion" id="teacherStudentContentAccordion">
{Object.keys(groupedHits).sort().map((unitName) => {
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={`#${unitName.slice(0, 6).replace(/\s/g, '')}`}
aria-expanded="false"
aria-controls={`${unitName.slice(0, 6).replace(/\s/g, '')}`}
>
{unitName}
</button>
</h3>
<div id={`${unitName.slice(0, 6).replace(/\s/g, '')}`} className="accordion-collapse collapse" data-bs-parent="#teacherStudentContentAccordion">
<div className="accordion-body">
{groupedHits[unitName].map((hit: Hit) => {
return (
<div key={hit._id}>
<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._source.lesson_page === '' && <p className='os-raise-text-bold os-search-results-text'>{hit._source.activity_name}</p>}
{hit._source.lesson_page !== '' && <p className='os-raise-text-bold os-search-results-text'>{`${hit._source.activity_name}; ${hit._source.lesson_page}`}</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>
}
</>
}
{searchResults !== undefined && searchResults.hits.total.value === 0 &&
<div className='os-search-results-count-container'>

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

View check run for this annotation

Codecov / codecov/patch

src/components/SearchBlock.tsx#L275

Added line #L275 was not covered by tests
<h3 className='os-search-heading'>Search Results</h3>
<div>
<p className='os-search-no-results-message'>Your query did not produce any results. Please try again.</p>
<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
1 change: 1 addition & 0 deletions src/styles/bootstrap.scss
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
@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

// The following are copied from bootstrap.css where the definitions under
// :root are otherwise not applied when namespaced so manually defined here
Expand Down
61 changes: 46 additions & 15 deletions src/styles/interactives.scss
Original file line number Diff line number Diff line change
Expand Up @@ -175,42 +175,73 @@ 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 {
margin-bottom: .5rem;
border-bottom: .1rem solid #d9d9d9;
padding-bottom: .5rem;
font-size: .875rem;
}

.os-search-error-message {
color: $os-wrong-answer-border-color;
text-align: center;
.os-search-content-type {
text-decoration: underline;
rnathuji marked this conversation as resolved.
Show resolved Hide resolved
font-weight: 700;
}

.os-search-no-results-message {
color: $os-selected-focus-border-color;
.os-search-heading {
font-size: 1rem;
font-weight: 700;
}

.os-search-results-text {
font-size: .875rem;
}

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