mirror of
https://github.com/uetchy/namae.git
synced 2025-03-17 12:30:32 +09:00
feat: add suggestion
This commit is contained in:
parent
1b872aabef
commit
3a90cd74cd
@ -14,5 +14,6 @@
|
|||||||
"s3": "AWS S3",
|
"s3": "AWS S3",
|
||||||
"twitter": "Twitter",
|
"twitter": "Twitter",
|
||||||
"slack": "Slack"
|
"slack": "Slack"
|
||||||
}
|
},
|
||||||
|
"try": "suggestion"
|
||||||
}
|
}
|
||||||
|
@ -14,5 +14,6 @@
|
|||||||
"s3": "AWS S3",
|
"s3": "AWS S3",
|
||||||
"twitter": "Twitter",
|
"twitter": "Twitter",
|
||||||
"slack": "Slack"
|
"slack": "Slack"
|
||||||
}
|
},
|
||||||
|
"try": "これはどう?"
|
||||||
}
|
}
|
||||||
|
@ -3,8 +3,6 @@ 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 Welcome from './components/Welcome'
|
|
||||||
import Footer from './components/Footer'
|
|
||||||
import { Cards, CardContainer } from './components/Cards'
|
import { Cards, CardContainer } from './components/Cards'
|
||||||
import GithubCard from './components/cards/GithubCard'
|
import GithubCard from './components/cards/GithubCard'
|
||||||
import DomainCard from './components/cards/DomainCard'
|
import DomainCard from './components/cards/DomainCard'
|
||||||
@ -18,15 +16,19 @@ import S3Card from './components/cards/S3Card'
|
|||||||
import CratesioCard from './components/cards/CratesioCard'
|
import CratesioCard from './components/cards/CratesioCard'
|
||||||
import RubyGemsCard from './components/cards/RubyGemsCard'
|
import RubyGemsCard from './components/cards/RubyGemsCard'
|
||||||
import { EventReporter } from './components/Analytics'
|
import { EventReporter } from './components/Analytics'
|
||||||
|
import Welcome from './components/Welcome'
|
||||||
|
import Footer from './components/Footer'
|
||||||
|
import Suggestion from './components/Suggestion'
|
||||||
|
|
||||||
import { useDeferredState } from './hooks/state'
|
import { useDeferredState } from './hooks/state'
|
||||||
import { mobile } from './util/css'
|
import { mobile } from './util/css'
|
||||||
import { isStandalone } from './util/pwa'
|
import { isStandalone } from './util/pwa'
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
const [query, setQuery] = useDeferredState(1000)
|
const [query, setQuery] = useDeferredState('', 1000)
|
||||||
const [inputValue, setInputValue] = useState('')
|
const [inputValue, setInputValue] = useState('')
|
||||||
const inputRef = useRef()
|
const inputRef = useRef()
|
||||||
|
const [suggested, setSuggested] = useState(false)
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
|
||||||
const queryGiven = query && query.length > 0
|
const queryGiven = query && query.length > 0
|
||||||
@ -35,32 +37,53 @@ export default function App() {
|
|||||||
setQuery(inputValue)
|
setQuery(inputValue)
|
||||||
}, [inputValue, setQuery])
|
}, [inputValue, setQuery])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (query.length === 0) {
|
||||||
|
setSuggested(false)
|
||||||
|
}
|
||||||
|
}, [query])
|
||||||
|
|
||||||
|
// set input value
|
||||||
function onInputChange(e) {
|
function onInputChange(e) {
|
||||||
setInputValue(e.target.value)
|
const value = e.target.value
|
||||||
|
setInputValue(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// clear input form and focus on it
|
||||||
function onLogoClick(e) {
|
function onLogoClick(e) {
|
||||||
setInputValue('')
|
setInputValue('')
|
||||||
inputRef.current.focus()
|
inputRef.current.focus()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// invoke when user clicked one of the suggested items
|
||||||
|
function onSuggestionCompleted(name) {
|
||||||
|
setInputValue(name)
|
||||||
|
setSuggested(true)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<GlobalStyle />
|
<GlobalStyle />
|
||||||
|
|
||||||
<Helmet>
|
<Helmet>
|
||||||
<title>namaæ — {t('title')}</title>
|
<title>namaæ — {t('title')}</title>
|
||||||
</Helmet>
|
</Helmet>
|
||||||
|
|
||||||
<Header>
|
<Header>
|
||||||
<InputContainer>
|
<InputContainer>
|
||||||
<Logo onClick={onLogoClick}>namæ</Logo>
|
<Logo onClick={onLogoClick}>namæ</Logo>
|
||||||
<Input
|
<InputView
|
||||||
onChange={onInputChange}
|
onChange={onInputChange}
|
||||||
value={inputValue}
|
value={inputValue}
|
||||||
ref={inputRef}
|
ref={inputRef}
|
||||||
placeholder={t('placeholder')}
|
placeholder={t('placeholder')}
|
||||||
/>
|
/>
|
||||||
|
{queryGiven && !suggested ? (
|
||||||
|
<Suggestion onSubmit={onSuggestionCompleted} query={query} />
|
||||||
|
) : null}
|
||||||
</InputContainer>
|
</InputContainer>
|
||||||
</Header>
|
</Header>
|
||||||
|
|
||||||
<Content>
|
<Content>
|
||||||
{queryGiven ? (
|
{queryGiven ? (
|
||||||
<Cards>
|
<Cards>
|
||||||
@ -155,7 +178,7 @@ const Logo = styled.div`
|
|||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
const Input = styled.input.attrs({
|
const InputView = styled.input.attrs({
|
||||||
type: 'text',
|
type: 'text',
|
||||||
autocomplete: 'off',
|
autocomplete: 'off',
|
||||||
autocorrect: 'off',
|
autocorrect: 'off',
|
||||||
|
@ -16,11 +16,15 @@ export function Card({ title, nameList = [], alternativeList = [], children }) {
|
|||||||
<CardTitle>{title}</CardTitle>
|
<CardTitle>{title}</CardTitle>
|
||||||
<CardList>
|
<CardList>
|
||||||
{nameList.map((name) => (
|
{nameList.map((name) => (
|
||||||
<AvailabilityContainer>{children(name)}</AvailabilityContainer>
|
<AvailabilityContainer key={name}>
|
||||||
|
{children(name)}
|
||||||
|
</AvailabilityContainer>
|
||||||
))}
|
))}
|
||||||
{revealAlternatives &&
|
{revealAlternatives &&
|
||||||
alternativeList.map((name) => (
|
alternativeList.map((name) => (
|
||||||
<AvailabilityContainer>{children(name)}</AvailabilityContainer>
|
<AvailabilityContainer key={name}>
|
||||||
|
{children(name)}
|
||||||
|
</AvailabilityContainer>
|
||||||
))}
|
))}
|
||||||
{alternativeList.length > 0 && !revealAlternatives ? (
|
{alternativeList.length > 0 && !revealAlternatives ? (
|
||||||
<ShowAlternativesButton onClick={onClick}>
|
<ShowAlternativesButton onClick={onClick}>
|
||||||
|
69
web/src/components/Suggestion.js
Normal file
69
web/src/components/Suggestion.js
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import styled from 'styled-components'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { capitalize } from '../util/text'
|
||||||
|
|
||||||
|
export default function Suggestion({ query, onSubmit }) {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const capital = capitalize(query)
|
||||||
|
const lower = query.toLowerCase()
|
||||||
|
|
||||||
|
const suggestions = [
|
||||||
|
`${lower}ify`,
|
||||||
|
`insta${lower}`,
|
||||||
|
`lib${lower}`,
|
||||||
|
`omni${lower}`,
|
||||||
|
`${capital}Lab`,
|
||||||
|
`${capital}Kit`,
|
||||||
|
`Open${capital}`,
|
||||||
|
]
|
||||||
|
.sort(() => Math.random() - 0.5)
|
||||||
|
.slice(0, 3)
|
||||||
|
|
||||||
|
function applyQuery(name) {
|
||||||
|
onSubmit(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container>
|
||||||
|
<Title>{t('try')}</Title>
|
||||||
|
<Items>
|
||||||
|
{suggestions.map((name) => (
|
||||||
|
<Item key={name} onClick={() => applyQuery(name)}>
|
||||||
|
{name}
|
||||||
|
</Item>
|
||||||
|
))}
|
||||||
|
</Items>
|
||||||
|
</Container>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const Container = styled.div`
|
||||||
|
margin-bottom: 10px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: center;
|
||||||
|
line-height: 1em;
|
||||||
|
`
|
||||||
|
|
||||||
|
const Title = styled.div`
|
||||||
|
margin-top: 15px;
|
||||||
|
border: 1px solid black;
|
||||||
|
padding: 1px 6px;
|
||||||
|
border-radius: 20px;
|
||||||
|
font-size: 0.6em;
|
||||||
|
`
|
||||||
|
|
||||||
|
const Items = styled.div`
|
||||||
|
margin-top: 15px;
|
||||||
|
margin-left: 8px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
`
|
||||||
|
|
||||||
|
const Item = styled.div`
|
||||||
|
margin-right: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-weight: bold;
|
||||||
|
`
|
@ -9,7 +9,7 @@ export default function CratesioCard({ name }) {
|
|||||||
const lowerCase = name.toLowerCase()
|
const lowerCase = name.toLowerCase()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card title={t('providers.rust')} key={lowerCase} nameList={[lowerCase]}>
|
<Card title={t('providers.rust')} nameList={[lowerCase]}>
|
||||||
{(name) => (
|
{(name) => (
|
||||||
<DedicatedAvailability
|
<DedicatedAvailability
|
||||||
name={name}
|
name={name}
|
||||||
|
@ -11,7 +11,6 @@ export default function DomainCard({ name }) {
|
|||||||
return (
|
return (
|
||||||
<Card
|
<Card
|
||||||
title={t('providers.domains')}
|
title={t('providers.domains')}
|
||||||
key={lowerCase}
|
|
||||||
nameList={[`${lowerCase}.com`, `${lowerCase}app.com`, `${lowerCase}.app`]}
|
nameList={[`${lowerCase}.com`, `${lowerCase}app.com`, `${lowerCase}.app`]}
|
||||||
alternativeList={[
|
alternativeList={[
|
||||||
`${lowerCase}.dev`,
|
`${lowerCase}.dev`,
|
||||||
|
@ -12,7 +12,6 @@ export default function GithubCard({ name }) {
|
|||||||
return (
|
return (
|
||||||
<Card
|
<Card
|
||||||
title={t('providers.github')}
|
title={t('providers.github')}
|
||||||
key={name}
|
|
||||||
nameList={[name]}
|
nameList={[name]}
|
||||||
alternativeList={[
|
alternativeList={[
|
||||||
`${lowerCase}hq`,
|
`${lowerCase}hq`,
|
||||||
|
@ -9,10 +9,7 @@ export default function HomebrewCard({ name }) {
|
|||||||
const lowerCase = name.toLowerCase()
|
const lowerCase = name.toLowerCase()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card
|
<Card title={t('providers.homebrew')} nameList={[lowerCase]}>
|
||||||
title={t('providers.homebrew')}
|
|
||||||
key={lowerCase}
|
|
||||||
nameList={[lowerCase]}>
|
|
||||||
{(name) => (
|
{(name) => (
|
||||||
<>
|
<>
|
||||||
<ExistentialAvailability
|
<ExistentialAvailability
|
||||||
|
@ -9,7 +9,7 @@ export default function JsOrgCard({ name }) {
|
|||||||
const lowerCase = name.toLowerCase()
|
const lowerCase = name.toLowerCase()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card title={t('providers.jsorg')} key={lowerCase} nameList={[lowerCase]}>
|
<Card title={t('providers.jsorg')} nameList={[lowerCase]}>
|
||||||
{(name) => (
|
{(name) => (
|
||||||
<DedicatedAvailability
|
<DedicatedAvailability
|
||||||
name={`${name}.js.org`}
|
name={`${name}.js.org`}
|
||||||
|
@ -11,7 +11,6 @@ export default function NpmCard({ name }) {
|
|||||||
return (
|
return (
|
||||||
<Card
|
<Card
|
||||||
title={t('providers.npm')}
|
title={t('providers.npm')}
|
||||||
key={lowerCase}
|
|
||||||
nameList={[lowerCase]}
|
nameList={[lowerCase]}
|
||||||
alternativeList={[`${lowerCase}-js`]}>
|
alternativeList={[`${lowerCase}-js`]}>
|
||||||
{(name) => (
|
{(name) => (
|
||||||
|
@ -11,7 +11,6 @@ export default function PypiCard({ name }) {
|
|||||||
return (
|
return (
|
||||||
<Card
|
<Card
|
||||||
title={t('providers.pypi')}
|
title={t('providers.pypi')}
|
||||||
key={name}
|
|
||||||
nameList={[name]}
|
nameList={[name]}
|
||||||
alternativeList={[`Py${capitalize(name)}`]}>
|
alternativeList={[`Py${capitalize(name)}`]}>
|
||||||
{(name) => (
|
{(name) => (
|
||||||
|
@ -10,7 +10,6 @@ export default function RubyGemsCard({ name }) {
|
|||||||
return (
|
return (
|
||||||
<Card
|
<Card
|
||||||
title={t('providers.rubygems')}
|
title={t('providers.rubygems')}
|
||||||
key={name}
|
|
||||||
nameList={[name]}
|
nameList={[name]}
|
||||||
alternativeList={[`${name.toLowerCase()}-rb`]}>
|
alternativeList={[`${name.toLowerCase()}-rb`]}>
|
||||||
{(name) => (
|
{(name) => (
|
||||||
|
@ -9,7 +9,7 @@ export default function S3Card({ name }) {
|
|||||||
const lowerCase = name.toLowerCase()
|
const lowerCase = name.toLowerCase()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card title={t('providers.s3')} key={lowerCase} nameList={[lowerCase]}>
|
<Card title={t('providers.s3')} nameList={[lowerCase]}>
|
||||||
{(name) => (
|
{(name) => (
|
||||||
<DedicatedAvailability
|
<DedicatedAvailability
|
||||||
name={name}
|
name={name}
|
||||||
|
@ -9,7 +9,7 @@ export default function SlackCard({ name }) {
|
|||||||
const lowerCase = name.toLowerCase()
|
const lowerCase = name.toLowerCase()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card title={t('providers.slack')} key={lowerCase} nameList={[lowerCase]}>
|
<Card title={t('providers.slack')} nameList={[lowerCase]}>
|
||||||
{(name) => (
|
{(name) => (
|
||||||
<DedicatedAvailability
|
<DedicatedAvailability
|
||||||
name={name}
|
name={name}
|
||||||
|
@ -11,7 +11,6 @@ export default function TwitterCard({ name }) {
|
|||||||
return (
|
return (
|
||||||
<Card
|
<Card
|
||||||
title={t('providers.twitter')}
|
title={t('providers.twitter')}
|
||||||
key={name}
|
|
||||||
nameList={[name]}
|
nameList={[name]}
|
||||||
alternativeList={[
|
alternativeList={[
|
||||||
`${capitalize(name)}HQ`,
|
`${capitalize(name)}HQ`,
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import { useState, useEffect } from 'react'
|
import { useState, useEffect } from 'react'
|
||||||
|
|
||||||
export function useDeferredState(duration) {
|
export function useDeferredState(initialValue = undefined, duration = 1000) {
|
||||||
const [response, setResponse] = useState()
|
const [response, setResponse] = useState(initialValue)
|
||||||
const [innerValue, setInnerValue] = useState()
|
const [innerValue, setInnerValue] = useState(initialValue)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fn = setTimeout(() => {
|
const fn = setTimeout(() => {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user