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:
@@ -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'));
|
||||
|
@@ -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 "{currentQuery}" — 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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@@ -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({
|
||||
|
@@ -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>
|
||||
);
|
||||
};
|
||||
|
@@ -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'),
|
||||
);
|
||||
|
Reference in New Issue
Block a user