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:
parent
3c220c5dc6
commit
0b91771e90
@ -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",
|
||||||
|
@ -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"
|
||||||
}
|
}
|
||||||
|
@ -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) => {
|
||||||
|
@ -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]',
|
||||||
|
}}
|
||||||
|
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}
|
||||||
/>
|
/>
|
||||||
</form>
|
<div className="!absolute left-3 top-[13px]">
|
||||||
|
<i>^</i>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default SearchBar;
|
export default forwardRef(SearchBar);
|
||||||
|
@ -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>
|
93
packages/frontend/src/components/projects/ProjectSearch.tsx
Normal file
93
packages/frontend/src/components/projects/ProjectSearch.tsx
Normal 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;
|
@ -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>
|
||||||
|
@ -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;
|
||||||
|
25
yarn.lock
25
yarn.lock
@ -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==
|
||||||
|
Loading…
Reference in New Issue
Block a user