Implement projects search functionality in home page (#11)

* Implement search functionality with downshift

* Show project details in suggestions and handle selection

* Rename component to ProjectSearch

* Use renamed component
This commit is contained in:
Nabarun Gogoi 2023-12-20 09:59:02 +05:30 committed by GitHub
parent 3c220c5dc6
commit 0b91771e90
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 219 additions and 58 deletions

View File

@ -11,6 +11,7 @@
"@types/node": "^16.18.68", "@types/node": "^16.18.68",
"@types/react": "^18.2.42", "@types/react": "^18.2.42",
"@types/react-dom": "^18.2.17", "@types/react-dom": "^18.2.17",
"downshift": "^8.2.3",
"luxon": "^3.4.4", "luxon": "^3.4.4",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",

View File

@ -2,14 +2,16 @@
{ {
"id": 1, "id": 1,
"icon": "^", "icon": "^",
"name": "iglotools",
"title": "Iglotools", "title": "Iglotools",
"domain": "iglotools.com", "organization": "Airfoil",
"domain": "iglotools.co",
"createdAt": "2023-12-07T04:20:00", "createdAt": "2023-12-07T04:20:00",
"createdBy": "Bob", "createdBy": "Alice",
"deployment": "iglotools.snowball.com", "deployment": "iglotools.snowballtools.co",
"source": "feature/add-remote-control", "source": "feature/add-remote-control",
"latestCommit": { "latestCommit": {
"message": "Subscription added", "message": "subscription added",
"createdAt": "2023-12-11T04:20:00", "createdAt": "2023-12-11T04:20:00",
"branch": "main" "branch": "main"
} }
@ -17,14 +19,16 @@
{ {
"id": 2, "id": 2,
"icon": "^", "icon": "^",
"title": "snowball-starter-kit", "name": "snowball-starter-kit",
"domain": "snowball-starter-kit.com", "title": "Snowball Starter Kit",
"organization": "Snowball",
"domain": "starterkit.snowballtools.com",
"createdAt": "2023-12-04T04:20:00", "createdAt": "2023-12-04T04:20:00",
"createdBy": "Erin", "createdBy": "Bob",
"deployment": "snowball-starter-kit.com", "deployment": "deploy.snowballtools.com",
"source": "prod/add-docker-compose", "source": "prod/add-docker-compose",
"latestCommit": { "latestCommit": {
"message": "404 added", "message": "component updates",
"createdAt": "2023-12-11T04:20:00", "createdAt": "2023-12-11T04:20:00",
"branch": "staging" "branch": "staging"
} }
@ -32,14 +36,16 @@
{ {
"id": 3, "id": 3,
"icon": "^", "icon": "^",
"title": "passkeys-demo", "name": "web3-android",
"domain": "passkeys-demo.com", "title": "Web3 Android",
"organization": "Personal",
"domain": "web3fordroids.com",
"createdAt": "2023-12-01T04:20:00", "createdAt": "2023-12-01T04:20:00",
"createdBy": "Charlie", "createdBy": "Charlie",
"deployment": "passkeys-demo.com", "deployment": "deploy.web3fordroids.com",
"source": "dev/style-page", "source": "dev/style-page",
"latestCommit": { "latestCommit": {
"message": "Design system integrated", "message": "No repo connected",
"createdAt": "2023-12-01T04:20:00", "createdAt": "2023-12-01T04:20:00",
"branch": "main" "branch": "main"
} }
@ -47,14 +53,50 @@
{ {
"id": 4, "id": 4,
"icon": "^", "icon": "^",
"title": "watcher-tool", "name": "passkeys-demo",
"domain": "azimuth-watcher.com", "title": "Passkeys Demo",
"organization": "Airfoil",
"domain": "passkeys.iglootools.xyz",
"createdAt": "2023-12-01T04:20:00",
"createdBy": "David",
"deployment": "demo.passkeys.xyz",
"source": "dev/style-page",
"latestCommit": {
"message": "hello world",
"createdAt": "2023-12-01T04:20:00",
"branch": "main"
}
},
{
"id": 5,
"icon": "^",
"name": "iglootools",
"title": "Iglootools",
"organization": "Airfoil",
"domain": "iglotools.xyz",
"createdAt": "2023-12-11T04:20:00", "createdAt": "2023-12-11T04:20:00",
"createdBy": "Alice", "createdBy": "Erin",
"deployment": "azimuth-watcher.com", "deployment": "staging.snowballtools.com",
"source": "prod/fix-error", "source": "prod/fix-error",
"latestCommit": { "latestCommit": {
"message": "Listen for subscription", "message": "404 added",
"createdAt": "2023-12-09T04:20:00",
"branch": "main"
}
},
{
"id": 6,
"icon": "^",
"name": "iglootools",
"title": "Iglootools",
"organization": "Airfoil",
"domain": "iglotools.xyz",
"createdAt": "2023-12-11T04:20:00",
"createdBy": "Frank",
"deployment": "iglotools.snowballtools.com",
"source": "prod/fix-error",
"latestCommit": {
"message": "design system integrated",
"createdAt": "2023-12-09T04:20:00", "createdAt": "2023-12-09T04:20:00",
"branch": "prod" "branch": "prod"
} }

View File

@ -16,7 +16,7 @@ const RepositoryList = () => {
/> />
</div> </div>
<div className="basis-2/3"> <div className="basis-2/3">
<SearchBar handler={() => {}} placeholder="Search for repositorry" /> <SearchBar onChange={() => {}} placeholder="Search for repository" />
</div> </div>
</div> </div>
{repositoryDetails.map((repo, key) => { {repositoryDetails.map((repo, key) => {

View File

@ -1,42 +1,42 @@
import React from 'react'; import React, { ChangeEventHandler, forwardRef } from 'react';
import { useForm, SubmitHandler } from 'react-hook-form';
import { Input } from '@material-tailwind/react';
interface SearchBarProps { interface SearchBarProps {
handler: (searchText: SearchInputs) => void; onChange: ChangeEventHandler<HTMLInputElement>;
value?: string;
placeholder?: string; placeholder?: string;
} }
interface SearchInputs { const SearchBar: React.ForwardRefRenderFunction<
search: string; HTMLInputElement,
} SearchBarProps
> = ({ value, onChange, placeholder = 'Search', ...props }, ref) => {
const SearchBar: React.FC<SearchBarProps> = ({
handler,
placeholder = 'Search',
}) => {
const { register, handleSubmit } = useForm({
defaultValues: {
search: '',
},
});
const onSubmit: SubmitHandler<SearchInputs> = (data) => {
handler(data);
};
return ( return (
<div className="w-full flex"> <div className="relative flex w-full gap-2">
<div className="text-gray-300">^</div> <Input
<form onSubmit={handleSubmit(onSubmit)}> variant="standard"
<input onChange={onChange}
{...register('search')} value={value}
type="text" type="search"
placeholder={placeholder} placeholder={placeholder}
className="grow text-gray-700 border-none focus:outline-none text-xs" containerProps={{
/> className: 'min-w-[288px]',
</form> }}
className="pl-9 placeholder:text-blue-gray-300 focus:!border-blue-gray-300"
labelProps={{
className: 'before:content-none after:content-none',
}}
// TODO: Debug issue: https://github.com/creativetimofficial/material-tailwind/issues/427
crossOrigin={undefined}
ref={ref}
{...props}
/>
<div className="!absolute left-3 top-[13px]">
<i>^</i>
</div>
</div> </div>
); );
}; };
export default SearchBar; export default forwardRef(SearchBar);

View File

@ -1,8 +1,8 @@
import React from 'react'; import React from 'react';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { relativeTime } from '../utils/time'; import { relativeTime } from '../../utils/time';
import { ProjectDetails } from '../types/project'; import { ProjectDetails } from '../../types/project';
interface ProjectCardProps { interface ProjectCardProps {
project: ProjectDetails; project: ProjectDetails;
@ -15,7 +15,7 @@ const ProjectCard: React.FC<ProjectCardProps> = ({ project }) => {
<div>{project.icon}</div> <div>{project.icon}</div>
<div className="grow"> <div className="grow">
<Link to={`projects/${project.id}`} className="text-sm text-gray-700"> <Link to={`projects/${project.id}`} className="text-sm text-gray-700">
{project.title} {project.name}
</Link> </Link>
<p className="text-sm text-gray-400">{project.domain}</p> <p className="text-sm text-gray-400">{project.domain}</p>
</div> </div>

View File

@ -0,0 +1,93 @@
import React, { useState } from 'react';
import { useCombobox } from 'downshift';
import {
List,
ListItem,
ListItemPrefix,
Card,
Typography,
} from '@material-tailwind/react';
import SearchBar from '../SearchBar';
import { ProjectDetails } from '../../types/project';
import projectsData from '../../assets/projects.json';
interface ProjectsSearchProps {
onChange?: (data: ProjectDetails) => void;
}
const ProjectSearch = ({ onChange }: ProjectsSearchProps) => {
const [items, setItems] = useState<ProjectDetails[]>([]);
const [selectedItem, setSelectedItem] = useState<ProjectDetails | null>(null);
const {
isOpen,
getMenuProps,
getInputProps,
getItemProps,
highlightedIndex,
} = useCombobox({
onInputValueChange({ inputValue }) {
setItems(
inputValue
? projectsData.filter((project) => project.title.includes(inputValue))
: [],
);
},
items,
itemToString(item) {
return item ? item.title : '';
},
selectedItem,
onSelectedItemChange: ({ selectedItem: newSelectedItem }) => {
if (newSelectedItem) {
setSelectedItem(newSelectedItem);
if (onChange) {
onChange(newSelectedItem);
}
}
},
});
return (
<div className="relative">
<SearchBar {...getInputProps()} />
<Card
className={`absolute w-1/2 max-h-100 overflow-y-scroll ${
!(isOpen && items.length) && 'hidden'
}`}
>
<p>Suggestions</p>
<List {...getMenuProps()}>
{items.map((item, index) => (
<ListItem
selected={highlightedIndex === index || selectedItem === item}
key={item.id}
{...getItemProps({ item, index })}
>
<ListItemPrefix>
<i>^</i>
</ListItemPrefix>
<div>
<Typography variant="h6" color="blue-gray">
{item.title}
</Typography>
<Typography
variant="small"
color="gray"
className="font-normal"
>
{item.organization}
</Typography>
</div>
</ListItem>
))}
</List>
</Card>
</div>
);
};
export default ProjectSearch;

View File

@ -3,17 +3,17 @@ import { Link } from 'react-router-dom';
import { Button } from '@material-tailwind/react'; import { Button } from '@material-tailwind/react';
import SearchBar from '../components/SearchBar'; import ProjectCard from '../components/projects/ProjectCard';
import ProjectCard from '../components/ProjectCard';
import HorizontalLine from '../components/HorizontalLine'; import HorizontalLine from '../components/HorizontalLine';
import projectsDetail from '../assets/projects.json'; import projectsDetail from '../assets/projects.json';
import ProjectSearch from '../components/projects/ProjectSearch';
const Projects = () => { const Projects = () => {
return ( return (
<div className="bg-white rounded-3xl h-full"> <div className="bg-white rounded-3xl h-full">
<div className="flex p-4"> <div className="flex p-4">
<div className="grow"> <div className="grow">
<SearchBar handler={() => {}} /> <ProjectSearch onChange={() => {}} />
</div> </div>
<div className="text-gray-300">^</div> <div className="text-gray-300">^</div>
<div className="text-gray-300">^</div> <div className="text-gray-300">^</div>

View File

@ -1,6 +1,8 @@
export interface ProjectDetails { export interface ProjectDetails {
icon: string; icon: string;
name: string;
title: string; title: string;
organization: string;
domain: string; domain: string;
id: number; id: number;
createdAt: string; createdAt: string;

View File

@ -1146,6 +1146,13 @@
dependencies: dependencies:
regenerator-runtime "^0.14.0" regenerator-runtime "^0.14.0"
"@babel/runtime@^7.22.15":
version "7.23.6"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.23.6.tgz#c05e610dc228855dc92ef1b53d07389ed8ab521d"
integrity sha512-zHd0eUrf5GZoOWVCXp6koAKQTfZV07eit6bGPmJgnZdnSAvvZee6zniW2XMF7Cmc4ISOOnPy3QaSiIJGJkVEDQ==
dependencies:
regenerator-runtime "^0.14.0"
"@babel/template@^7.22.15", "@babel/template@^7.3.3": "@babel/template@^7.22.15", "@babel/template@^7.3.3":
version "7.22.15" version "7.22.15"
resolved "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz" resolved "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz"
@ -4322,6 +4329,11 @@ compression@^1.7.4:
safe-buffer "5.1.2" safe-buffer "5.1.2"
vary "~1.1.2" vary "~1.1.2"
compute-scroll-into-view@^3.0.3:
version "3.1.0"
resolved "https://registry.yarnpkg.com/compute-scroll-into-view/-/compute-scroll-into-view-3.1.0.tgz#753f11d972596558d8fe7c6bcbc8497690ab4c87"
integrity sha512-rj8l8pD4bJ1nx+dAkMhV1xB5RuZEyVysfxJqB1pRchh1KVvwOv9b7CGB8ZfjTImVv2oF+sYMUkMZq6Na5Ftmbg==
concat-map@0.0.1: concat-map@0.0.1:
version "0.0.1" version "0.0.1"
resolved "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz" resolved "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz"
@ -5103,6 +5115,17 @@ dotenv@~16.3.1:
resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.3.1.tgz#369034de7d7e5b120972693352a3bf112172cc3e" resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.3.1.tgz#369034de7d7e5b120972693352a3bf112172cc3e"
integrity sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ== integrity sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ==
downshift@^8.2.3:
version "8.2.3"
resolved "https://registry.yarnpkg.com/downshift/-/downshift-8.2.3.tgz#27106a5d9f408a6f6f9350ca465801d07e52db87"
integrity sha512-1HkvqaMTZpk24aqnXaRDnT+N5JCbpFpW+dCogB11+x+FCtfkFX0MbAO4vr/JdXi1VYQF174KjNUveBXqaXTPtg==
dependencies:
"@babel/runtime" "^7.22.15"
compute-scroll-into-view "^3.0.3"
prop-types "^15.8.1"
react-is "^18.2.0"
tslib "^2.6.2"
duplexer@^0.1.1, duplexer@^0.1.2: duplexer@^0.1.1, duplexer@^0.1.2:
version "0.1.2" version "0.1.2"
resolved "https://registry.yarnpkg.com/duplexer/-/duplexer-0.1.2.tgz#3abe43aef3835f8ae077d136ddce0f276b0400e6" resolved "https://registry.yarnpkg.com/duplexer/-/duplexer-0.1.2.tgz#3abe43aef3835f8ae077d136ddce0f276b0400e6"
@ -10361,7 +10384,7 @@ react-is@^17.0.1:
resolved "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz" resolved "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz"
integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w== integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==
react-is@^18.0.0: react-is@^18.0.0, react-is@^18.2.0:
version "18.2.0" version "18.2.0"
resolved "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz" resolved "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz"
integrity sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w== integrity sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==