1
0
mirror of https://github.com/uetchy/namae.git synced 2025-08-20 09:58:13 +09:00

feat: persistent search result

This commit is contained in:
2020-02-05 15:59:53 +09:00
parent 9187b817af
commit 6f5099d00c
9 changed files with 1775 additions and 1823 deletions

View File

@@ -1,12 +1,14 @@
import React, {Suspense} from 'react';
import {render, waitForElement} from '@testing-library/react';
import {BrowserRouter as Router} from 'react-router-dom';
import App from './App';
it('renders welcome message', async () => {
const {getByText} = render(
<Suspense fallback={<div>loading</div>}>
<App />
<Router>
<App />
</Router>
</Suspense>,
);
const text = await waitForElement(() => getByText('name new project'));

View File

@@ -1,8 +1,7 @@
import React, {useState} from 'react';
import React from 'react';
import styled, {createGlobalStyle} from 'styled-components';
import {Helmet} from 'react-helmet';
import {useTranslation} from 'react-i18next';
import {sendQueryStatistics} from './util/analytics';
import Welcome from './components/Welcome';
import Form from './components/Form';
@@ -11,33 +10,55 @@ import Footer from './components/Footer';
import {mobile} from './util/css';
import {isStandalone} from './util/pwa';
import {Switch, Route, useParams} from 'react-router-dom';
export default function App() {
const [query, setQuery] = useState('');
const {t} = useTranslation();
function onQuery(query: string) {
setQuery(query);
sendQueryStatistics(query.length);
}
return (
<>
<GlobalStyle />
<Switch>
<Route path="/s/:query">
<Search />
</Route>
<Route path="/">
<Home />
</Route>
</Switch>
<Footer />
</>
);
}
function Search() {
const {query: currentQuery} = useParams<{query: string}>();
return (
<>
<Helmet>
<title>Search for &quot;{currentQuery}&quot; namae</title>
</Helmet>
<Header>
<Form initialValue={currentQuery} />
</Header>
<Content>
<Cards query={currentQuery} />
</Content>
</>
);
}
function Home() {
const {t} = useTranslation();
return (
<>
<Helmet>
<title>namae {t('title')}</title>
</Helmet>
<Header>
<Form onQuery={onQuery} />
<Form />
</Header>
<Content>
{query !== '' ? (
<Cards query={query} />
) : (
!isStandalone() && <Welcome />
)}
</Content>
<Footer />
<Content>{!isStandalone() && <Welcome />}</Content>
</>
);
}

View File

@@ -1,15 +1,19 @@
import React, {useState, useRef, useEffect} from 'react';
import styled from 'styled-components';
import {useTranslation} from 'react-i18next';
import {Link, useHistory} from 'react-router-dom';
import {sendQueryStatistics} from '../util/analytics';
import {useDeferredState} from '../util/hooks';
import {mobile} from '../util/css';
import Suggestion from './Suggestion';
const Form: React.FC<{onQuery: (query: string) => void}> = ({onQuery}) => {
const Form: React.FC<{
initialValue?: string;
}> = ({initialValue = ''}) => {
const history = useHistory();
const [query, setQuery] = useDeferredState(800, '');
const [inputValue, setInputValue] = useState('');
const [inputValue, setInputValue] = useState(initialValue);
const [suggested, setSuggested] = useState(false);
const inputRef = useRef<HTMLInputElement>(null);
const {t} = useTranslation();
@@ -23,7 +27,8 @@ const Form: React.FC<{onQuery: (query: string) => void}> = ({onQuery}) => {
// clear input form and focus on it
function onLogoClick(): void {
setInputValue('');
inputRef.current?.focus();
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
inputRef.current!.focus();
}
// invoke when user clicked one of the suggested items
@@ -35,11 +40,20 @@ const Form: React.FC<{onQuery: (query: string) => void}> = ({onQuery}) => {
const queryGiven = query && query.length > 0;
useEffect(() => {
function onQuery(query: string) {
if (!query || query === '') {
return;
}
sendQueryStatistics(query.length);
history.push(`/s/${query}`);
}
if (query.length === 0) {
setSuggested(false);
} else {
onQuery(query);
}
onQuery(query);
}, [query, onQuery]);
}, [query, history]);
useEffect(() => {
const modifiedValue = inputValue.replace(/[\s@+!#$%^&*()[\]]/g, '');
@@ -48,7 +62,9 @@ const Form: React.FC<{onQuery: (query: string) => void}> = ({onQuery}) => {
return (
<InputContainer>
<Logo onClick={onLogoClick}>namæ</Logo>
<Logo onClick={onLogoClick}>
<Link to="/">namæ</Link>
</Logo>
<InputView
onChange={onInputChange}
value={inputValue}
@@ -89,6 +105,14 @@ const Logo = styled.div`
${mobile} {
font-size: 15px;
}
a:link,
a:hover,
a:active,
a:visited {
text-decoration: none;
color: #4a90e2;
}
`;
const InputView = styled.input.attrs({

View File

@@ -121,9 +121,13 @@ const Suggestion: React.FC<{
}
useEffect(() => {
let isEffective = true;
const fn = async (): Promise<void> => {
if (query && query.length > 0) {
const synonyms = await findSynonyms(query);
if (!isEffective) {
return;
}
synonymRef.current = synonyms;
const best = fillArray(
sampleFromArray(synonyms, maximumCount),
@@ -134,6 +138,9 @@ const Suggestion: React.FC<{
}
};
fn();
return () => {
isEffective = false;
};
}, [query]);
return (
@@ -141,15 +148,15 @@ const Suggestion: React.FC<{
<Title>{t('try')}</Title>
<Items>
{bestWords &&
bestWords.map((name) => (
<Item key={name} onClick={(): void => applyQuery(name)}>
bestWords.map((name, i) => (
<Item key={name + i} onClick={(): void => applyQuery(name)}>
{name}
</Item>
))}
<Icon>
<TiArrowSync onClick={shuffle} />
</Icon>
</Items>
<Icon>
<TiArrowSync onClick={shuffle} />
</Icon>
</Container>
);
};

View File

@@ -8,13 +8,17 @@ import {initGA, initSentry} from './util/analytics';
import {initCrisp} from './util/crip';
import './util/i18n';
import {BrowserRouter as Router} from 'react-router-dom';
initGA();
initSentry();
initCrisp();
ReactDOM.render(
<FullScreenSuspense>
<App />
<Router>
<App />
</Router>
</FullScreenSuspense>,
document.getElementById('root'),
);