Skip to content

Commit

Permalink
Add search (#16)
Browse files Browse the repository at this point in the history
* Add articles search API
* Add main search input
  • Loading branch information
diegocouto authored Jun 19, 2021
1 parent 6472fa8 commit c9b606e
Show file tree
Hide file tree
Showing 27 changed files with 543 additions and 33 deletions.
4 changes: 4 additions & 0 deletions .babelrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"presets": ["next/babel"],
"plugins": [["styled-components", { "ssr": true }]]
}
1 change: 1 addition & 0 deletions .husky/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
_
4 changes: 4 additions & 0 deletions .husky/pre-commit
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"

yarn cache:update
1 change: 1 addition & 0 deletions articles/en/getting-started/welcome-aboard.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
---
title: 'Welcome aboard 👋'
description: 'We want to help you provide great customer support to all of your customers in a sane and sustainable way.'
tags: 'getting started, intro, presentation'
order: 1
updatedAt: '2021-04-29'
---
Expand Down
1 change: 1 addition & 0 deletions articles/pt-BR/primeiros-passos/seja-bem-vindo.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
---
title: 'Seja bem-vindo 👋'
description: 'Queremos ajudá-lo a fornecer um ótimo suporte para todos os seus clientes de uma forma simples e sustentável.'
tags: 'primeiros passos, introdução, apresentação'
order: 1
updatedAt: '2021-04-29'
---
Expand Down
Empty file added cache/.gitkeep
Empty file.
1 change: 1 addition & 0 deletions cache/data.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
[{"locale":"en","category":"getting-started","slug":"welcome-aboard","tags":"getting started, intro, presentation","title":"Welcome aboard 👋","description":"We want to help you provide great customer support to all of your customers in a sane and sustainable way."},{"locale":"pt-BR","category":"primeiros-passos","slug":"seja-bem-vindo","tags":"primeiros passos, introdução, apresentação","title":"Seja bem-vindo 👋","description":"Queremos ajudá-lo a fornecer um ótimo suporte para todos os seus clientes de uma forma simples e sustentável."}]
6 changes: 1 addition & 5 deletions components/lists/CategoriesList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,6 @@ export default function CategoriesList({ categories, locale }: Props) {
const { t } = useTranslation('categories');

const sortedCategories = categories.sort((a, b) => {
console.log(t(`${a.category}.title`));
console.log(t(`${a.category}.order`));
console.log(t(`${b.category}.order`));

return Number(t(`${a.category}.order`)) - Number(t(`${b.category}.order`));
});

Expand All @@ -28,7 +24,7 @@ export default function CategoriesList({ categories, locale }: Props) {
key={category}
title={t(`${category}.title`)}
description={t(`${category}.description`)}
path={category}
path={`/${category}`}
locale={locale}
/>
))}
Expand Down
9 changes: 2 additions & 7 deletions components/lists/ListItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ interface Props {
export default function ListItem({ title, description, path, locale }: Props) {
return (
<Item key={path}>
<Link href={path} locale={locale}>
<Link href={path} locale={locale} passHref={true}>
<ItemLink>
<ItemLabel>{title}</ItemLabel>
<ItemDescription>{description}</ItemDescription>
Expand All @@ -23,11 +23,6 @@ export default function ListItem({ title, description, path, locale }: Props) {
}

const Item = styled.li``;

const ItemLink = styled.a.attrs({
className:
'flex flex-col cursor-pointer border border-gray-50 bg-gray-50 rounded-xl p-6 space-y-1 hover:bg-white hover:border-gray-200',
})``;

const ItemLink = styled.a.attrs({ className: 'list-item focusable' })``;
const ItemLabel = styled.p.attrs({ className: 'font-medium text-lg text-primary-500' })``;
const ItemDescription = styled.p.attrs({ className: 'text-gray-500' })``;
103 changes: 103 additions & 0 deletions components/navigation/AppHeader/SearchInput.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import { useRouter } from 'next/router';
import React, { useRef, useState } from 'react';
import { useEffect } from 'react';
import { RiLoader5Line, RiSearchLine } from 'react-icons/ri';
import { useQuery } from 'react-query';
import styled from 'styled-components';

import { SearchResultType } from '../../../utils/datasource';
import { searchForArticles } from '../../../utils/libs/api';
import SearchResults from './SearchResults';

export default function SearchInput() {
const [isSearching, setIsSearching] = useState(false);
const [cursor, setCursor] = useState(0);
const [term, setTerm] = useState('');

const inputRef = useRef<HTMLInputElement>();
const router = useRouter();

const { isLoading, data } = useQuery<SearchResultType[]>(['searchResults', term], () =>
searchForArticles(term, router.locale)
);

const results = data || [];

useEffect(() => {
if (!isSearching) {
inputRef.current.blur();
}
}, [isSearching, inputRef]);

useEffect(() => {
if (cursor > results.length) {
setCursor(0);
}
}, [results]);

function handleKeyUp({ key }: React.KeyboardEvent<HTMLInputElement>) {
if (key === 'Escape') {
setIsSearching(false);
return;
}

if (key === 'ArrowDown' && cursor < results.length) {
setCursor(cursor + 1);
return;
}

if (cursor > 0) {
switch (key) {
case 'ArrowUp':
setCursor(cursor - 1);
break;
case 'Enter':
handleSelect(results[cursor - 1]);
}
}
}

function handleSelect({ item }: SearchResultType) {
const path = `/${item.category}/${item.slug}`;
const locale = item.locale;

setIsSearching(false);

router.push(path, path, { locale });
}

return (
<Container>
<InputContainer>
<Input
ref={inputRef}
onChange={({ target }) => setTerm(target.value)}
onFocus={() => setIsSearching(true)}
onKeyUp={handleKeyUp}
/>

<InputIconContainer>{isLoading ? <LoaderIcon /> : <SearchIcon />}</InputIconContainer>
</InputContainer>

{isSearching && <SearchResults cursor={cursor} results={results} onSelect={handleSelect} />}
</Container>
);
}

const Container = styled.div.attrs({ className: 'relative' })``;

const InputContainer = styled.div.attrs({
className: 'transition relative text-gray-400 focus-within:text-primary-500',
})``;

const InputIconContainer = styled.div.attrs({
className: 'flex absolute items-center inset-y-0 right-4',
})``;

const Input = styled.input.attrs({
placeholder: 'Search',
className: 'form-input focusable',
})``;

const LoaderIcon = styled(RiLoader5Line).attrs({ size: 24, className: 'animate-spin' })``;
const SearchIcon = styled(RiSearchLine).attrs({ size: 24, className: '' })``;
58 changes: 58 additions & 0 deletions components/navigation/AppHeader/SearchResults.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import useTranslation from 'next-translate/useTranslation';
import React from 'react';
import styled from 'styled-components';

import { SearchResultType } from '../../../utils/datasource';

export interface SelectableItem {
onSelect?: (param: SearchResultType) => void;
}

interface Props extends SelectableItem {
cursor: number;
results: SearchResultType[];
}

interface ItemProps extends SelectableItem {
item: SearchResultType['item'];
isActive?: boolean;
}

export default function SearchResults({ cursor, results, onSelect }: Props) {
return (
<>
{results?.length > 0 && (
<ItemsList>
{results.map(({ item }, index) => (
<SearchResultsItem key={item.slug} item={item} onSelect={onSelect} isActive={cursor === index + 1} />
))}
</ItemsList>
)}
</>
);
}

function SearchResultsItem({ item, isActive, onSelect }: ItemProps) {
const { t } = useTranslation('categories');

function handleSelect() {
onSelect?.({ item });
}

return (
<Item className={isActive ? 'search-result-item active' : 'search-result-item'}>
<ItemButton onClick={handleSelect}>
<ItemCategory>{t(`${item.category}.title`)}</ItemCategory>
<ItemLabel>{item.title}</ItemLabel>
</ItemButton>
</Item>
);
}

const ItemsList = styled.ul.attrs({ className: 'search-result-list' })``;

const Item = styled.li``;
const ItemButton = styled.button``;

const ItemCategory = styled.span.attrs({ className: 'search-result-description' })``;
const ItemLabel = styled.span.attrs({ className: 'search-result-title' })``;
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ import Link from 'next/link';
import React from 'react';
import styled from 'styled-components';

import { Brand } from '../../utils/constants/app';
import { Brand } from '../../../utils/constants/app';
import SearchInput from './SearchInput';

interface Props {
isCompact?: boolean;
Expand All @@ -28,11 +29,10 @@ export default function AppHeader({ isCompact }: Props) {
</NavigationActions>
</NavigationBar>

{!isCompact && (
<SearchBar>
<Title>{t('header.title')}</Title>
</SearchBar>
)}
<SearchBar>
{!isCompact && <Title>{t('header.title')}</Title>}
<SearchInput />
</SearchBar>
</Container>
</Wrapper>
);
Expand Down
9 changes: 8 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,21 @@
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start"
"start": "next start",
"cache:update": "ts-node utils/scripts/updateCache",
"prepare": "husky install"
},
"dependencies": {
"autoprefixer": "^10.2.5",
"fuse.js": "^6.4.6",
"gray-matter": "^4.0.2",
"next": "^10.2.0",
"next-translate": "^1.0.6",
"postcss": "^8.2.14",
"react": "17.0.2",
"react-dom": "17.0.2",
"react-icons": "^4.2.0",
"react-query": "^3.17.0",
"remark": "^12.0.1",
"remark-html": "^13.0.1",
"styled-components": "^5.3.0",
Expand All @@ -31,7 +36,9 @@
"eslint-plugin-prettier": "^3.1.4",
"eslint-plugin-react": "^7.21.3",
"eslint-plugin-simple-import-sort": "^5.0.3",
"husky": "^6.0.0",
"prettier": "^2.1.2",
"ts-node": "^10.0.0",
"typescript": "^4.2.4"
}
}
9 changes: 8 additions & 1 deletion pages/_app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,16 @@ import '../styles/globals.css';

import { AppProps } from 'next/dist/next-server/lib/router/router';
import React from 'react';
import { QueryClient, QueryClientProvider } from 'react-query';

const queryClient = new QueryClient();

function App({ Component, pageProps }: AppProps) {
return <Component {...pageProps} />;
return (
<QueryClientProvider client={queryClient}>
<Component {...pageProps} />
</QueryClientProvider>
);
}

export default App;
6 changes: 0 additions & 6 deletions pages/api/hello.ts

This file was deleted.

14 changes: 14 additions & 0 deletions pages/api/search.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { NextApiRequest, NextApiResponse } from 'next';

import Search from '../../utils/libs/search';

export default (req: NextApiRequest, res: NextApiResponse) => {
const term = req.query.term as string;
const locale = req.query.locale as string;

try {
res.json(Search.find(term, locale));
} catch {
res.json([]);
}
};
7 changes: 7 additions & 0 deletions styles/custom/buttons.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
.btn {
@apply font-medium rounded-xl transition ring-inset px-4 py-2 focus:outline-none focus:ring-2;
}

.btn-default {
@apply bg-gray-100 ring-gray-300 hover:bg-gray-200;
}
40 changes: 36 additions & 4 deletions styles/custom/components.css
Original file line number Diff line number Diff line change
@@ -1,11 +1,43 @@
.btn {
@apply font-medium rounded-xl transition ring-inset px-4 py-2 focus:outline-none focus:ring-2;
.list-item {
@apply transition flex flex-col cursor-pointer border border-gray-50 bg-gray-50 rounded-xl p-6 space-y-1 hover:bg-white hover:border-gray-200 focus:border-primary-500;
}

.btn-default {
@apply bg-gray-100 ring-gray-300 hover:bg-gray-200;
/**
* ------------------------------
* Search results
* ------------------------------
*/

.search-result-list {
@apply absolute rounded-lg shadow-lg overflow-hidden w-full mt-1;
}

.search-result-item button {
@apply group flex flex-col text-left bg-white cursor-pointer p-4 w-full hover:bg-primary-500 hover:text-white;
}

.search-result-item .search-result-description {
@apply text-xs text-gray-500 uppercase group-hover:text-primary-200;
}

.search-result-item .search-result-title {
@apply text-lg font-medium;
}

.search-result-item.active button {
@apply bg-primary-500 text-white;
}

.search-result-item.active .search-result-description {
@apply text-primary-200;
}

/**
* ------------------------------
* Article content
* ------------------------------
*/

.article-content {
@apply leading-relaxed;
}
Expand Down
7 changes: 7 additions & 0 deletions styles/custom/inputs.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
.focusable {
@apply ring-primary-500 ring-inset focus:bg-white focus:ring-1 focus:outline-none;
}

.form-input {
@apply transition text-gray-800 border border-gray-200 rounded-lg py-3 pl-4 pr-10 w-full focus:border-primary-500;
}
Loading

0 comments on commit c9b606e

Please sign in to comment.