Skip to content

Commit

Permalink
feat: implement search highlight (#205)
Browse files Browse the repository at this point in the history
* feat: implement search highlight

* fix: add highlight on `lang` as well

* fix: handle special characters

* fix: handle empty spaces
  • Loading branch information
ansh-saini authored Oct 29, 2024
1 parent 17837ac commit 0512e66
Show file tree
Hide file tree
Showing 3 changed files with 87 additions and 35 deletions.
78 changes: 61 additions & 17 deletions components/ProjectCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ interface ProjectCardProps {
project: Project;
vertical?: boolean;
preload?: boolean;
githubColors: GitHubColors
githubColors: GitHubColors;
searchQuery?: string;
}

interface ProjectCardImageProps {
Expand Down Expand Up @@ -43,19 +44,53 @@ function ProjectCardImage({ project, preload }: ProjectCardImageProps) {
}

interface ProjectCardBodyProps {
githubColors: GitHubColors,
project: Project
githubColors: GitHubColors;
project: Project;
searchQuery?: string;
}

function ProjectCardBody(props: ProjectCardBodyProps) {
const {
name,
description,
repo,
link,
lang,
topics,
} = props.project;
const escapeSpecialCharacters = (string: string) => {
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
};

const highlightText = (text: string, query: string) => {
query = query.trim();
if (!query) return text;

try {
const escapedQuery = escapeSpecialCharacters(query);
const parts = text.split(new RegExp(`(${escapedQuery})`, 'gi'));
return parts.map((part, index) =>
part.toLowerCase() === query.toLowerCase() ? (
<mark key={index}>{part}</mark>
) : (
part
),
);
} catch (e) {
return text;
}
};

function ProjectCardBody({
project,
searchQuery = '',
...props
}: ProjectCardBodyProps) {
const { repo, link, description, lang } = project;

const name = highlightText(project.name, searchQuery);

const topics = (() => {
if (project.topics.length === 0) return [];
return project.topics.map((topic, index) => (
<span key={index}>
{highlightText(topic, searchQuery)}
{index < project.topics.length - 1 && ', '}
</span>
));
})();

return (
<div className="card-body">
<h3 className="mt-1">
Expand All @@ -70,10 +105,10 @@ function ProjectCardBody(props: ProjectCardBodyProps) {
: 'black',
}}
></span>{' '}
{lang || 'Markdown'}
{topics.length > 0 && <span>{topics.join(', ')}</span>}
{highlightText(lang, searchQuery) || 'Markdown'}
{topics.length > 0 && <span>{topics}</span>}
</p>
<p>{description}</p>
<p>{highlightText(description, searchQuery)}</p>
<ELink link={repo}>GitHub Repository</ELink>
</div>
);
Expand All @@ -86,12 +121,17 @@ function ProjectCard({
vertical = false,
preload = false,
githubColors,
searchQuery = '',
}: ProjectCardProps): JSX.Element {
if (vertical) {
return (
<div className="card">
<ProjectCardImage project={project} preload={preload} />
<ProjectCardBody project={project} githubColors={githubColors} />
<ProjectCardBody
searchQuery={searchQuery}
project={project}
githubColors={githubColors}
/>
</div>
);
}
Expand All @@ -102,7 +142,11 @@ function ProjectCard({
<ProjectCardImage project={project} preload={preload} />
</div>
<div className="col-6">
<ProjectCardBody project={project} githubColors={githubColors} />
<ProjectCardBody
searchQuery={searchQuery}
project={project}
githubColors={githubColors}
/>
</div>
</div>
</div>
Expand Down
37 changes: 20 additions & 17 deletions components/SearchFilter/SearchFilter.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import React, { ChangeEvent, useState, useEffect } from 'react';
import React, { ChangeEvent, useEffect } from 'react';
import Searchbar from './Searchbar';
import { Project } from '../../util';

interface SearchFilterProps {
projects: Project[];
setFilteredProjects: (projectList: Project[]) => void;
searchQuery: string;
setSearchQuery: (search: string) => void;
}

function lowercaseRemove(s: string) {
Expand All @@ -16,12 +18,14 @@ function lowercaseRemove(s: string) {
return newString;
}

function SearchFilter({projects, setFilteredProjects}: SearchFilterProps): JSX.Element {

const [searchbarText, setSearchbarText] = useState('');

function SearchFilter({
projects,
setFilteredProjects,
setSearchQuery,
searchQuery,
}: SearchFilterProps): JSX.Element {
const handleSearchInput = (e: ChangeEvent<HTMLInputElement>) => {
setSearchbarText(e.target.value);
setSearchQuery(e.target.value);
};

const filterProjectsBySearchText = (project: Project) => {
Expand All @@ -30,21 +34,20 @@ function SearchFilter({projects, setFilteredProjects}: SearchFilterProps): JSX.E
// remove - and _ and white spaces from search, and make it lowercase to make it easier for the user to
// search things without typing the exact name
// e.g. if the user searches "devpathways" or "dev pathways" they should still be able to see "Dev-Pathways"
const search = lowercaseRemove(searchbarText);
const search = lowercaseRemove(searchQuery);

const {
name,
description,
lang,
topics,
} = project;
const { name, description, lang, topics } = project;

// can search by name, description, or language
const lowercaseName = lowercaseRemove(name);
const lowercaseDescription = lowercaseRemove(description);
const lowercaseLang = lowercaseRemove(lang);

if (lowercaseName.includes(search) || lowercaseDescription.includes(search) || lowercaseLang.includes(search)) {
if (
lowercaseName.includes(search) ||
lowercaseDescription.includes(search) ||
lowercaseLang.includes(search)
) {
return true;
}

Expand All @@ -64,16 +67,16 @@ function SearchFilter({projects, setFilteredProjects}: SearchFilterProps): JSX.E
useEffect(() => {
const tempProjects = projects.filter(filterProjectsBySearchText);
setFilteredProjects(tempProjects);
}, [searchbarText]);
}, [searchQuery]);

return (
<div>
<Searchbar
value={searchbarText}
value={searchQuery}
onChange={(e: ChangeEvent<HTMLInputElement>) => handleSearchInput(e)}
/>
</div>
);
}

export default SearchFilter;
export default SearchFilter;
7 changes: 6 additions & 1 deletion pages/projects.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import React, { useState } from 'react';
import Layout from '../components/Layout';
import ProjectCard from '../components/ProjectCard';
import SearchFilter from '../components/SearchFilter/SearchFilter';
import { getProjects, Project, GitHubColors, getGithubColors } from '../util';
import { getGithubColors, getProjects, GitHubColors, Project } from '../util';

interface ProjectsProps {
projects: Project[];
Expand All @@ -15,6 +15,8 @@ function Projects({ projects, githubColors }: ProjectsProps): JSX.Element {
// projects is a master list of all the projects that we fetched, filteredProjects is the one that we render
// to the user
const [filteredProjects, setFilteredProjects] = useState(projects);
const [searchQuery, setSearchQuery] = useState('');


return (
<Layout>
Expand Down Expand Up @@ -43,6 +45,8 @@ function Projects({ projects, githubColors }: ProjectsProps): JSX.Element {
<SearchFilter
projects={projects}
setFilteredProjects={setFilteredProjects}
setSearchQuery={setSearchQuery}
searchQuery={searchQuery}
/>
<hr />
<div className="row same-height-grid">
Expand All @@ -51,6 +55,7 @@ function Projects({ projects, githubColors }: ProjectsProps): JSX.Element {
return (
<div className="col-4" key={project.name}>
<ProjectCard
searchQuery={searchQuery}
project={project}
vertical
preload={i < 3}
Expand Down

0 comments on commit 0512e66

Please sign in to comment.