1
0
mirror of https://github.com/uetchy/namae.git synced 2025-07-01 05:50:03 +09:00
namae/src/components/Suggestion.tsx

373 lines
14 KiB
TypeScript
Raw Normal View History

2021-02-25 15:13:15 +09:00
import fetch from 'cross-fetch';
2020-08-31 08:41:53 +09:00
import { motion } from 'framer-motion';
2021-10-06 16:13:32 +09:00
import React, { useEffect, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { TiArrowSync } from 'react-icons/ti';
import { PropagateLoader } from 'react-spinners';
2021-10-06 16:13:32 +09:00
import styled from 'styled-components';
2020-03-05 22:09:12 +09:00
import {
sendAcceptSuggestionEvent,
2021-10-06 16:13:32 +09:00
sendShuffleSuggestionEvent,
2020-08-31 08:41:53 +09:00
} from '../util/analytics';
2021-10-07 12:44:01 +09:00
import { sample, sampleMany, times } from '../util/array';
2021-10-06 16:13:32 +09:00
import { mobile, slideUp } from '../util/css';
import {
capitalize,
germanify,
lower,
njoin,
sanitize,
stem,
upper,
} from '../util/text';
2019-08-03 16:44:48 +09:00
2020-08-31 08:41:53 +09:00
type Modifier = (word: string) => string;
2019-09-01 01:28:24 +09:00
const MODIFIERS: Modifier[] = [
2020-02-06 00:06:04 +09:00
(word): string => `${capitalize(germanify(word))}`,
2020-02-05 21:25:00 +09:00
(word): string => `${capitalize(word)}`,
(word): string => njoin('Air', capitalize(word), { elision: false }),
(word): string => njoin('All', capitalize(word), { elision: false }),
(word): string => njoin('Cloud', capitalize(word), { elision: false }),
(word): string => njoin('Co', lower(word), { elision: false }),
(word): string => njoin('Deep', capitalize(word), { elision: false }),
(word): string => njoin('Easy', capitalize(word), { elision: false }),
(word): string => njoin('En', lower(word), { elision: false }),
(word): string => njoin('Fast', lower(word), { elision: false }),
(word): string => njoin('Fire', lower(word), { elision: false }),
(word): string => njoin('Fusion', capitalize(word), { elision: false }),
(word): string => njoin('Git', capitalize(word), { elision: false }),
2021-10-06 16:13:32 +09:00
(word): string => njoin('Go', capitalize(word)),
(word): string => njoin('Hyper', capitalize(word)),
(word): string => njoin('In', capitalize(word), { elision: false }),
2020-02-12 12:19:12 +09:00
(word): string => njoin('Infini', lower(word)),
2021-10-06 16:13:32 +09:00
(word): string => njoin('Insta', lower(word)),
2020-02-12 12:19:12 +09:00
(word): string => njoin('i', capitalize(word)),
2021-10-06 16:13:32 +09:00
(word): string => njoin('Lead', lower(word)),
2020-02-12 12:19:12 +09:00
(word): string => njoin('Less', lower(word)),
2021-10-06 16:13:32 +09:00
(word): string => njoin('lib', lower(word)),
2020-02-12 12:19:12 +09:00
(word): string => njoin('Many', lower(word)),
2021-10-06 16:13:32 +09:00
(word): string => njoin('Max', capitalize(word)),
(word): string => njoin('Micro', lower(word)),
2020-02-12 12:19:12 +09:00
(word): string => njoin('mini', lower(word)),
(word): string => njoin('Mono', lower(word)),
(word): string => njoin('Meta', lower(word)),
2021-10-06 16:13:32 +09:00
(word): string => njoin('nano', lower(word)),
(word): string => njoin('Native', capitalize(word), { elision: false }),
2020-02-12 12:19:12 +09:00
(word): string => njoin('Next', lower(word)),
(word): string => njoin('No', upper(word), { elision: false }),
2021-10-06 16:13:32 +09:00
(word): string => njoin('Octo', lower(word)),
(word): string => njoin('Omni', capitalize(word), { elision: false }),
(word): string => njoin('One', capitalize(word), { elision: false }),
(word): string => njoin('Open', capitalize(word), { elision: false }),
(word): string => njoin('Pro', capitalize(word), { elision: false }),
2021-10-06 16:13:32 +09:00
(word): string => njoin('Quick', lower(word)),
(word): string => njoin('Semantic', capitalize(word), { elision: false }),
2021-10-06 16:13:32 +09:00
(word): string => njoin('Smart', lower(word)),
(word): string => njoin('Snap', capitalize(word), { elision: false }),
(word): string => njoin('Super', lower(word), { elision: false }),
2021-10-06 16:13:32 +09:00
(word): string => njoin('Strong', lower(word)),
2020-03-31 18:19:18 +09:00
(word): string => njoin('Ultra', lower(word)),
(word): string => njoin('Un', lower(word), { elision: false }),
2020-02-12 12:19:12 +09:00
(word): string => njoin('Uni', lower(word)),
2021-10-06 16:13:32 +09:00
(word): string => njoin('unified', lower(word)),
(word): string => njoin('Up', lower(word), { elision: false }),
(word): string => njoin('Wunder', lower(germanify(word)), { elision: false }),
(word): string => njoin('Zen', capitalize(word), { elision: false }),
(word): string => njoin('Zero', capitalize(word), { elision: false }),
(word): string => njoin(capitalize(stem(word)), 'able', { elision: false }),
(word): string => njoin(capitalize(stem(word)), 'al', { elision: false }),
(word): string => njoin(capitalize(stem(word)), 'el', { elision: false }),
(word): string => njoin(capitalize(stem(word)), 'em', { elision: false }),
(word): string => njoin(capitalize(stem(word)), 'en', { elision: false }),
(word): string => njoin(capitalize(stem(word)), 'er', { elision: false }),
(word): string => njoin(capitalize(stem(word)), 'ery', { elision: false }),
(word): string => njoin(capitalize(stem(word)), 'ia', { elision: false }),
(word): string => njoin(capitalize(stem(word)), 'ible', { elision: false }),
(word): string => njoin(capitalize(stem(word)), 'ics', { elision: false }),
(word): string => njoin(capitalize(stem(word)), 'ifier', { elision: false }),
(word): string => njoin(capitalize(stem(word)), 'ify', { elision: false }),
(word): string => njoin(capitalize(stem(word)), 'ii', { elision: false }),
(word): string => njoin(capitalize(stem(word)), 'in', { elision: false }),
(word): string => njoin(capitalize(stem(word)), 'io', { elision: false }),
(word): string => njoin(capitalize(stem(word)), 'ist', { elision: false }),
(word): string => njoin(capitalize(stem(word)), 'ity', { elision: false }),
(word): string => njoin(capitalize(stem(word)), 'ium', { elision: false }),
(word): string => njoin(capitalize(stem(word)), 'iverse', { elision: false }),
(word): string => njoin(capitalize(stem(word)), 'ory', { elision: false }),
(word): string => njoin(capitalize(stem(word)), 'um', { elision: false }),
2020-02-12 12:19:12 +09:00
(word): string => njoin(capitalize(stem(word)), 'y'),
(word): string => njoin(capitalize(word), 'AI', { elision: false }),
(word): string => njoin(capitalize(word), 'API', { elision: false }),
2020-02-12 12:19:12 +09:00
(word): string => njoin(capitalize(word), 'App'),
2021-10-06 16:13:32 +09:00
(word): string => njoin(capitalize(word), 'base'),
(word): string => njoin(capitalize(word), 'book', { elision: false }),
(word): string => njoin(capitalize(word), 'Bot', { elision: false }),
2021-10-06 16:13:32 +09:00
(word): string => njoin(capitalize(word), 'butler'),
(word): string => njoin(capitalize(word), 'byte'),
(word): string => njoin(capitalize(word), 'cast'),
(word): string => njoin(capitalize(word), 'CDN', { elision: false }),
(word): string => njoin(capitalize(word), 'CI', { elision: false }),
(word): string => njoin(capitalize(word), 'Club', { elision: false }),
(word): string => njoin(capitalize(word), 'DB', { elision: false }),
(word): string => njoin(capitalize(word), 'Express', { elision: false }),
2020-03-05 22:09:12 +09:00
(word): string => njoin(capitalize(word), 'en'),
2021-10-06 16:13:32 +09:00
(word): string => njoin(capitalize(word), 'feed'),
(word): string => njoin(capitalize(word), 'Finder', { elision: false }),
2021-10-06 16:13:32 +09:00
(word): string => njoin(capitalize(word), 'flow'),
(word): string => njoin(capitalize(word), 'form'),
2020-02-06 00:06:04 +09:00
(word): string => njoin(capitalize(word), 'ful'),
(word): string => njoin(capitalize(word), 'Go', { elision: false }),
2021-10-06 16:13:32 +09:00
(word): string => njoin(capitalize(word), 'gram'),
(word): string => njoin(capitalize(word), 'Hero', { elision: false }),
(word): string => njoin(capitalize(word), 'Hub', { elision: false }),
(word): string => njoin(capitalize(word), 'Hunt', { elision: false }),
2021-10-06 16:13:32 +09:00
(word): string => njoin(capitalize(word), '.IO', { elision: false }),
(word): string => njoin(capitalize(word), 'It', { elision: false }),
(word): string => njoin(capitalize(word), 'Kit', { elision: false }),
(word): string => njoin(capitalize(word), 'Lab', { elision: false }),
2020-02-06 00:06:04 +09:00
(word): string => njoin(capitalize(word), 'let'),
2020-02-12 12:19:12 +09:00
(word): string => njoin(capitalize(word), 'less'),
(word): string => njoin(capitalize(word), 'Link', { elision: false }),
(word): string => njoin(capitalize(word), 'list', { elision: false }),
(word): string => njoin(capitalize(word), 'list', { elision: false }),
(word): string => njoin(capitalize(word), 'lit', { elision: false }),
(word): string => njoin(capitalize(word), 'mind', { elision: false }),
(word): string => njoin(capitalize(word), 'ML', { elision: false }),
(word): string => njoin(capitalize(word), 'note', { elision: false }),
(word): string => njoin(capitalize(word), 'Notes', { elision: false }),
(word): string => njoin(capitalize(word), 'Pod', { elision: false }),
(word): string => njoin(capitalize(word), 'Pro', { elision: false }),
(word): string => njoin(capitalize(word), 'Scan', { elision: false }),
2020-02-06 00:06:04 +09:00
(word): string => njoin(capitalize(word), 'shot'),
(word): string => njoin(capitalize(word), 'space'),
(word): string => njoin(capitalize(word), 'Stack', { elision: false }),
(word): string => njoin(capitalize(word), 'Studio', { elision: false }),
(word): string => njoin(capitalize(word), 'Sensei', { elision: false }),
2020-02-06 00:06:04 +09:00
(word): string => njoin(capitalize(word), 'time'),
(word): string => njoin(capitalize(word), 'way'),
(word): string => njoin(capitalize(word), 'x', { elision: false }),
(word): string => njoin(capitalize(word), 'check', { elision: false }),
2020-02-12 12:19:12 +09:00
(word): string => njoin(capitalize(word), 'joy'),
(word): string => njoin(lower(word), 'lint', { elision: false }),
(word): string => njoin(lower(word), 'ly', { elision: false }),
2021-10-06 16:13:32 +09:00
(word): string => njoin(capitalize(word), 'mate'),
(word): string => capitalize(word) + ' by Design',
];
const FONT_FAMILIES = [
2021-10-06 16:13:32 +09:00
`'Helvetica', sans-serif`,
`'Avenir', sans-serif`,
2022-03-10 13:13:53 +09:00
`'Futura', sans-serif`,
2021-10-06 16:13:32 +09:00
`'Montserrat', sans-serif`,
2020-08-31 08:41:53 +09:00
];
2019-08-03 22:26:37 +09:00
const FONT_WEIGHTS = [300, 600, 900];
2021-10-06 16:13:32 +09:00
const NUM_SUGGESTION = 3;
2021-10-06 16:13:32 +09:00
2019-12-24 01:57:07 +09:00
function modifyWord(word: string): string {
return sample(MODIFIERS)(word);
2019-08-03 21:51:12 +09:00
}
2021-10-07 12:44:01 +09:00
async function getSynonyms(word: string): Promise<string[]> {
2019-08-03 21:51:12 +09:00
try {
const response = await fetch(
`https://translate.googleapis.com/translate_a/single?client=gtx&sl=auto&dt=ss&ie=UTF-8&oe=UTF-8&dj=1&q=${encodeURIComponent(
2020-08-20 00:57:33 +09:00
word
)}`
2020-08-31 08:41:53 +09:00
);
2019-09-01 01:28:24 +09:00
const json: {
2020-08-31 08:41:53 +09:00
synsets: Array<{ entry: Array<{ synonym: string[] }> }>;
} = await response.json();
2019-09-01 01:28:24 +09:00
const synonyms = Array.from(
new Set<string>(
2019-08-07 22:26:03 +09:00
json.synsets.reduce(
2019-09-01 01:28:24 +09:00
(sum, synset) => [...sum, ...synset.entry.map((e) => e.synonym[0])],
2020-08-20 00:57:33 +09:00
[] as string[]
)
)
2020-02-11 17:57:10 +09:00
)
.filter((word) => !/[\s-]/.exec(word))
2020-08-31 08:41:53 +09:00
.map((word) => sanitize(word));
return synonyms;
2019-08-03 21:51:12 +09:00
} catch (err) {
2020-08-31 08:41:53 +09:00
return [];
2019-08-03 21:51:12 +09:00
}
}
2019-09-01 01:28:24 +09:00
const Suggestion: React.FC<{
2020-08-31 08:41:53 +09:00
query: string;
onSubmit: (name: string) => void;
}> = ({ query, onSubmit }) => {
2020-08-31 08:41:53 +09:00
const { t } = useTranslation();
const synonymRef = useRef<string[]>([]);
const [bestWords, setBestWords] = useState<string[]>([]);
2019-08-07 22:26:03 +09:00
2019-12-24 01:57:07 +09:00
function shuffle(): void {
2021-10-07 12:44:01 +09:00
const best = sampleMany(
[...synonymRef.current.filter((s) => s.length < 8), ...times(query, 10)],
NUM_SUGGESTION
2020-08-31 08:41:53 +09:00
).map((word) => modifyWord(word));
setBestWords(best);
2019-08-07 22:26:03 +09:00
}
2019-12-24 01:57:07 +09:00
function applyQuery(name: string): void {
2020-08-31 08:41:53 +09:00
onSubmit(name);
2021-10-07 23:54:21 +09:00
sendAcceptSuggestionEvent();
2019-08-07 22:26:03 +09:00
}
2019-08-03 16:44:48 +09:00
2020-03-05 22:09:12 +09:00
function onShuffleButtonClicked() {
2020-08-31 08:41:53 +09:00
shuffle();
2021-10-07 23:54:21 +09:00
sendShuffleSuggestionEvent();
2020-03-05 22:09:12 +09:00
}
2019-08-03 21:51:12 +09:00
useEffect(() => {
2020-08-31 08:41:53 +09:00
let isEffective = true;
2019-12-24 01:57:07 +09:00
const fn = async (): Promise<void> => {
2019-08-03 21:51:12 +09:00
if (query && query.length > 0) {
2021-10-07 12:44:01 +09:00
const synonyms = await getSynonyms(query);
2020-02-05 15:59:53 +09:00
if (!isEffective) {
2020-08-31 08:41:53 +09:00
return;
2020-02-05 15:59:53 +09:00
}
2020-08-31 08:41:53 +09:00
synonymRef.current = [query, ...synonyms];
shuffle();
2019-08-03 21:51:12 +09:00
}
2020-08-31 08:41:53 +09:00
};
fn();
2020-02-05 15:59:53 +09:00
return () => {
2020-08-31 08:41:53 +09:00
isEffective = false;
};
2020-03-27 00:49:23 +09:00
// eslint-disable-next-line
2020-08-31 08:41:53 +09:00
}, [query]);
2019-08-03 16:44:48 +09:00
return (
<Container>
<Title>{t('try')}</Title>
{bestWords.length > 0 ? (
<Items>
{bestWords.map((name, i) => (
2020-03-06 00:05:54 +09:00
<Item
2021-10-06 16:13:32 +09:00
style={{
fontFamily: sample(FONT_FAMILIES),
fontWeight: sample(FONT_WEIGHTS),
2021-10-06 16:13:32 +09:00
}}
2020-03-06 00:05:54 +09:00
key={name + i}
onClick={(): void => applyQuery(name)}
2020-03-11 12:37:39 +09:00
delay={i + 1}
>
2020-03-06 00:05:54 +09:00
<span>{name}</span>
2019-08-03 21:51:12 +09:00
</Item>
))}{' '}
</Items>
) : (
<div
style={{
height: '50px',
display: 'flex',
justifyContent: 'center',
flexDirection: 'column',
}}
>
<PropagateLoader size={10} />
</div>
)}
2020-03-05 22:09:12 +09:00
<Button onClick={onShuffleButtonClicked}>
2020-02-05 20:33:24 +09:00
<TiArrowSync />
2020-02-13 15:00:41 +09:00
</Button>
2019-08-03 16:44:48 +09:00
</Container>
2020-08-31 08:41:53 +09:00
);
};
2019-08-03 16:44:48 +09:00
2020-08-31 08:41:53 +09:00
export default Suggestion;
2019-09-01 01:28:24 +09:00
2019-08-03 16:44:48 +09:00
const Container = styled.div`
2020-02-08 19:05:33 +09:00
margin-top: 20px;
2019-08-03 16:44:48 +09:00
margin-bottom: 10px;
display: flex;
2019-08-07 22:26:03 +09:00
flex-direction: column;
align-items: center;
2019-08-03 16:44:48 +09:00
flex-wrap: wrap;
justify-content: center;
2020-02-08 19:05:33 +09:00
${mobile} {
margin-top: 15px;
}
2020-08-31 08:41:53 +09:00
`;
2019-08-03 16:44:48 +09:00
const Title = styled.div`
2020-02-06 17:21:22 +09:00
padding: 0 10px;
2019-08-07 22:26:03 +09:00
color: gray;
border: 1px solid gray;
border-radius: 2em;
2020-02-06 03:39:06 +09:00
text-transform: uppercase;
font-size: 12px;
user-select: none;
2020-08-31 08:41:53 +09:00
`;
2019-08-03 16:44:48 +09:00
const Items = styled.div`
2020-02-06 17:21:22 +09:00
margin: 5px 0;
2019-08-03 16:44:48 +09:00
display: flex;
flex-direction: row;
2019-08-03 21:51:12 +09:00
flex-wrap: wrap;
justify-content: center;
2019-08-07 22:26:03 +09:00
${mobile} {
flex-direction: column;
align-items: center;
}
2020-08-31 08:41:53 +09:00
`;
2019-08-03 16:44:48 +09:00
const Item = styled.div<{ delay: number }>`
2021-10-06 16:13:32 +09:00
margin: 10px 15px 0;
2020-06-20 18:19:05 +09:00
padding-bottom: 5px;
2019-08-03 16:44:48 +09:00
cursor: pointer;
2020-06-20 18:19:05 +09:00
font-weight: 600;
2021-10-06 16:13:32 +09:00
font-size: 1.7rem;
2022-03-10 13:13:53 +09:00
/* letter-spacing: -0.5px; */
2019-08-03 22:26:37 +09:00
border-bottom: 1px dashed black;
2019-08-07 22:26:03 +09:00
color: black;
2020-03-06 00:05:54 +09:00
overflow: hidden;
span {
display: block;
2020-03-08 11:44:34 +09:00
animation: 0.6s cubic-bezier(0.19, 1, 0.22, 1)
${(props) => `${props.delay * 0.1}s`} 1 normal both running ${slideUp};
2020-03-06 00:05:54 +09:00
}
2019-08-07 22:26:03 +09:00
${mobile} {
margin: 5px 0 0;
padding-bottom: 0;
font-size: 1rem;
2019-08-07 22:26:03 +09:00
}
2020-08-31 08:41:53 +09:00
`;
2019-08-07 22:26:03 +09:00
2020-02-13 15:00:41 +09:00
const Button = styled(motion.div).attrs({
whileHover: { scale: 1.1 },
whileTap: { scale: 0.9 },
2020-02-13 15:00:41 +09:00
})`
2020-02-05 20:33:24 +09:00
margin: 15px 0 0 0;
2020-02-13 15:00:41 +09:00
padding: 8px 12px;
2019-08-07 22:26:03 +09:00
display: flex;
align-items: center;
border-bottom: none;
2020-02-05 20:33:24 +09:00
color: white;
border-radius: 4px;
font-size: 1.3rem;
user-select: none;
2020-02-05 20:47:03 +09:00
background: #5610ff;
2020-02-05 20:33:24 +09:00
transition: background 0.1s ease-out;
2020-02-13 15:00:41 +09:00
cursor: pointer;
2020-02-05 20:33:24 +09:00
&:hover {
2020-02-05 20:47:03 +09:00
background: #723df3;
2020-02-05 20:33:24 +09:00
}
&:active {
2020-02-05 20:47:03 +09:00
background: #a17ff5;
2020-02-05 20:33:24 +09:00
}
2020-08-31 08:41:53 +09:00
`;