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

Add search #16

Merged
merged 2 commits into from
Jun 19, 2021
Merged
Show file tree
Hide file tree
Changes from all 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
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