1
0
mirror of https://github.com/uetchy/namae.git synced 2025-03-17 04:30:31 +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"
},
"dependencies": {
"google-it": "^1.2.1",
"isomorphic-unfetch": "^3.0.0",
"npm-name": "^5.5.0",
"typescript": "^3.6.2",
"whois-json": "^2.0.4"
},
"devDependencies": {
"@types/jest": "^24.0.18",
"@types/nock": "^10.0.3",
"@types/node": "^12.7.3",
"@types/jest": "^25.1.1",
"@types/node": "^13.7.0",
"@types/node-fetch": "^2.5.0",
"jest": "^24.9.0",
"nock": "^10.0.6",
"ts-jest": "^24.0.2"
"nock": "^11.7.2",
"ts-jest": "^25.2.0"
},
"engines": {
"node": ">=10.0.0"

View File

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

View File

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

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'),
);

3419
yarn.lock

File diff suppressed because it is too large Load Diff