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:
parent
9187b817af
commit
6f5099d00c
@ -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"
|
||||
|
@ -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",
|
||||
|
@ -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": {
|
||||
|
@ -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'),
|
||||
);
|
||||
|
Loading…
x
Reference in New Issue
Block a user