1
0
mirror of https://github.com/uetchy/namae.git synced 2025-03-17 12:30:32 +09:00

feat: persistent search result

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

View File

@ -8,20 +8,17 @@
"test": "jest --coverage" "test": "jest --coverage"
}, },
"dependencies": { "dependencies": {
"google-it": "^1.2.1",
"isomorphic-unfetch": "^3.0.0", "isomorphic-unfetch": "^3.0.0",
"npm-name": "^5.5.0", "npm-name": "^5.5.0",
"typescript": "^3.6.2",
"whois-json": "^2.0.4" "whois-json": "^2.0.4"
}, },
"devDependencies": { "devDependencies": {
"@types/jest": "^24.0.18", "@types/jest": "^25.1.1",
"@types/nock": "^10.0.3", "@types/node": "^13.7.0",
"@types/node": "^12.7.3",
"@types/node-fetch": "^2.5.0", "@types/node-fetch": "^2.5.0",
"jest": "^24.9.0", "jest": "^24.9.0",
"nock": "^10.0.6", "nock": "^11.7.2",
"ts-jest": "^24.0.2" "ts-jest": "^25.2.0"
}, },
"engines": { "engines": {
"node": ">=10.0.0" "node": ">=10.0.0"

View File

@ -9,7 +9,7 @@
"test": "CI=true yarn workspaces run test" "test": "CI=true yarn workspaces run test"
}, },
"dependencies": { "dependencies": {
"now": "^16.7.0" "typescript": "^3.7.5"
}, },
"devDependencies": { "devDependencies": {
"@sentry/cli": "^1.49.0", "@sentry/cli": "^1.49.0",

View File

@ -11,11 +11,11 @@
"test": "react-scripts test --coverage" "test": "react-scripts test --coverage"
}, },
"dependencies": { "dependencies": {
"@sentry/browser": "^5.6.3", "@sentry/browser": "^5.12.1",
"fetch-suspense": "^1.2.0", "fetch-suspense": "^1.2.0",
"i18next": ">=17.0.12", "i18next": ">=19.1.0",
"i18next-browser-languagedetector": "^3.0.3", "i18next-browser-languagedetector": "^4.0.1",
"i18next-chained-backend": "^2.0.0", "i18next-chained-backend": "^2.0.1",
"i18next-localstorage-backend": "^3.0.0", "i18next-localstorage-backend": "^3.0.0",
"i18next-xhr-backend": "^3.1.2", "i18next-xhr-backend": "^3.1.2",
"isomorphic-unfetch": "^3.0.0", "isomorphic-unfetch": "^3.0.0",
@ -24,23 +24,29 @@
"react-dom": "^16.9.0", "react-dom": "^16.9.0",
"react-ga": "^2.6.0", "react-ga": "^2.6.0",
"react-helmet": "^5.2.1", "react-helmet": "^5.2.1",
"react-i18next": "10.12.2", "react-i18next": "11.3.1",
"react-icons": "^3.7.0", "react-icons": "^3.9.0",
"react-router-dom": "^5.0.1", "react-router": "^5.1.2",
"react-scripts": "3.3.0", "react-router-dom": "^5.1.2",
"react-spinners": "^0.6.1", "react-scripts": "3.3.1",
"react-tippy": "^1.2.3", "react-spinners": "^0.8.0",
"styled-components": "^4.3.2", "react-tippy": "^1.3.4",
"typescript": "3.7.4" "styled-components": "^5.0.1"
}, },
"devDependencies": { "devDependencies": {
"@testing-library/jest-dom": "^4.1.0", "@testing-library/jest-dom": "^5.1.1",
"@testing-library/react": "^9.1.4", "@testing-library/react": "^9.1.4",
"@types/i18next-node-fs-backend": "^2.1.0", "@types/i18next-node-fs-backend": "^2.1.0",
"@types/jest": "^24.0.18", "@types/jest": "^25.1.1",
"@types/node": "^12.7.3", "@types/node": "^13.7.0",
"@types/react-helmet": "^5.0.9", "@types/react-helmet": "^5.0.15",
"@types/styled-components": "^4.1.18", "@types/react-router-dom": "^5.1.3",
"@types/styled-components": "^4.4.2",
"@typescript-eslint/eslint-plugin": "^2.19.0",
"@typescript-eslint/parser": "^2.19.0",
"eslint-config-prettier": "^6.10.0",
"eslint-plugin-react": "^7.18.3",
"husky": "^4.2.1",
"i18next-node-fs-backend": "^2.1.3" "i18next-node-fs-backend": "^2.1.3"
}, },
"browserslist": { "browserslist": {

View File

@ -1,12 +1,14 @@
import React, {Suspense} from 'react'; import React, {Suspense} from 'react';
import {render, waitForElement} from '@testing-library/react'; import {render, waitForElement} from '@testing-library/react';
import {BrowserRouter as Router} from 'react-router-dom';
import App from './App'; import App from './App';
it('renders welcome message', async () => { it('renders welcome message', async () => {
const {getByText} = render( const {getByText} = render(
<Suspense fallback={<div>loading</div>}> <Suspense fallback={<div>loading</div>}>
<Router>
<App /> <App />
</Router>
</Suspense>, </Suspense>,
); );
const text = await waitForElement(() => getByText('name new project')); 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 styled, {createGlobalStyle} from 'styled-components';
import {Helmet} from 'react-helmet'; import {Helmet} from 'react-helmet';
import {useTranslation} from 'react-i18next'; import {useTranslation} from 'react-i18next';
import {sendQueryStatistics} from './util/analytics';
import Welcome from './components/Welcome'; import Welcome from './components/Welcome';
import Form from './components/Form'; import Form from './components/Form';
@ -11,33 +10,55 @@ import Footer from './components/Footer';
import {mobile} from './util/css'; import {mobile} from './util/css';
import {isStandalone} from './util/pwa'; import {isStandalone} from './util/pwa';
import {Switch, Route, useParams} from 'react-router-dom';
export default function App() { export default function App() {
const [query, setQuery] = useState('');
const {t} = useTranslation();
function onQuery(query: string) {
setQuery(query);
sendQueryStatistics(query.length);
}
return ( return (
<> <>
<GlobalStyle /> <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> <Helmet>
<title>namae {t('title')}</title> <title>namae {t('title')}</title>
</Helmet> </Helmet>
<Header> <Header>
<Form onQuery={onQuery} /> <Form />
</Header> </Header>
<Content> <Content>{!isStandalone() && <Welcome />}</Content>
{query !== '' ? (
<Cards query={query} />
) : (
!isStandalone() && <Welcome />
)}
</Content>
<Footer />
</> </>
); );
} }

View File

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

View File

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

View File

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

3419
yarn.lock

File diff suppressed because it is too large Load Diff