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

style: prettier

This commit is contained in:
uetchy 2020-08-20 00:57:33 +09:00
parent a31c13e725
commit c7bbe707a9
71 changed files with 1074 additions and 1089 deletions

View File

@ -1,5 +1,4 @@
{ {
"singleQuote": true, "semi": false,
"trailingComma": "all", "singleQuote": true
"arrowParens": "always"
} }

View File

@ -1,39 +1,39 @@
import {send, sendError, fetch} from '../../../util/http'; import { send, sendError, fetch } from '../../../util/http'
import {NowRequest, NowResponse} from '@vercel/node'; import { NowRequest, NowResponse } from '@vercel/node'
interface App { interface App {
trackId: string; trackId: string
trackName: string; trackName: string
kind: string; kind: string
version: string; version: string
price: string; price: string
trackViewUrl: string; trackViewUrl: string
} }
interface AppStoreResponse { interface AppStoreResponse {
results: App[]; results: App[]
} }
export default async function handler( export default async function handler(
req: NowRequest, req: NowRequest,
res: NowResponse, res: NowResponse
): Promise<void> { ): Promise<void> {
const {query, country} = req.query; const { query, country } = req.query
if (!query || typeof query !== 'string') { if (!query || typeof query !== 'string') {
return sendError(res, new Error('No query given')); return sendError(res, new Error('No query given'))
} }
const term = encodeURIComponent(query); const term = encodeURIComponent(query)
const countryCode = country || 'us'; const countryCode = country || 'us'
const limit = 10; const limit = 10
try { try {
const response = await fetch( const response = await fetch(
`https://itunes.apple.com/search?media=software&entity=software,iPadSoftware,macSoftware&country=${countryCode}&limit=${limit}&term=${term}`, `https://itunes.apple.com/search?media=software&entity=software,iPadSoftware,macSoftware&country=${countryCode}&limit=${limit}&term=${term}`,
'GET', 'GET'
); )
const body: AppStoreResponse = await response.json(); const body: AppStoreResponse = await response.json()
const apps = body.results.map((app) => ({ const apps = body.results.map((app) => ({
id: app.trackId, id: app.trackId,
name: app.trackName, name: app.trackName,
@ -41,9 +41,9 @@ export default async function handler(
version: app.version, version: app.version,
price: app.price, price: app.price,
viewURL: app.trackViewUrl, viewURL: app.trackViewUrl,
})); }))
send(res, {result: apps || []}); send(res, { result: apps || [] })
} catch (err) { } catch (err) {
sendError(res, err); sendError(res, err)
} }
} }

View File

@ -1,29 +1,29 @@
import {send, sendError, fetch} from '../../../util/http'; import { send, sendError, fetch } from '../../../util/http'
import {NowRequest, NowResponse} from '@vercel/node'; import { NowRequest, NowResponse } from '@vercel/node'
export default async function handler( export default async function handler(
req: NowRequest, req: NowRequest,
res: NowResponse, res: NowResponse
): Promise<void> { ): Promise<void> {
const {query} = req.query; const { query } = req.query
if (!query || typeof query !== 'string') { if (!query || typeof query !== 'string') {
return sendError(res, new Error('No query given')); return sendError(res, new Error('No query given'))
} }
if (/[^a-zA-Z0-9_-]/.test(query)) { if (/[^a-zA-Z0-9_-]/.test(query)) {
return sendError(res, new Error('Invalid characters')); return sendError(res, new Error('Invalid characters'))
} }
try { try {
const response = await fetch( const response = await fetch(
`https://packages.debian.org/buster/${encodeURIComponent(query)}`, `https://packages.debian.org/buster/${encodeURIComponent(query)}`,
'GET', 'GET'
); )
const body = await response.text(); const body = await response.text()
const availability = body.includes('No such package'); const availability = body.includes('No such package')
send(res, {availability}); send(res, { availability })
} catch (err) { } catch (err) {
sendError(res, err); sendError(res, err)
} }
} }

View File

@ -1,34 +1,34 @@
import dns from 'dns'; import dns from 'dns'
import {send, sendError} from '../../../util/http'; import { send, sendError } from '../../../util/http'
import {NowRequest, NowResponse} from '@vercel/node'; import { NowRequest, NowResponse } from '@vercel/node'
function resolvePromise(hostname: string): Promise<string[]> { function resolvePromise(hostname: string): Promise<string[]> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
dns.resolve4(hostname, function (err, addresses) { dns.resolve4(hostname, function (err, addresses) {
if (err) return reject(err); if (err) return reject(err)
resolve(addresses); resolve(addresses)
}); })
}); })
} }
export default async function handler( export default async function handler(
req: NowRequest, req: NowRequest,
res: NowResponse, res: NowResponse
): Promise<void> { ): Promise<void> {
const {query} = req.query; const { query } = req.query
if (!query || typeof query !== 'string') { if (!query || typeof query !== 'string') {
return sendError(res, new Error('No query given')); return sendError(res, new Error('No query given'))
} }
try { try {
const response = await resolvePromise(query); const response = await resolvePromise(query)
const availability = response && response.length > 0 ? false : true; const availability = response && response.length > 0 ? false : true
send(res, {availability}); send(res, { availability })
} catch (err) { } catch (err) {
if (err.code === 'ENODATA' || err.code === 'ENOTFOUND') { if (err.code === 'ENODATA' || err.code === 'ENOTFOUND') {
return send(res, {availability: true}); return send(res, { availability: true })
} }
sendError(res, err); sendError(res, err)
} }
} }

View File

@ -1,22 +1,22 @@
import whois from 'whois-json'; import whois from 'whois-json'
import {send, sendError} from '../../../util/http'; import { send, sendError } from '../../../util/http'
import {NowRequest, NowResponse} from '@vercel/node'; import { NowRequest, NowResponse } from '@vercel/node'
export default async function handler( export default async function handler(
req: NowRequest, req: NowRequest,
res: NowResponse, res: NowResponse
): Promise<void> { ): Promise<void> {
const {query} = req.query; const { query } = req.query
if (!query || typeof query !== 'string') { if (!query || typeof query !== 'string') {
return sendError(res, new Error('No query given')); return sendError(res, new Error('No query given'))
} }
try { try {
const response = await whois(query, {follow: 3, verbose: true}); const response = await whois(query, { follow: 3, verbose: true })
const availability = response[0].data.domainName ? false : true; const availability = response[0].data.domainName ? false : true
send(res, {availability}); send(res, { availability })
} catch (err) { } catch (err) {
sendError(res, err); sendError(res, err)
} }
} }

View File

@ -1,30 +1,30 @@
import isURL from 'validator/lib/isURL'; import isURL from 'validator/lib/isURL'
import {send, sendError, fetch} from '../../../util/http'; import { send, sendError, fetch } from '../../../util/http'
import {NowRequest, NowResponse} from '@vercel/node'; import { NowRequest, NowResponse } from '@vercel/node'
export default async function handler( export default async function handler(
req: NowRequest, req: NowRequest,
res: NowResponse, res: NowResponse
): Promise<void> { ): Promise<void> {
const {query} = req.query; const { query } = req.query
if (!query || typeof query !== 'string') { if (!query || typeof query !== 'string') {
return sendError(res, new Error('no query given')); return sendError(res, new Error('no query given'))
} }
if (!isURL(query)) { if (!isURL(query)) {
return sendError(res, new Error('Invalid URL: ' + query)); return sendError(res, new Error('Invalid URL: ' + query))
} }
try { try {
const response = await fetch(`https://${query}`); const response = await fetch(`https://${query}`)
const availability = response.status === 404; const availability = response.status === 404
send(res, {availability}); send(res, { availability })
} catch (err) { } catch (err) {
console.log(err.code); console.log(err.code)
if (err.code === 'ENOTFOUND') { if (err.code === 'ENOTFOUND') {
return send(res, {availability: true}); return send(res, { availability: true })
} }
sendError(res, err); sendError(res, err)
} }
} }

View File

@ -1,28 +1,28 @@
import {send, sendError} from '../../../util/http'; import { send, sendError } from '../../../util/http'
import {NowRequest, NowResponse} from '@vercel/node'; import { NowRequest, NowResponse } from '@vercel/node'
import nodeFetch from 'node-fetch'; import nodeFetch from 'node-fetch'
export default async function handler( export default async function handler(
req: NowRequest, req: NowRequest,
res: NowResponse, res: NowResponse
): Promise<void> { ): Promise<void> {
const {query} = req.query; const { query } = req.query
if (!query || typeof query !== 'string') { if (!query || typeof query !== 'string') {
return sendError(res, new Error('No query given')); return sendError(res, new Error('No query given'))
} }
if (/[^a-zA-Z0-9_-]/.test(query)) { if (/[^a-zA-Z0-9_-]/.test(query)) {
return sendError(res, new Error('Invalid characters')); return sendError(res, new Error('Invalid characters'))
} }
try { try {
const response = await nodeFetch(`https://gitlab.com/${query}`, { const response = await nodeFetch(`https://gitlab.com/${query}`, {
redirect: 'manual', redirect: 'manual',
}); })
const availability = response.status === 302; const availability = response.status === 302
send(res, {availability}); send(res, { availability })
} catch (err) { } catch (err) {
sendError(res, err); sendError(res, err)
} }
} }

View File

@ -1,30 +1,30 @@
import {send, sendError, fetch} from '../../../util/http'; import { send, sendError, fetch } from '../../../util/http'
import {NowRequest, NowResponse} from '@vercel/node'; import { NowRequest, NowResponse } from '@vercel/node'
export default async function handler( export default async function handler(
req: NowRequest, req: NowRequest,
res: NowResponse, res: NowResponse
): Promise<void> { ): Promise<void> {
const {query} = req.query; const { query } = req.query
if (!query || typeof query !== 'string') { if (!query || typeof query !== 'string') {
return sendError(res, new Error('No query given')); return sendError(res, new Error('No query given'))
} }
if (/[^a-zA-Z0-9_-]/.test(query)) { if (/[^a-zA-Z0-9_-]/.test(query)) {
return sendError(res, new Error('Invalid characters')); return sendError(res, new Error('Invalid characters'))
} }
try { try {
const response = await fetch( const response = await fetch(
`https://api.launchpad.net/devel/ubuntu/+source/${encodeURIComponent( `https://api.launchpad.net/devel/ubuntu/+source/${encodeURIComponent(
query, query
)}`, )}`,
'GET', 'GET'
); )
const availability = response.status !== 200; const availability = response.status !== 200
send(res, {availability}); send(res, { availability })
} catch (err) { } catch (err) {
sendError(res, err); sendError(res, err)
} }
} }

View File

@ -1,24 +1,24 @@
import npmName from 'npm-name'; import npmName from 'npm-name'
import {send, sendError} from '../../../util/http'; import { send, sendError } from '../../../util/http'
import {NowRequest, NowResponse} from '@vercel/node'; import { NowRequest, NowResponse } from '@vercel/node'
export default async function handler( export default async function handler(
req: NowRequest, req: NowRequest,
res: NowResponse, res: NowResponse
): Promise<void> { ): Promise<void> {
const {query} = req.query; const { query } = req.query
if (!query || typeof query !== 'string') { if (!query || typeof query !== 'string') {
return sendError(res, new Error('No query given')); return sendError(res, new Error('No query given'))
} }
try { try {
const availability = await npmName(`@${query}`); const availability = await npmName(`@${query}`)
send(res, {availability}); send(res, { availability })
} catch (err) { } catch (err) {
if (err.code === 'ENOTFOUND') { if (err.code === 'ENOTFOUND') {
return send(res, {availability: true}); return send(res, { availability: true })
} }
sendError(res, err); sendError(res, err)
} }
} }

View File

@ -1,21 +1,21 @@
import npmName from 'npm-name'; import npmName from 'npm-name'
import {send, sendError} from '../../../util/http'; import { send, sendError } from '../../../util/http'
import {NowRequest, NowResponse} from '@vercel/node'; import { NowRequest, NowResponse } from '@vercel/node'
export default async function handler( export default async function handler(
req: NowRequest, req: NowRequest,
res: NowResponse, res: NowResponse
): Promise<void> { ): Promise<void> {
const {query} = req.query; const { query } = req.query
if (!query || typeof query !== 'string') { if (!query || typeof query !== 'string') {
return sendError(res, new Error('No query given')); return sendError(res, new Error('No query given'))
} }
try { try {
const availability = await npmName(query); const availability = await npmName(query)
send(res, {availability}); send(res, { availability })
} catch (err) { } catch (err) {
sendError(res, err); sendError(res, err)
} }
} }

View File

@ -1,42 +1,42 @@
import {send, sendError, fetch} from '../../../util/http'; import { send, sendError, fetch } from '../../../util/http'
import {NowRequest, NowResponse} from '@vercel/node'; import { NowRequest, NowResponse } from '@vercel/node'
const APPLICATION_ID = process.env.NTA_APPLICATION_ID; const APPLICATION_ID = process.env.NTA_APPLICATION_ID
export default async function handler( export default async function handler(
req: NowRequest, req: NowRequest,
res: NowResponse, res: NowResponse
): Promise<void> { ): Promise<void> {
const {query} = req.query; const { query } = req.query
if (!query || typeof query !== 'string') { if (!query || typeof query !== 'string') {
return sendError(res, new Error('No query given')); return sendError(res, new Error('No query given'))
} }
const encodedQuery = encodeURIComponent( const encodedQuery = encodeURIComponent(
query.replace(/[A-Za-z0-9]/g, (str) => query.replace(/[A-Za-z0-9]/g, (str) =>
String.fromCharCode(str.charCodeAt(0) + 0xfee0), String.fromCharCode(str.charCodeAt(0) + 0xfee0)
), )
); )
try { try {
const response = await fetch( const response = await fetch(
`https://api.houjin-bangou.nta.go.jp/4/name?id=${APPLICATION_ID}&name=${encodedQuery}&mode=1&target=1&type=02`, `https://api.houjin-bangou.nta.go.jp/4/name?id=${APPLICATION_ID}&name=${encodedQuery}&mode=1&target=1&type=02`,
'GET', 'GET'
); )
const body: string[] = (await response.text()).split('\n').slice(0, -1); const body: string[] = (await response.text()).split('\n').slice(0, -1)
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const header = body.shift()!.split(','); const header = body.shift()!.split(',')
const result = body.map((csv) => { const result = body.map((csv) => {
const entry = csv.split(',').map((item) => const entry = csv.split(',').map((item) =>
item item
.replace(/(^"|"$)/g, '') .replace(/(^"|"$)/g, '')
.replace(/[A-Za-z0-9]/g, (str) => .replace(/[A-Za-z0-9]/g, (str) =>
String.fromCharCode(str.charCodeAt(0) - 0xfee0), String.fromCharCode(str.charCodeAt(0) - 0xfee0)
) )
// eslint-disable-next-line no-irregular-whitespace // eslint-disable-next-line no-irregular-whitespace
.replace(/ /g, ' '), .replace(/ /g, ' ')
); )
return { return {
index: entry[0], index: entry[0],
@ -73,8 +73,8 @@ export default async function handler(
excluded: entry[29], excluded: entry[29],
processSection: entry[2], processSection: entry[2],
modifiedSection: entry[3], modifiedSection: entry[3],
}; }
}); })
send(res, { send(res, {
meta: { meta: {
@ -91,8 +91,8 @@ export default async function handler(
englishName: entry.englishName, englishName: entry.englishName,
})) }))
.slice(10) || [], .slice(10) || [],
}); })
} catch (err) { } catch (err) {
sendError(res, err); sendError(res, err)
} }
} }

View File

@ -1,27 +1,27 @@
import {send, sendError, fetch} from '../../../util/http'; import { send, sendError, fetch } from '../../../util/http'
import {NowRequest, NowResponse} from '@vercel/node'; import { NowRequest, NowResponse } from '@vercel/node'
export default async function handler( export default async function handler(
req: NowRequest, req: NowRequest,
res: NowResponse, res: NowResponse
): Promise<void> { ): Promise<void> {
const {query} = req.query; const { query } = req.query
if (!query || typeof query !== 'string') { if (!query || typeof query !== 'string') {
return sendError(res, new Error('No query given')); return sendError(res, new Error('No query given'))
} }
if (/[^a-zA-Z0-9_-]/.test(query)) { if (/[^a-zA-Z0-9_-]/.test(query)) {
return sendError(res, new Error('Invalid characters')); return sendError(res, new Error('Invalid characters'))
} }
try { try {
const response = await fetch( const response = await fetch(
`https://${encodeURIComponent(query)}.slack.com`, `https://${encodeURIComponent(query)}.slack.com`
); )
const availability = response.status !== 200; const availability = response.status !== 200
send(res, {availability}); send(res, { availability })
} catch (err) { } catch (err) {
sendError(res, err); sendError(res, err)
} }
} }

View File

@ -1,31 +1,31 @@
import {send, sendError, fetch} from '../../../util/http'; import { send, sendError, fetch } from '../../../util/http'
import {NowRequest, NowResponse} from '@vercel/node'; import { NowRequest, NowResponse } from '@vercel/node'
export default async function handler( export default async function handler(
req: NowRequest, req: NowRequest,
res: NowResponse, res: NowResponse
): Promise<void> { ): Promise<void> {
const {query} = req.query; const { query } = req.query
if (!query || typeof query !== 'string') { if (!query || typeof query !== 'string') {
return sendError(res, new Error('No query given')); return sendError(res, new Error('No query given'))
} }
if (/[^a-zA-Z0-9_-]/.test(query)) { if (/[^a-zA-Z0-9_-]/.test(query)) {
return sendError(res, new Error('Invalid characters')); return sendError(res, new Error('Invalid characters'))
} }
try { try {
const response = await fetch( const response = await fetch(
`https://spectrum.chat/${encodeURIComponent(query)}`, `https://spectrum.chat/${encodeURIComponent(query)}`,
'GET', 'GET'
); )
const body = await response.text(); const body = await response.text()
const availability = body.includes( const availability = body.includes(
'You may be trying to view something that is deleted', 'You may be trying to view something that is deleted'
); )
send(res, {availability}); send(res, { availability })
} catch (err) { } catch (err) {
sendError(res, err); sendError(res, err)
} }
} }

View File

@ -1,28 +1,28 @@
import { NowRequest, NowResponse } from '@vercel/node'; import { NowRequest, NowResponse } from '@vercel/node'
import { fetch, send, sendError } from '../../../util/http'; import { fetch, send, sendError } from '../../../util/http'
export default async function handler( export default async function handler(
req: NowRequest, req: NowRequest,
res: NowResponse, res: NowResponse
): Promise<void> { ): Promise<void> {
const { query } = req.query; const { query } = req.query
if (!query || typeof query !== 'string') { if (!query || typeof query !== 'string') {
return sendError(res, new Error('No query given')); return sendError(res, new Error('No query given'))
} }
if (/[^a-zA-Z0-9_]/.test(query)) { if (/[^a-zA-Z0-9_]/.test(query)) {
return sendError(res, new Error('Invalid characters')); return sendError(res, new Error('Invalid characters'))
} }
try { try {
const response = await fetch( const response = await fetch(
`https://api.twitter.com/i/users/username_available.json?username=${query}`, `https://api.twitter.com/i/users/username_available.json?username=${query}`,
'GET', 'GET'
).then((res) => res.json()); ).then((res) => res.json())
const availability = response.valid; const availability = response.valid
send(res, { availability }); send(res, { availability })
} catch (err) { } catch (err) {
sendError(res, err); sendError(res, err)
} }
} }

View File

@ -59,5 +59,11 @@
<body> <body>
<noscript>You need to enable JavaScript to run this app.</noscript> <noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div> <div id="root"></div>
<script
async
defer
data-website-id="8d3a7603-b5f6-4a94-9c19-344cc57dfd52"
src="https://umami.uechi.io/umami.js"
></script>
</body> </body>
</html> </html>

View File

@ -1,8 +1,8 @@
import { render } from '@testing-library/react'; import { render } from '@testing-library/react'
import 'mutationobserver-shim'; import 'mutationobserver-shim'
import React, { Suspense } from 'react'; import React, { Suspense } from 'react'
import { BrowserRouter as Router } from 'react-router-dom'; 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 { findByText } = render( const { findByText } = render(
@ -10,8 +10,8 @@ it('renders welcome message', async () => {
<Router> <Router>
<App /> <App />
</Router> </Router>
</Suspense>, </Suspense>
); )
const text = await findByText('Grab a slick name for your new app'); const text = await findByText('Grab a slick name for your new app')
expect(text).toBeTruthy(); expect(text).toBeTruthy()
}); })

View File

@ -1,14 +1,14 @@
import React from 'react'; import React from 'react'
import { Redirect, Route, Switch } from 'react-router-dom'; import { Redirect, Route, Switch } from 'react-router-dom'
import Footer from './components/Footer'; import Footer from './components/Footer'
import Home from './pages/Home'; import Home from './pages/Home'
import Search from './pages/Search'; import Search from './pages/Search'
import { GlobalStyle } from './theme'; import { GlobalStyle } from './theme'
import { useOpenSearch } from './util/hooks'; import { useOpenSearch } from './util/hooks'
import { isStandalone } from './util/pwa'; import { isStandalone } from './util/pwa'
export default function App() { export default function App() {
const OpenSearch = useOpenSearch('/opensearch.xml'); const OpenSearch = useOpenSearch('/opensearch.xml')
return ( return (
<> <>
@ -31,5 +31,5 @@ export default function App() {
{!isStandalone() && <Footer />} {!isStandalone() && <Footer />}
</> </>
); )
} }

View File

@ -1,38 +1,38 @@
import React from 'react'; import React from 'react'
import styled from 'styled-components'; import styled from 'styled-components'
import useSWR from 'swr'; import useSWR from 'swr'
export interface Contributors { export interface Contributors {
projectName: string; projectName: string
projectOwner: string; projectOwner: string
repoType: string; repoType: string
repoHost: string; repoHost: string
files: string[]; files: string[]
imageSize: number; imageSize: number
commit: boolean; commit: boolean
commitConvention: string; commitConvention: string
contributors: Contributor[]; contributors: Contributor[]
contributorsPerLine: number; contributorsPerLine: number
skipCi: boolean; skipCi: boolean
} }
export interface Contributor { export interface Contributor {
login: string; login: string
name: string; name: string
avatar_url: string; avatar_url: string
profile: string; profile: string
contributions: string[]; contributions: string[]
} }
const fetcher = (url: string) => fetch(url).then((r) => r.json()); const fetcher = (url: string) => fetch(url).then((r) => r.json())
const Contributors: React.FC = () => { const Contributors: React.FC = () => {
const { data } = useSWR<Contributors>( const { data } = useSWR<Contributors>(
'https://raw.githubusercontent.com/uetchy/namae/master/.all-contributorsrc', 'https://raw.githubusercontent.com/uetchy/namae/master/.all-contributorsrc',
fetcher, fetcher
); )
if (!data) return <Container>Loading</Container>; if (!data) return <Container>Loading</Container>
return ( return (
<Container> <Container>
@ -48,24 +48,24 @@ const Contributors: React.FC = () => {
</Item> </Item>
))} ))}
</Container> </Container>
); )
}; }
const Container = styled.div` const Container = styled.div`
display: flex; display: flex;
flex-direction: row; flex-direction: row;
`; `
const Item = styled.div` const Item = styled.div`
margin-left: 10px; margin-left: 10px;
:first-child { :first-child {
margin-left: 0; margin-left: 0;
} }
`; `
const avatarSize = 32; const avatarSize = 32
const Avatar = styled.img.attrs({ width: avatarSize, height: avatarSize })` const Avatar = styled.img.attrs({ width: avatarSize, height: avatarSize })`
border-radius: ${avatarSize}px; border-radius: ${avatarSize}px;
`; `
export default Contributors; export default Contributors

View File

@ -1,12 +1,12 @@
import React from 'react'; import React from 'react'
import { OutboundLink } from 'react-ga'; import { OutboundLink } from 'react-ga'
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next'
import { FaGithub, FaProductHunt, FaTwitter } from 'react-icons/fa'; import { FaGithub, FaProductHunt, FaTwitter } from 'react-icons/fa'
import { GoHeart } from 'react-icons/go'; import { GoHeart } from 'react-icons/go'
import styled from 'styled-components'; import styled from 'styled-components'
import { Section } from '../theme'; import { Section } from '../theme'
import { tablet } from '../util/css'; import { tablet } from '../util/css'
import Contributors from '../components/Contributors'; import Contributors from '../components/Contributors'
const Footer: React.FC = () => { const Footer: React.FC = () => {
return ( return (
@ -15,12 +15,12 @@ const Footer: React.FC = () => {
<Community /> <Community />
<About /> <About />
</Container> </Container>
); )
}; }
export default Footer; export default Footer
const Languages = () => { const Languages = () => {
const { t } = useTranslation(); const { t } = useTranslation()
return ( return (
<Pane> <Pane>
@ -46,11 +46,11 @@ const Languages = () => {
</li> </li>
</ul> </ul>
</Pane> </Pane>
); )
}; }
const Community = () => { const Community = () => {
const { t } = useTranslation(); const { t } = useTranslation()
return ( return (
<Pane> <Pane>
@ -92,11 +92,11 @@ const Community = () => {
<Contributors /> <Contributors />
</Box> </Box>
</Pane> </Pane>
); )
}; }
const About = () => { const About = () => {
const { t } = useTranslation(); const { t } = useTranslation()
return ( return (
<Pane> <Pane>
@ -120,7 +120,7 @@ const About = () => {
<Links> <Links>
<OutboundLink <OutboundLink
to={`https://twitter.com/intent/tweet?text=${encodeURIComponent( to={`https://twitter.com/intent/tweet?text=${encodeURIComponent(
`namae — ${t('title')}`, `namae — ${t('title')}`
)}&url=${encodeURIComponent('https://namae.dev')}`} )}&url=${encodeURIComponent('https://namae.dev')}`}
eventLabel="Tweet" eventLabel="Tweet"
aria-label="Tweet this page" aria-label="Tweet this page"
@ -158,8 +158,8 @@ const About = () => {
</Links> </Links>
</ShareBox> </ShareBox>
</Pane> </Pane>
); )
}; }
const Container = styled(Section)` const Container = styled(Section)`
--text: #bdbdbd; --text: #bdbdbd;
@ -187,7 +187,7 @@ const Container = styled(Section)`
${tablet} { ${tablet} {
flex-direction: column; flex-direction: column;
} }
`; `
const Pane = styled.div` const Pane = styled.div`
font-size: 1rem; font-size: 1rem;
@ -195,19 +195,19 @@ const Pane = styled.div`
${tablet} { ${tablet} {
margin-bottom: 50px; margin-bottom: 50px;
} }
`; `
const Box = styled.div` const Box = styled.div`
margin: 15px 0; margin: 15px 0;
`; `
const Title = styled.h3` const Title = styled.h3`
margin-bottom: 15px; margin-bottom: 15px;
`; `
const Subtitle = styled.h4` const Subtitle = styled.h4`
margin-bottom: 12px; margin-bottom: 12px;
`; `
const Links = styled.div` const Links = styled.div`
display: flex; display: flex;
@ -216,7 +216,7 @@ const Links = styled.div`
a { a {
margin-right: 10px; margin-right: 10px;
} }
`; `
const ShareBox = styled.div` const ShareBox = styled.div`
margin-top: 15px; margin-top: 15px;
@ -224,10 +224,10 @@ const ShareBox = styled.div`
font-size: 1.5rem; font-size: 1.5rem;
display: flex; display: flex;
align-items: center; align-items: center;
`; `
const Bold = styled.span` const Bold = styled.span`
font-weight: bold; font-weight: bold;
`; `
const SponsorBadge = styled.div` const SponsorBadge = styled.div`
padding: 5px 13px 5px 10px; padding: 5px 13px 5px 10px;
@ -252,4 +252,4 @@ const SponsorBadge = styled.div`
color: rgb(236, 69, 171); color: rgb(236, 69, 171);
margin-right: 5px; margin-right: 5px;
} }
`; `

View File

@ -1,55 +1,55 @@
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 { Link, useHistory } from 'react-router-dom'
import { sanitize } from '../util/text'; import { sanitize } from '../util/text'
import { sendQueryEvent } from '../util/analytics'; import { sendQueryEvent } from '../util/analytics'
import { mobile } from '../util/css'; import { mobile } from '../util/css'
import Suggestion from './Suggestion'; import Suggestion from './Suggestion'
import { useDeferredState } from '../util/hooks'; import { useDeferredState } from '../util/hooks'
const Form: React.FC<{ const Form: React.FC<{
initialValue?: string; initialValue?: string
}> = ({ initialValue = '' }) => { }> = ({ initialValue = '' }) => {
const history = useHistory(); const history = useHistory()
const [inputValue, setInputValue] = useState(initialValue); const [inputValue, setInputValue] = useState(initialValue)
const [suggestionQuery, setSuggestionQuery] = useDeferredState(800, ''); const [suggestionQuery, setSuggestionQuery] = useDeferredState(800, '')
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()
function search(query: string) { function search(query: string) {
sendQueryEvent(sanitize(query)); sendQueryEvent(sanitize(query))
history.push(`/s/${query}`); history.push(`/s/${query}`)
} }
// set input value // set input value
function onInputChange(e: React.FormEvent<HTMLInputElement>): void { function onInputChange(e: React.FormEvent<HTMLInputElement>): void {
setInputValue(e.currentTarget.value); setInputValue(e.currentTarget.value)
} }
// invoke when user clicked one of the suggested items // invoke when user clicked one of the suggested items
function onSuggestionCompleted(name: string): void { function onSuggestionCompleted(name: string): void {
setInputValue(name); setInputValue(name)
search(name); search(name)
setSuggested(true); setSuggested(true)
} }
function onSubmitQuery(e: React.FormEvent) { function onSubmitQuery(e: React.FormEvent) {
e.preventDefault(); e.preventDefault()
inputRef.current!.blur(); inputRef.current!.blur()
if (!inputValue || inputValue === '') { if (!inputValue || inputValue === '') {
return; return
} }
search(inputValue); search(inputValue)
} }
useEffect(() => { useEffect(() => {
const modifiedValue = sanitize(inputValue); const modifiedValue = sanitize(inputValue)
setSuggestionQuery(modifiedValue); setSuggestionQuery(modifiedValue)
}, [inputValue, setSuggestionQuery]); }, [inputValue, setSuggestionQuery])
const queryGiven = suggestionQuery && suggestionQuery !== ''; const queryGiven = suggestionQuery && suggestionQuery !== ''
return ( return (
<InputContainer> <InputContainer>
@ -71,10 +71,10 @@ const Form: React.FC<{
<Suggestion onSubmit={onSuggestionCompleted} query={suggestionQuery} /> <Suggestion onSubmit={onSuggestionCompleted} query={suggestionQuery} />
) : null} ) : null}
</InputContainer> </InputContainer>
); )
}; }
export default Form; export default Form
const InputContainer = styled.div` const InputContainer = styled.div`
display: flex; display: flex;
@ -91,7 +91,7 @@ const InputContainer = styled.div`
transform: translateY(20px); transform: translateY(20px);
border-radius: 30px; border-radius: 30px;
} }
`; `
const InputHeader = styled.div` const InputHeader = styled.div`
display: flex; display: flex;
@ -99,7 +99,7 @@ const InputHeader = styled.div`
align-items: center; align-items: center;
justify-content: center; justify-content: center;
margin: 12px 0 5px 0; margin: 12px 0 5px 0;
`; `
const Logo = styled(Link)` const Logo = styled(Link)`
cursor: pointer; cursor: pointer;
@ -107,7 +107,7 @@ const Logo = styled(Link)`
${mobile} { ${mobile} {
font-size: 15px; font-size: 15px;
} }
`; `
const LogoImage = styled.img` const LogoImage = styled.img`
width: 140px; width: 140px;
@ -115,7 +115,7 @@ const LogoImage = styled.img`
${mobile} { ${mobile} {
width: 90px; width: 90px;
} }
`; `
const InputView = styled.input.attrs({ const InputView = styled.input.attrs({
type: 'search', type: 'search',
@ -141,4 +141,4 @@ const InputView = styled.input.attrs({
::placeholder { ::placeholder {
color: #c8cdda; color: #c8cdda;
} }
`; `

View File

@ -1,4 +1,4 @@
import React from 'react'; import React from 'react'
export const SpectrumIcon: React.FC = () => ( export const SpectrumIcon: React.FC = () => (
<svg <svg
@ -12,7 +12,7 @@ export const SpectrumIcon: React.FC = () => (
> >
<path d="M6 14.5C6 15.3284 6.67157 16 7.5 16H9C12.866 16 16 19.134 16 23V24.5C16 25.3284 16.6716 26 17.5 26H24.5C25.3284 26 26 25.3284 26 24.5V23C26 13.6111 18.3889 6 9 6H7.5C6.67157 6 6 6.67157 6 7.5V14.5Z" /> <path d="M6 14.5C6 15.3284 6.67157 16 7.5 16H9C12.866 16 16 19.134 16 23V24.5C16 25.3284 16.6716 26 17.5 26H24.5C25.3284 26 26 25.3284 26 24.5V23C26 13.6111 18.3889 6 9 6H7.5C6.67157 6 6 6.67157 6 7.5V14.5Z" />
</svg> </svg>
); )
export const NowIcon: React.FC = () => ( export const NowIcon: React.FC = () => (
<svg <svg
@ -28,7 +28,7 @@ export const NowIcon: React.FC = () => (
<polygon id="Logotype---Black" points="350 150 407 250 293 250"></polygon> <polygon id="Logotype---Black" points="350 150 407 250 293 250"></polygon>
</g> </g>
</svg> </svg>
); )
export const NetlifyIcon: React.FC = () => ( export const NetlifyIcon: React.FC = () => (
<svg <svg
@ -45,7 +45,7 @@ export const NetlifyIcon: React.FC = () => (
> >
<path d="M185.532 88.839l-.094-.04a.396.396 0 0 1-.154-.087a.734.734 0 0 1-.187-.621l5.167-31.553l24.229 24.209l-25.198 10.709a.555.555 0 0 1-.22.04h-.101a.694.694 0 0 1-.134-.114a11.468 11.468 0 0 0-3.308-2.543zm35.144-1.923l25.906 25.878c5.38 5.381 8.075 8.065 9.057 11.177c.147.46.267.921.361 1.395l-61.913-26.192a4.868 4.868 0 0 0-.1-.04c-.248-.1-.535-.214-.535-.467c0-.254.294-.374.541-.474l.08-.034l26.603-11.243zm34.268 46.756c-1.337 2.51-3.944 5.114-8.355 9.527l-29.209 29.17l-37.777-7.858l-.2-.04c-.335-.054-.689-.114-.689-.414a11.387 11.387 0 0 0-4.378-7.965c-.154-.154-.113-.394-.067-.615c0-.033 0-.066.014-.093l7.105-43.571l.026-.147c.04-.334.1-.721.401-.721a11.566 11.566 0 0 0 7.754-4.44c.06-.067.1-.14.18-.18c.214-.1.468 0 .689.093l64.5 27.254h.006zm-44.28 45.407l-48.031 47.978l8.22-50.475l.014-.067a.905.905 0 0 1 .04-.193c.067-.16.24-.227.408-.294l.08-.034c1.8-.767 3.392-1.95 4.646-3.451c.16-.187.354-.368.601-.401c.064-.01.13-.01.194 0l33.82 6.944l.007-.007zm-58.198 58.133l-5.414 5.408l-59.854-86.408a2.831 2.831 0 0 0-.067-.094c-.093-.127-.194-.253-.173-.4c.006-.107.073-.2.147-.28l.066-.087c.18-.268.335-.535.502-.822l.133-.233l.02-.02c.094-.16.18-.314.341-.401c.14-.067.335-.04.488-.007l66.311 13.66c.186.03.36.105.508.22c.087.088.107.181.127.288a11.735 11.735 0 0 0 6.871 7.845c.187.093.107.3.02.52a1.588 1.588 0 0 0-.1.301c-.835 5.074-8 48.726-9.926 60.51zm-11.309 11.29c-3.99 3.946-6.343 6.035-9.003 6.877a13.382 13.382 0 0 1-8.06 0c-3.115-.989-5.809-3.672-11.19-9.054l-60.108-60.042l15.7-24.323a1 1 0 0 1 .268-.314c.167-.12.408-.066.608 0a16.285 16.285 0 0 0 10.948-.554c.18-.066.361-.113.502.014c.07.064.133.135.187.213l60.148 87.19v-.007zm-94.156-68.008l-13.789-13.773l27.23-11.604a.562.562 0 0 1 .221-.047c.227 0 .361.227.481.434c.274.42.564.83.87 1.229l.086.106c.08.114.027.227-.053.334l-15.04 23.321h-.006zM27.11 160.625L9.665 143.199c-2.968-2.964-5.12-5.114-6.617-6.963l53.043 10.99l.2.033c.328.053.69.113.69.42c0 .334-.395.488-.73.614l-.153.067l-28.988 12.265zM0 127.275a13.34 13.34 0 0 1 .602-3.304c.989-3.112 3.676-5.796 9.063-11.177l22.324-22.3a14524.43 14524.43 0 0 0 30.92 44.647c.18.24.38.507.174.707c-.976 1.075-1.952 2.25-2.64 3.526c-.075.163-.19.306-.335.413c-.087.054-.18.034-.28.014h-.014L0 127.269v.007zm37.965-42.75l30.017-29.984c2.82 1.235 13.087 5.568 22.27 9.44c6.952 2.939 13.288 5.61 15.28 6.477c.2.08.381.16.468.36c.053.12.027.274 0 .401a13.363 13.363 0 0 0 3.496 12.205c.2.2 0 .487-.174.734l-.094.14l-30.478 47.157c-.08.134-.154.247-.288.334c-.16.1-.387.053-.575.007a15.215 15.215 0 0 0-3.629-.494c-1.096 0-2.286.2-3.489.42h-.007c-.133.02-.254.047-.36-.033a1.403 1.403 0 0 1-.301-.34L37.965 84.525zm36.08-36.04l38.86-38.817c5.38-5.375 8.074-8.065 11.188-9.047a13.382 13.382 0 0 1 8.061 0c3.115.982 5.808 3.672 11.189 9.047l8.422 8.413l-27.638 42.756a1.035 1.035 0 0 1-.274.32c-.167.114-.401.067-.602 0a14.028 14.028 0 0 0-12.833 2.471c-.18.187-.448.08-.675-.02c-3.61-1.569-31.682-13.42-35.699-15.122zm83.588-24.542l25.52 25.49l-6.15 38.044v.1a.9.9 0 0 1-.053.254c-.067.133-.201.16-.335.2a12.237 12.237 0 0 0-3.662 1.823a1.029 1.029 0 0 0-.134.113c-.074.08-.147.154-.267.167a.763.763 0 0 1-.288-.047l-38.887-16.504l-.073-.034c-.248-.1-.542-.22-.542-.474a14.664 14.664 0 0 0-2.072-6.109c-.187-.307-.394-.627-.234-.941l27.177-42.082zM131.352 81.4l36.454 15.423c.2.093.421.18.508.387a.707.707 0 0 1 0 .38c-.107.535-.2 1.142-.2 1.757v1.021c0 .254-.261.36-.502.46l-.073.027c-5.775 2.464-81.076 34.538-81.19 34.538c-.113 0-.234 0-.347-.113c-.2-.2 0-.48.18-.735l.094-.133l29.957-46.335l.053-.08c.174-.281.375-.595.696-.595l.3.047c.682.093 1.284.18 1.892.18c4.545 0 8.756-2.21 11.296-5.989c.06-.1.137-.19.227-.267c.18-.133.448-.066.655.027zm-41.748 61.324l82.079-34.965s.12 0 .234.114c.447.447.828.747 1.196 1.028l.18.113c.168.094.335.2.348.374c0 .067 0 .107-.013.167l-7.032 43.144l-.027.174c-.046.333-.093.714-.407.714a11.558 11.558 0 0 0-9.177 5.655l-.034.053c-.093.154-.18.3-.334.38c-.14.068-.32.041-.468.008l-65.455-13.487c-.067-.013-1.016-3.465-1.09-3.472z" /> <path d="M185.532 88.839l-.094-.04a.396.396 0 0 1-.154-.087a.734.734 0 0 1-.187-.621l5.167-31.553l24.229 24.209l-25.198 10.709a.555.555 0 0 1-.22.04h-.101a.694.694 0 0 1-.134-.114a11.468 11.468 0 0 0-3.308-2.543zm35.144-1.923l25.906 25.878c5.38 5.381 8.075 8.065 9.057 11.177c.147.46.267.921.361 1.395l-61.913-26.192a4.868 4.868 0 0 0-.1-.04c-.248-.1-.535-.214-.535-.467c0-.254.294-.374.541-.474l.08-.034l26.603-11.243zm34.268 46.756c-1.337 2.51-3.944 5.114-8.355 9.527l-29.209 29.17l-37.777-7.858l-.2-.04c-.335-.054-.689-.114-.689-.414a11.387 11.387 0 0 0-4.378-7.965c-.154-.154-.113-.394-.067-.615c0-.033 0-.066.014-.093l7.105-43.571l.026-.147c.04-.334.1-.721.401-.721a11.566 11.566 0 0 0 7.754-4.44c.06-.067.1-.14.18-.18c.214-.1.468 0 .689.093l64.5 27.254h.006zm-44.28 45.407l-48.031 47.978l8.22-50.475l.014-.067a.905.905 0 0 1 .04-.193c.067-.16.24-.227.408-.294l.08-.034c1.8-.767 3.392-1.95 4.646-3.451c.16-.187.354-.368.601-.401c.064-.01.13-.01.194 0l33.82 6.944l.007-.007zm-58.198 58.133l-5.414 5.408l-59.854-86.408a2.831 2.831 0 0 0-.067-.094c-.093-.127-.194-.253-.173-.4c.006-.107.073-.2.147-.28l.066-.087c.18-.268.335-.535.502-.822l.133-.233l.02-.02c.094-.16.18-.314.341-.401c.14-.067.335-.04.488-.007l66.311 13.66c.186.03.36.105.508.22c.087.088.107.181.127.288a11.735 11.735 0 0 0 6.871 7.845c.187.093.107.3.02.52a1.588 1.588 0 0 0-.1.301c-.835 5.074-8 48.726-9.926 60.51zm-11.309 11.29c-3.99 3.946-6.343 6.035-9.003 6.877a13.382 13.382 0 0 1-8.06 0c-3.115-.989-5.809-3.672-11.19-9.054l-60.108-60.042l15.7-24.323a1 1 0 0 1 .268-.314c.167-.12.408-.066.608 0a16.285 16.285 0 0 0 10.948-.554c.18-.066.361-.113.502.014c.07.064.133.135.187.213l60.148 87.19v-.007zm-94.156-68.008l-13.789-13.773l27.23-11.604a.562.562 0 0 1 .221-.047c.227 0 .361.227.481.434c.274.42.564.83.87 1.229l.086.106c.08.114.027.227-.053.334l-15.04 23.321h-.006zM27.11 160.625L9.665 143.199c-2.968-2.964-5.12-5.114-6.617-6.963l53.043 10.99l.2.033c.328.053.69.113.69.42c0 .334-.395.488-.73.614l-.153.067l-28.988 12.265zM0 127.275a13.34 13.34 0 0 1 .602-3.304c.989-3.112 3.676-5.796 9.063-11.177l22.324-22.3a14524.43 14524.43 0 0 0 30.92 44.647c.18.24.38.507.174.707c-.976 1.075-1.952 2.25-2.64 3.526c-.075.163-.19.306-.335.413c-.087.054-.18.034-.28.014h-.014L0 127.269v.007zm37.965-42.75l30.017-29.984c2.82 1.235 13.087 5.568 22.27 9.44c6.952 2.939 13.288 5.61 15.28 6.477c.2.08.381.16.468.36c.053.12.027.274 0 .401a13.363 13.363 0 0 0 3.496 12.205c.2.2 0 .487-.174.734l-.094.14l-30.478 47.157c-.08.134-.154.247-.288.334c-.16.1-.387.053-.575.007a15.215 15.215 0 0 0-3.629-.494c-1.096 0-2.286.2-3.489.42h-.007c-.133.02-.254.047-.36-.033a1.403 1.403 0 0 1-.301-.34L37.965 84.525zm36.08-36.04l38.86-38.817c5.38-5.375 8.074-8.065 11.188-9.047a13.382 13.382 0 0 1 8.061 0c3.115.982 5.808 3.672 11.189 9.047l8.422 8.413l-27.638 42.756a1.035 1.035 0 0 1-.274.32c-.167.114-.401.067-.602 0a14.028 14.028 0 0 0-12.833 2.471c-.18.187-.448.08-.675-.02c-3.61-1.569-31.682-13.42-35.699-15.122zm83.588-24.542l25.52 25.49l-6.15 38.044v.1a.9.9 0 0 1-.053.254c-.067.133-.201.16-.335.2a12.237 12.237 0 0 0-3.662 1.823a1.029 1.029 0 0 0-.134.113c-.074.08-.147.154-.267.167a.763.763 0 0 1-.288-.047l-38.887-16.504l-.073-.034c-.248-.1-.542-.22-.542-.474a14.664 14.664 0 0 0-2.072-6.109c-.187-.307-.394-.627-.234-.941l27.177-42.082zM131.352 81.4l36.454 15.423c.2.093.421.18.508.387a.707.707 0 0 1 0 .38c-.107.535-.2 1.142-.2 1.757v1.021c0 .254-.261.36-.502.46l-.073.027c-5.775 2.464-81.076 34.538-81.19 34.538c-.113 0-.234 0-.347-.113c-.2-.2 0-.48.18-.735l.094-.133l29.957-46.335l.053-.08c.174-.281.375-.595.696-.595l.3.047c.682.093 1.284.18 1.892.18c4.545 0 8.756-2.21 11.296-5.989c.06-.1.137-.19.227-.267c.18-.133.448-.066.655.027zm-41.748 61.324l82.079-34.965s.12 0 .234.114c.447.447.828.747 1.196 1.028l.18.113c.168.094.335.2.348.374c0 .067 0 .107-.013.167l-7.032 43.144l-.027.174c-.046.333-.093.714-.407.714a11.558 11.558 0 0 0-9.177 5.655l-.034.053c-.093.154-.18.3-.334.38c-.14.068-.32.041-.468.008l-65.455-13.487c-.067-.013-1.016-3.465-1.09-3.472z" />
</svg> </svg>
); )
export const OcamlIcon: React.FC = () => ( export const OcamlIcon: React.FC = () => (
<svg <svg
@ -96,4 +96,4 @@ export const OcamlIcon: React.FC = () => (
/> />
</g> </g>
</svg> </svg>
); )

View File

@ -1,22 +1,22 @@
import React, { useEffect, useState, useRef } from 'react'; import React, { useEffect, useState, useRef } from 'react'
import styled from 'styled-components'; import styled from 'styled-components'
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next'
import fetch from 'isomorphic-unfetch'; import fetch from 'isomorphic-unfetch'
import { TiArrowSync } from 'react-icons/ti'; import { TiArrowSync } from 'react-icons/ti'
import { motion } from 'framer-motion'; import { motion } from 'framer-motion'
import { capitalize, stem, germanify, njoin, lower, upper } from '../util/text'; import { capitalize, stem, germanify, njoin, lower, upper } from '../util/text'
import { sampleFromArray, fillArray } from '../util/array'; import { sampleFromArray, fillArray } from '../util/array'
import { mobile, slideUp } from '../util/css'; import { mobile, slideUp } from '../util/css'
import { sanitize } from '../util/text'; import { sanitize } from '../util/text'
import { import {
sendShuffleSuggestionEvent, sendShuffleSuggestionEvent,
sendAcceptSuggestionEvent, sendAcceptSuggestionEvent,
} from '../util/analytics'; } from '../util/analytics'
type Modifier = (word: string) => string; type Modifier = (word: string) => string
const maximumCount = 3; const maximumCount = 3
const modifiers: Modifier[] = [ const modifiers: Modifier[] = [
(word): string => `${capitalize(germanify(word))}`, (word): string => `${capitalize(germanify(word))}`,
(word): string => `${capitalize(word)}`, (word): string => `${capitalize(word)}`,
@ -144,83 +144,83 @@ const modifiers: Modifier[] = [
(word): string => njoin(capitalize(word), 'joy'), (word): string => njoin(capitalize(word), 'joy'),
(word): string => njoin(lower(word), 'lint', { elision: false }), (word): string => njoin(lower(word), 'lint', { elision: false }),
(word): string => njoin(lower(word), 'ly', { elision: false }), (word): string => njoin(lower(word), 'ly', { elision: false }),
]; ]
function modifyWord(word: string): string { function modifyWord(word: string): string {
return modifiers[Math.floor(Math.random() * modifiers.length)](word); return modifiers[Math.floor(Math.random() * modifiers.length)](word)
} }
async function findSynonyms(word: string): Promise<string[]> { async function findSynonyms(word: string): Promise<string[]> {
try { try {
const response = await fetch( 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( `https://translate.googleapis.com/translate_a/single?client=gtx&sl=auto&dt=ss&ie=UTF-8&oe=UTF-8&dj=1&q=${encodeURIComponent(
word, word
)}`, )}`
); )
const json: { const json: {
synsets: Array<{ entry: Array<{ synonym: string[] }> }>; synsets: Array<{ entry: Array<{ synonym: string[] }> }>
} = await response.json(); } = await response.json()
const synonyms = Array.from( const synonyms = Array.from(
new Set<string>( new Set<string>(
json.synsets.reduce( json.synsets.reduce(
(sum, synset) => [...sum, ...synset.entry.map((e) => e.synonym[0])], (sum, synset) => [...sum, ...synset.entry.map((e) => e.synonym[0])],
[] as string[], [] as string[]
), )
), )
) )
.filter((word) => !/[\s-]/.exec(word)) .filter((word) => !/[\s-]/.exec(word))
.map((word) => sanitize(word)); .map((word) => sanitize(word))
return synonyms; return synonyms
} catch (err) { } catch (err) {
return []; return []
} }
} }
const Suggestion: React.FC<{ const Suggestion: React.FC<{
query: string; query: string
onSubmit: (name: string) => void; onSubmit: (name: string) => void
}> = ({ query, onSubmit }) => { }> = ({ query, onSubmit }) => {
const { t } = useTranslation(); const { t } = useTranslation()
const synonymRef = useRef<string[]>([]); const synonymRef = useRef<string[]>([])
const [bestWords, setBestWords] = useState<string[]>([]); const [bestWords, setBestWords] = useState<string[]>([])
function shuffle(): void { function shuffle(): void {
const best = fillArray( const best = fillArray(
sampleFromArray(synonymRef.current, maximumCount), sampleFromArray(synonymRef.current, maximumCount),
query, query,
maximumCount, maximumCount
).map((word) => modifyWord(word)); ).map((word) => modifyWord(word))
setBestWords(best); setBestWords(best)
} }
function applyQuery(name: string): void { function applyQuery(name: string): void {
sendAcceptSuggestionEvent(); sendAcceptSuggestionEvent()
onSubmit(name); onSubmit(name)
} }
function onShuffleButtonClicked() { function onShuffleButtonClicked() {
sendShuffleSuggestionEvent(); sendShuffleSuggestionEvent()
shuffle(); shuffle()
} }
useEffect(() => { useEffect(() => {
let isEffective = true; 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) { if (!isEffective) {
return; return
} }
synonymRef.current = [query, ...synonyms]; synonymRef.current = [query, ...synonyms]
shuffle(); shuffle()
} }
}; }
fn(); fn()
return () => { return () => {
isEffective = false; isEffective = false
}; }
// eslint-disable-next-line // eslint-disable-next-line
}, [query]); }, [query])
return ( return (
<Container> <Container>
@ -241,10 +241,10 @@ const Suggestion: React.FC<{
<TiArrowSync /> <TiArrowSync />
</Button> </Button>
</Container> </Container>
); )
}; }
export default Suggestion; export default Suggestion
const Container = styled.div` const Container = styled.div`
margin-top: 20px; margin-top: 20px;
@ -258,7 +258,7 @@ const Container = styled.div`
${mobile} { ${mobile} {
margin-top: 15px; margin-top: 15px;
} }
`; `
const Title = styled.div` const Title = styled.div`
padding: 0 10px; padding: 0 10px;
@ -268,7 +268,7 @@ const Title = styled.div`
text-transform: uppercase; text-transform: uppercase;
font-size: 12px; font-size: 12px;
user-select: none; user-select: none;
`; `
const Items = styled.div` const Items = styled.div`
margin: 5px 0; margin: 5px 0;
@ -281,7 +281,7 @@ const Items = styled.div`
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
} }
`; `
const Item = styled.div<{ delay: number }>` const Item = styled.div<{ delay: number }>`
margin: 10px 10px 0; margin: 10px 10px 0;
@ -305,7 +305,7 @@ const Item = styled.div<{ delay: number }>`
padding-bottom: 0; padding-bottom: 0;
font-size: 1rem; font-size: 1rem;
} }
`; `
const Button = styled(motion.div).attrs({ const Button = styled(motion.div).attrs({
whileHover: { scale: 1.1 }, whileHover: { scale: 1.1 },
@ -331,4 +331,4 @@ const Button = styled(motion.div).attrs({
&:active { &:active {
background: #a17ff5; background: #a17ff5;
} }
`; `

View File

@ -1,7 +1,7 @@
import React from 'react'; import React from 'react'
import styled from 'styled-components'; import styled from 'styled-components'
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next'
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom'
import { import {
FaMapSigns, FaMapSigns,
@ -18,13 +18,13 @@ import {
FaAws, FaAws,
FaJsSquare, FaJsSquare,
FaBuilding, FaBuilding,
} from 'react-icons/fa'; } from 'react-icons/fa'
import { IoIosBeer } from 'react-icons/io'; import { IoIosBeer } from 'react-icons/io'
import { DiRust, DiHeroku, DiFirebase } from 'react-icons/di'; import { DiRust, DiHeroku, DiFirebase } from 'react-icons/di'
import { SpectrumIcon, NowIcon, NetlifyIcon, OcamlIcon } from './Icons'; import { SpectrumIcon, NowIcon, NetlifyIcon, OcamlIcon } from './Icons'
import { mobile } from '../util/css'; import { mobile } from '../util/css'
import { sendGettingStartedEvent } from '../util/analytics'; import { sendGettingStartedEvent } from '../util/analytics'
const supportedProviders: Record<string, React.ReactNode> = { const supportedProviders: Record<string, React.ReactNode> = {
domains: <FaMapSigns />, domains: <FaMapSigns />,
@ -50,10 +50,10 @@ const supportedProviders: Record<string, React.ReactNode> = {
githubSearch: <FaGithub />, githubSearch: <FaGithub />,
appStore: <FaAppStore />, appStore: <FaAppStore />,
nta: <FaBuilding />, nta: <FaBuilding />,
}; }
const Welcome: React.FC = () => { const Welcome: React.FC = () => {
const { t } = useTranslation(); const { t } = useTranslation()
return ( return (
<Container> <Container>
@ -82,10 +82,10 @@ const Welcome: React.FC = () => {
<blockquote>Soon</blockquote> <blockquote>Soon</blockquote>
</Section> </Section>
</Container> </Container>
); )
}; }
export default Welcome; export default Welcome
const Container = styled.div` const Container = styled.div`
padding-bottom: 40px; padding-bottom: 40px;
@ -97,7 +97,7 @@ const Container = styled.div`
text-align: left; text-align: left;
font-size: 1.1rem; font-size: 1.1rem;
} }
`; `
const Section = styled.div` const Section = styled.div`
padding: 100px 20vw; padding: 100px 20vw;
@ -105,7 +105,7 @@ const Section = styled.div`
${mobile} { ${mobile} {
padding: 60px 40px; padding: 60px 40px;
} }
`; `
const HighlightSection = styled.div` const HighlightSection = styled.div`
padding: 100px 20vw; padding: 100px 20vw;
@ -123,7 +123,7 @@ const HighlightSection = styled.div`
color: white; color: white;
/* background-image: linear-gradient(180deg, #a57bf3 0%, #4364e1 100%); */ /* background-image: linear-gradient(180deg, #a57bf3 0%, #4364e1 100%); */
background: #632bec; background: #632bec;
`; `
const Title = styled.h1` const Title = styled.h1`
line-height: 1.6em; line-height: 1.6em;
@ -133,23 +133,23 @@ const Title = styled.h1`
${mobile} { ${mobile} {
font-size: 2.5em; font-size: 2.5em;
} }
`; `
const HeroTitle = styled(Title)` const HeroTitle = styled(Title)`
padding-bottom: 30px; padding-bottom: 30px;
line-height: 1em; line-height: 1em;
`; `
const HeroText = styled.p` const HeroText = styled.p`
font-size: 1.2em; font-size: 1.2em;
font-weight: 400; font-weight: 400;
line-height: 1.3em; line-height: 1.3em;
color: #3c3c3c; color: #3c3c3c;
`; `
const ButtonContainer = styled.div` const ButtonContainer = styled.div`
margin: 10px 0 0 0; margin: 10px 0 0 0;
`; `
const List = styled.div` const List = styled.div`
display: flex; display: flex;
@ -162,7 +162,7 @@ const List = styled.div`
${mobile} { ${mobile} {
justify-content: flex-start; justify-content: flex-start;
} }
`; `
const ListItem = styled.div` const ListItem = styled.div`
margin: 20px 25px; margin: 20px 25px;
@ -179,7 +179,7 @@ const ListItem = styled.div`
svg { svg {
margin-right: 5px; margin-right: 5px;
} }
`; `
const ListButton = styled.div` const ListButton = styled.div`
margin: 10px 5px; margin: 10px 5px;
@ -204,4 +204,4 @@ const ListButton = styled.div`
background: black; background: black;
} }
} }
`; `

View File

@ -1,21 +1,21 @@
import useFetch from 'fetch-suspense'; import useFetch from 'fetch-suspense'
import Tooltip from 'rc-tooltip'; import Tooltip from 'rc-tooltip'
import React, { Suspense, useEffect, useState } from 'react'; import React, { Suspense, useEffect, useState } from 'react'
import { OutboundLink } from 'react-ga'; import { OutboundLink } from 'react-ga'
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next'
import { GoInfo } from 'react-icons/go'; import { GoInfo } from 'react-icons/go'
import { IoIosFlash } from 'react-icons/io'; import { IoIosFlash } from 'react-icons/io'
import BarLoader from 'react-spinners/BarLoader'; import BarLoader from 'react-spinners/BarLoader'
import styled from 'styled-components'; import styled from 'styled-components'
import { useStoreActions } from '../../store'; import { useStoreActions } from '../../store'
import { sendError, sendExpandEvent } from '../../util/analytics'; import { sendError, sendExpandEvent } from '../../util/analytics'
import { mobile } from '../../util/css'; import { mobile } from '../../util/css'
export const COLORS = { export const COLORS = {
available: '#6e00ff', available: '#6e00ff',
unavailable: 'darkgrey', unavailable: 'darkgrey',
error: '#ff388b', error: '#ff388b',
}; }
export const Card: React.FC<{ title: string }> = ({ title, children }) => { export const Card: React.FC<{ title: string }> = ({ title, children }) => {
return ( return (
@ -25,25 +25,25 @@ export const Card: React.FC<{ title: string }> = ({ title, children }) => {
<ErrorHandler>{children}</ErrorHandler> <ErrorHandler>{children}</ErrorHandler>
</CardContent> </CardContent>
</CardContainer> </CardContainer>
); )
}; }
export const Repeater: React.FC<{ export const Repeater: React.FC<{
items: string[]; items: string[]
moreItems?: string[]; moreItems?: string[]
children: (name: string) => React.ReactNode; children: (name: string) => React.ReactNode
}> = ({ items = [], moreItems = [], children }) => { }> = ({ items = [], moreItems = [], children }) => {
const [revealAlternatives, setRevealAlternatives] = useState(false); const [revealAlternatives, setRevealAlternatives] = useState(false)
const { t } = useTranslation(); const { t } = useTranslation()
function onClick() { function onClick() {
sendExpandEvent(); sendExpandEvent()
setRevealAlternatives(true); setRevealAlternatives(true)
} }
useEffect(() => { useEffect(() => {
setRevealAlternatives(false); setRevealAlternatives(false)
}, [items, moreItems]); }, [items, moreItems])
return ( return (
<> <>
@ -60,38 +60,38 @@ export const Repeater: React.FC<{
<Button onClick={onClick}>{t('showMore')}</Button> <Button onClick={onClick}>{t('showMore')}</Button>
) : null} ) : null}
</> </>
); )
}; }
interface Response { interface Response {
error?: string; error?: string
availability: boolean; availability: boolean
} }
class APIError extends Error { class APIError extends Error {
constructor(message?: string) { constructor(message?: string) {
super(message); super(message)
Object.setPrototypeOf(this, APIError.prototype); Object.setPrototypeOf(this, APIError.prototype)
} }
} }
class NotFoundError extends Error { class NotFoundError extends Error {
constructor(message?: string) { constructor(message?: string) {
super(message); super(message)
Object.setPrototypeOf(this, NotFoundError.prototype); Object.setPrototypeOf(this, NotFoundError.prototype)
} }
} }
export const DedicatedAvailability: React.FC<{ export const DedicatedAvailability: React.FC<{
name: string; name: string
query?: string; query?: string
message?: string; message?: string
messageIfTaken?: string; messageIfTaken?: string
service: string; service: string
link: string; link: string
linkIfTaken?: string; linkIfTaken?: string
prefix?: string; prefix?: string
suffix?: string; suffix?: string
icon: React.ReactNode; icon: React.ReactNode
}> = ({ }> = ({
name, name,
query = undefined, query = undefined,
@ -104,19 +104,19 @@ export const DedicatedAvailability: React.FC<{
suffix = '', suffix = '',
icon, icon,
}) => { }) => {
const increaseCounter = useStoreActions((actions) => actions.stats.add); const increaseCounter = useStoreActions((actions) => actions.stats.add)
const response = useFetch( const response = useFetch(
`/api/services/${service}/${encodeURIComponent(query || name)}`, `/api/services/${service}/${encodeURIComponent(query || name)}`
) as Response; ) as Response
if (response.error) { if (response.error) {
throw new APIError(`${service}: ${response.error}`); throw new APIError(`${service}: ${response.error}`)
} }
useEffect(() => { useEffect(() => {
increaseCounter(response.availability); increaseCounter(response.availability)
// eslint-disable-next-line // eslint-disable-next-line
}, []); }, [])
return ( return (
<Result <Result
@ -128,19 +128,19 @@ export const DedicatedAvailability: React.FC<{
suffix={suffix} suffix={suffix}
availability={response.availability} availability={response.availability}
/> />
); )
}; }
export const ExistentialAvailability: React.FC<{ export const ExistentialAvailability: React.FC<{
name: string; name: string
target: string; target: string
message?: string; message?: string
messageIfTaken?: string; messageIfTaken?: string
link: string; link: string
linkIfTaken?: string; linkIfTaken?: string
prefix?: string; prefix?: string
suffix?: string; suffix?: string
icon: React.ReactNode; icon: React.ReactNode
}> = ({ }> = ({
name, name,
message = '', message = '',
@ -152,19 +152,19 @@ export const ExistentialAvailability: React.FC<{
suffix = '', suffix = '',
icon, icon,
}) => { }) => {
const increaseCounter = useStoreActions((actions) => actions.stats.add); const increaseCounter = useStoreActions((actions) => actions.stats.add)
const response = useFetch(target, undefined, { metadata: true }); const response = useFetch(target, undefined, { metadata: true })
if (response.status !== 404 && response.status !== 200) { if (response.status !== 404 && response.status !== 200) {
throw new NotFoundError(`${name}: ${response.status}`); throw new NotFoundError(`${name}: ${response.status}`)
} }
const availability = response.status === 404; const availability = response.status === 404
useEffect(() => { useEffect(() => {
increaseCounter(availability); increaseCounter(availability)
// eslint-disable-next-line // eslint-disable-next-line
}, []); }, [])
return ( return (
<Result <Result
@ -176,17 +176,17 @@ export const ExistentialAvailability: React.FC<{
suffix={suffix} suffix={suffix}
availability={availability} availability={availability}
/> />
); )
}; }
export const Result: React.FC<{ export const Result: React.FC<{
title: string; title: string
message?: string; message?: string
link?: string; link?: string
icon: React.ReactNode; icon: React.ReactNode
prefix?: string; prefix?: string
suffix?: string; suffix?: string
availability?: boolean; availability?: boolean
}> = ({ }> = ({
title, title,
message = '', message = '',
@ -202,13 +202,13 @@ export const Result: React.FC<{
{title} {title}
{suffix} {suffix}
</> </>
); )
const itemColor = const itemColor =
availability === undefined availability === undefined
? 'inherit' ? 'inherit'
: availability : availability
? COLORS.available ? COLORS.available
: COLORS.unavailable; : COLORS.unavailable
return ( return (
<ResultContainer> <ResultContainer>
<Tooltip overlay={message} placement="top" trigger={['hover']}> <Tooltip overlay={message} placement="top" trigger={['hover']}>
@ -235,8 +235,8 @@ export const Result: React.FC<{
</ResultItem> </ResultItem>
</Tooltip> </Tooltip>
</ResultContainer> </ResultContainer>
); )
}; }
// 1. getDerivedStateFromError // 1. getDerivedStateFromError
// 2. render() // 2. render()
@ -247,23 +247,23 @@ class ErrorBoundary extends React.Component<
{ hasError: boolean; message: string; eventId?: string } { hasError: boolean; message: string; eventId?: string }
> { > {
constructor(props: {}) { constructor(props: {}) {
super(props); super(props)
this.state = { hasError: false, message: '', eventId: undefined }; this.state = { hasError: false, message: '', eventId: undefined }
} }
// used in SSR // used in SSR
static getDerivedStateFromError(error: Error) { static getDerivedStateFromError(error: Error) {
return { hasError: true, message: error.message }; return { hasError: true, message: error.message }
} }
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
componentDidCatch(error: Error, errorInfo: any) { componentDidCatch(error: Error, errorInfo: any) {
if (error instanceof APIError || error instanceof NotFoundError) { if (error instanceof APIError || error instanceof NotFoundError) {
return; return
} }
sendError(error, errorInfo).then((eventId) => { sendError(error, errorInfo).then((eventId) => {
this.setState({ eventId }); this.setState({ eventId })
}); })
} }
render() { render() {
@ -285,9 +285,9 @@ class ErrorBoundary extends React.Component<
</ResultItem> </ResultItem>
</Tooltip> </Tooltip>
</ResultContainer> </ResultContainer>
); )
} }
return this.props.children; return this.props.children
} }
} }
@ -303,7 +303,7 @@ const ErrorHandler: React.FC = ({ children }) => (
{children} {children}
</Suspense> </Suspense>
</ErrorBoundary> </ErrorBoundary>
); )
const CardContainer = styled.div` const CardContainer = styled.div`
padding: 40px; padding: 40px;
@ -314,7 +314,7 @@ const CardContainer = styled.div`
margin-bottom: 40px; margin-bottom: 40px;
padding: 0px; padding: 0px;
} }
`; `
const CardTitle = styled.div` const CardTitle = styled.div`
margin-bottom: 15px; margin-bottom: 15px;
@ -327,7 +327,7 @@ const CardTitle = styled.div`
font-size: 1.2rem; font-size: 1.2rem;
font-weight: 600; font-weight: 600;
} }
`; `
const CardContent = styled.div` const CardContent = styled.div`
border-radius: 2px; border-radius: 2px;
@ -339,7 +339,7 @@ const CardContent = styled.div`
border-radius: 0; border-radius: 0;
font-size: 1.2em; font-size: 1.2em;
} }
`; `
const Button = styled.div` const Button = styled.div`
margin-top: 5px; margin-top: 5px;
@ -350,17 +350,17 @@ const Button = styled.div`
cursor: pointer; cursor: pointer;
font-family: monospace; font-family: monospace;
font-size: 0.8em; font-size: 0.8em;
`; `
const ResultContainer = styled.div` const ResultContainer = styled.div`
display: flex; display: flex;
align-items: center; align-items: center;
padding: 4px 0; padding: 4px 0;
`; `
export const ResultIcon = styled.div` export const ResultIcon = styled.div`
width: 1em; width: 1em;
`; `
export const ResultItem = styled.div` export const ResultItem = styled.div`
display: flex; display: flex;
@ -368,7 +368,7 @@ export const ResultItem = styled.div`
align-items: flex-start; align-items: flex-start;
word-break: break-all; word-break: break-all;
color: ${({ color }) => color}; color: ${({ color }) => color};
`; `
export const ResultName = styled.div` export const ResultName = styled.div`
margin-left: 6px; margin-left: 6px;
@ -378,7 +378,7 @@ export const ResultName = styled.div`
text-decoration: none; text-decoration: none;
color: inherit; color: inherit;
} }
`; `
export const AvailableIcon = styled.div` export const AvailableIcon = styled.div`
margin-top: 2px; margin-top: 2px;
@ -388,4 +388,4 @@ export const AvailableIcon = styled.div`
text-align: center; text-align: center;
font-size: 13px; font-size: 13px;
height: 15px; height: 15px;
`; `

View File

@ -1,35 +1,35 @@
import React from 'react'; import React from 'react'
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next'
import styled from 'styled-components'; import styled from 'styled-components'
import { mobile } from '../../util/css'; import { mobile } from '../../util/css'
import AppStoreCard from './providers/AppStore'; import AppStoreCard from './providers/AppStore'
import CratesioCard from './providers/Cratesio'; import CratesioCard from './providers/Cratesio'
import DomainCard from './providers/Domains'; import DomainCard from './providers/Domains'
import FirebaseCard from './providers/Firebase'; import FirebaseCard from './providers/Firebase'
import GithubCard from './providers/GitHubRepository'; import GithubCard from './providers/GitHubRepository'
import GithubSearchCard from './providers/GitHubSearch'; import GithubSearchCard from './providers/GitHubSearch'
import GitLabCard from './providers/GitLab'; import GitLabCard from './providers/GitLab'
import HerokuCard from './providers/Heroku'; import HerokuCard from './providers/Heroku'
import HomebrewCard from './providers/Homebrew'; import HomebrewCard from './providers/Homebrew'
import InstagramCard from './providers/Instagram'; import InstagramCard from './providers/Instagram'
import JsOrgCard from './providers/JsOrg'; import JsOrgCard from './providers/JsOrg'
import LinuxCard from './providers/Linux'; import LinuxCard from './providers/Linux'
import NetlifyCard from './providers/Netlify'; import NetlifyCard from './providers/Netlify'
import NpmCard from './providers/Npm'; import NpmCard from './providers/Npm'
import NtaCard from './providers/Nta'; import NtaCard from './providers/Nta'
import OcamlCard from './providers/Ocaml'; import OcamlCard from './providers/Ocaml'
import PypiCard from './providers/PyPI'; import PypiCard from './providers/PyPI'
import RubyGemsCard from './providers/RubyGems'; import RubyGemsCard from './providers/RubyGems'
import S3Card from './providers/S3'; import S3Card from './providers/S3'
import SlackCard from './providers/Slack'; import SlackCard from './providers/Slack'
import SpectrumCard from './providers/Spectrum'; import SpectrumCard from './providers/Spectrum'
import TwitterCard from './providers/Twitter'; import TwitterCard from './providers/Twitter'
import VercelCard from './providers/Vercel'; import VercelCard from './providers/Vercel'
const Index: React.FC<{ query: string }> = ({ query }) => { const Index: React.FC<{ query: string }> = ({ query }) => {
const { const {
i18n: { language }, i18n: { language },
} = useTranslation(); } = useTranslation()
return ( return (
<> <>
@ -61,10 +61,10 @@ const Index: React.FC<{ query: string }> = ({ query }) => {
{language === 'ja' ? <NtaCard query={query} /> : null} {language === 'ja' ? <NtaCard query={query} /> : null}
</Cards> </Cards>
</> </>
); )
}; }
export default Index; export default Index
const Cards = styled.div` const Cards = styled.div`
display: flex; display: flex;
@ -75,4 +75,4 @@ const Cards = styled.div`
${mobile} { ${mobile} {
flex-direction: column; flex-direction: column;
} }
`; `

View File

@ -1,18 +1,18 @@
import useFetch from 'fetch-suspense'; import useFetch from 'fetch-suspense'
import React from 'react'; import React from 'react'
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next'
import { FaAppStore, FaInfoCircle } from 'react-icons/fa'; import { FaAppStore, FaInfoCircle } from 'react-icons/fa'
import { Card, Result } from '../core'; import { Card, Result } from '../core'
const Search: React.FC<{ query: string }> = ({ query }) => { const Search: React.FC<{ query: string }> = ({ query }) => {
const { t } = useTranslation(); const { t } = useTranslation()
const term = encodeURIComponent(query); const term = encodeURIComponent(query)
const response = useFetch( const response = useFetch(
`/availability/appstore/${term}?country=${t('countryCode')}`, `/availability/appstore/${term}?country=${t('countryCode')}`
) as { ) as {
result: Array<{ name: string; viewURL: string; price: number; id: string }>; result: Array<{ name: string; viewURL: string; price: number; id: string }>
}; }
const apps = response.result; const apps = response.result
return ( return (
<> <>
@ -30,17 +30,17 @@ const Search: React.FC<{ query: string }> = ({ query }) => {
<Result title={t('noResult')} icon={<FaInfoCircle />} /> <Result title={t('noResult')} icon={<FaInfoCircle />} />
)} )}
</> </>
); )
}; }
const AppStoreCard: React.FC<{ query: string }> = ({ query }) => { const AppStoreCard: React.FC<{ query: string }> = ({ query }) => {
const { t } = useTranslation(); const { t } = useTranslation()
return ( return (
<Card title={t('providers.appStore')}> <Card title={t('providers.appStore')}>
<Search query={query} /> <Search query={query} />
</Card> </Card>
); )
}; }
export default AppStoreCard; export default AppStoreCard

View File

@ -1,14 +1,13 @@
import React from 'react'; import React from 'react'
import {useTranslation} from 'react-i18next'; import { useTranslation } from 'react-i18next'
import {DiRust} from 'react-icons/di'; import { DiRust } from 'react-icons/di'
import { Card, DedicatedAvailability, Repeater } from '../core'
import {Card, Repeater, DedicatedAvailability} from '../core'; const CratesioCard: React.FC<{ query: string }> = ({ query }) => {
const { t } = useTranslation()
const lowerCase = query.toLowerCase()
const CratesioCard: React.FC<{query: string}> = ({query}) => { const names = [lowerCase]
const {t} = useTranslation();
const lowerCase = query.toLowerCase();
const names = [lowerCase];
return ( return (
<Card title={t('providers.rust')}> <Card title={t('providers.rust')}>
@ -25,7 +24,7 @@ const CratesioCard: React.FC<{query: string}> = ({query}) => {
)} )}
</Repeater> </Repeater>
</Card> </Card>
); )
}; }
export default CratesioCard; export default CratesioCard

View File

@ -1,17 +1,15 @@
import React from 'react'; import React from 'react'
import {useTranslation} from 'react-i18next'; import { useTranslation } from 'react-i18next'
import {MdDomain} from 'react-icons/md'; import { MdDomain } from 'react-icons/md'
import {Card, Repeater, DedicatedAvailability} from '../core'; import { Card, Repeater, DedicatedAvailability } from '../core'
import {zones} from '../../../util/zones'; import { zones } from '../../../util/zones'
const DomainCard: React.FC<{query: string}> = ({query}) => { const DomainCard: React.FC<{ query: string }> = ({ query }) => {
const {t} = useTranslation(); const { t } = useTranslation()
const sanitizedQuery = query const sanitizedQuery = query.replace(/[^0-9a-zA-Z_-]/g, '').replace(/_/g, '-')
.replace(/[^0-9a-zA-Z_-]/g, '') const lowerCase = sanitizedQuery.toLowerCase()
.replace(/_/g, '-');
const lowerCase = sanitizedQuery.toLowerCase();
const domainHackSuggestions = zones const domainHackSuggestions = zones
.map((zone) => new RegExp(`${zone}$`).exec(lowerCase.slice(1))) .map((zone) => new RegExp(`${zone}$`).exec(lowerCase.slice(1)))
@ -20,8 +18,8 @@ const DomainCard: React.FC<{query: string}> = ({query}) => {
(m) => (m) =>
lowerCase.substring(0, m.index + 1) + lowerCase.substring(0, m.index + 1) +
'.' + '.' +
lowerCase.substring(m.index + 1), lowerCase.substring(m.index + 1)
); )
const names = [ const names = [
`${lowerCase}.com`, `${lowerCase}.com`,
@ -31,7 +29,7 @@ const DomainCard: React.FC<{query: string}> = ({query}) => {
`${lowerCase}.org`, `${lowerCase}.org`,
`${lowerCase}.io`, `${lowerCase}.io`,
...domainHackSuggestions, ...domainHackSuggestions,
]; ]
const moreNames = [ const moreNames = [
`${lowerCase}.sh`, `${lowerCase}.sh`,
`${lowerCase}.tools`, `${lowerCase}.tools`,
@ -44,7 +42,7 @@ const DomainCard: React.FC<{query: string}> = ({query}) => {
`${lowerCase}.info`, `${lowerCase}.info`,
`${lowerCase}.biz`, `${lowerCase}.biz`,
`${lowerCase}.website`, `${lowerCase}.website`,
]; ]
return ( return (
<Card title={t('providers.domains')}> <Card title={t('providers.domains')}>
@ -60,7 +58,7 @@ const DomainCard: React.FC<{query: string}> = ({query}) => {
)} )}
</Repeater> </Repeater>
</Card> </Card>
); )
}; }
export default DomainCard; export default DomainCard

View File

@ -1,18 +1,16 @@
import React from 'react'; import React from 'react'
import {useTranslation} from 'react-i18next'; import { useTranslation } from 'react-i18next'
import {DiFirebase} from 'react-icons/di'; import { DiFirebase } from 'react-icons/di'
import {Card, Repeater, DedicatedAvailability} from '../core'; import { Card, Repeater, DedicatedAvailability } from '../core'
const FirebaseCard: React.FC<{query: string}> = ({query}) => { const FirebaseCard: React.FC<{ query: string }> = ({ query }) => {
const {t} = useTranslation(); const { t } = useTranslation()
const sanitizedQuery = query const sanitizedQuery = query.replace(/[^0-9a-zA-Z_-]/g, '').replace(/_/g, '-')
.replace(/[^0-9a-zA-Z_-]/g, '') const lowerCase = sanitizedQuery.toLowerCase()
.replace(/_/g, '-');
const lowerCase = sanitizedQuery.toLowerCase();
const names = [lowerCase]; const names = [lowerCase]
return ( return (
<Card title={t('providers.firebase')}> <Card title={t('providers.firebase')}>
@ -29,7 +27,7 @@ const FirebaseCard: React.FC<{query: string}> = ({query}) => {
)} )}
</Repeater> </Repeater>
</Card> </Card>
); )
}; }
export default FirebaseCard; export default FirebaseCard

View File

@ -1,12 +1,11 @@
import React from 'react'; import React from 'react'
import {useTranslation} from 'react-i18next'; import { useTranslation } from 'react-i18next'
import {FaGithub} from 'react-icons/fa'; import { FaGithub } from 'react-icons/fa'
import { Card, DedicatedAvailability, Repeater } from '../core'
import {Card, Repeater, DedicatedAvailability} from '../core';
const GithubCard: React.FC<{ query: string }> = ({ query }) => { const GithubCard: React.FC<{ query: string }> = ({ query }) => {
const {t} = useTranslation(); const { t } = useTranslation()
const lowerCase = query.toLowerCase(); const lowerCase = query.toLowerCase()
const names = [query, `${lowerCase}-dev`, `${lowerCase}-org`] const names = [query, `${lowerCase}-dev`, `${lowerCase}-org`]
const moreNames = [ const moreNames = [
@ -32,7 +31,7 @@ const GithubCard: React.FC<{ query: string }> = ({ query }) => {
)} )}
</Repeater> </Repeater>
</Card> </Card>
); )
}; }
export default GithubCard; export default GithubCard

View File

@ -1,26 +1,26 @@
import React from 'react'; import React from 'react'
import useFetch from 'fetch-suspense'; import useFetch from 'fetch-suspense'
import {useTranslation} from 'react-i18next'; import { useTranslation } from 'react-i18next'
import {FaGithub, FaInfoCircle} from 'react-icons/fa'; import { FaGithub, FaInfoCircle } from 'react-icons/fa'
import {Card, Result} from '../core'; import { Card, Result } from '../core'
const Search: React.FC<{query: string}> = ({query}) => { const Search: React.FC<{ query: string }> = ({ query }) => {
const {t} = useTranslation(); const { t } = useTranslation()
const searchQuery = encodeURIComponent(`${query} in:name`); const searchQuery = encodeURIComponent(`${query} in:name`)
const limit = 10; const limit = 10
const response = useFetch( const response = useFetch(
`https://api.github.com/search/repositories?q=${searchQuery}&per_page=${limit}`, `https://api.github.com/search/repositories?q=${searchQuery}&per_page=${limit}`
) as { ) as {
items: Array<{ items: Array<{
full_name: string; full_name: string
description: string; description: string
stargazers_count: number; stargazers_count: number
html_url: string; html_url: string
id: string; id: string
}>; }>
}; }
const repos = response.items || []; const repos = response.items || []
return ( return (
<> <>
@ -40,17 +40,17 @@ const Search: React.FC<{query: string}> = ({query}) => {
<Result title={t('noResult')} icon={<FaInfoCircle />} /> <Result title={t('noResult')} icon={<FaInfoCircle />} />
)} )}
</> </>
); )
}; }
const GithubSearchCard: React.FC<{query: string}> = ({query}) => { const GithubSearchCard: React.FC<{ query: string }> = ({ query }) => {
const {t} = useTranslation(); const { t } = useTranslation()
return ( return (
<Card title={t('providers.githubSearch')}> <Card title={t('providers.githubSearch')}>
<Search query={query} /> <Search query={query} />
</Card> </Card>
); )
}; }
export default GithubSearchCard; export default GithubSearchCard

View File

@ -1,14 +1,14 @@
import React from 'react'; import React from 'react'
import {useTranslation} from 'react-i18next'; import { useTranslation } from 'react-i18next'
import {FaGitlab} from 'react-icons/fa'; import { FaGitlab } from 'react-icons/fa'
import {Card, Repeater, DedicatedAvailability} from '../core'; import { Card, Repeater, DedicatedAvailability } from '../core'
const GitLabCard: React.FC<{query: string}> = ({query}) => { const GitLabCard: React.FC<{ query: string }> = ({ query }) => {
const {t} = useTranslation(); const { t } = useTranslation()
const lowerCase = query.toLowerCase(); const lowerCase = query.toLowerCase()
const names = [lowerCase]; const names = [lowerCase]
return ( return (
<Card title={t('providers.gitlab')}> <Card title={t('providers.gitlab')}>
@ -25,7 +25,7 @@ const GitLabCard: React.FC<{query: string}> = ({query}) => {
)} )}
</Repeater> </Repeater>
</Card> </Card>
); )
}; }
export default GitLabCard; export default GitLabCard

View File

@ -1,18 +1,16 @@
import React from 'react'; import React from 'react'
import {useTranslation} from 'react-i18next'; import { useTranslation } from 'react-i18next'
import {DiHeroku} from 'react-icons/di'; import { DiHeroku } from 'react-icons/di'
import {Card, Repeater, DedicatedAvailability} from '../core'; import { Card, Repeater, DedicatedAvailability } from '../core'
const HerokuCard: React.FC<{query: string}> = ({query}) => { const HerokuCard: React.FC<{ query: string }> = ({ query }) => {
const {t} = useTranslation(); const { t } = useTranslation()
const sanitizedQuery = query const sanitizedQuery = query.replace(/[^0-9a-zA-Z_-]/g, '').replace(/_/g, '-')
.replace(/[^0-9a-zA-Z_-]/g, '') const lowerCase = sanitizedQuery.toLowerCase()
.replace(/_/g, '-');
const lowerCase = sanitizedQuery.toLowerCase();
const names = [lowerCase]; const names = [lowerCase]
return ( return (
<Card title={t('providers.heroku')}> <Card title={t('providers.heroku')}>
@ -28,7 +26,7 @@ const HerokuCard: React.FC<{query: string}> = ({query}) => {
)} )}
</Repeater> </Repeater>
</Card> </Card>
); )
}; }
export default HerokuCard; export default HerokuCard

View File

@ -1,14 +1,14 @@
import React from 'react'; import React from 'react'
import {useTranslation} from 'react-i18next'; import { useTranslation } from 'react-i18next'
import {IoIosBeer} from 'react-icons/io'; import { IoIosBeer } from 'react-icons/io'
import {Card, Repeater, ExistentialAvailability} from '../core'; import { Card, Repeater, ExistentialAvailability } from '../core'
const HomebrewCard: React.FC<{query: string}> = ({query}) => { const HomebrewCard: React.FC<{ query: string }> = ({ query }) => {
const {t} = useTranslation(); const { t } = useTranslation()
const lowerCase = query.toLowerCase(); const lowerCase = query.toLowerCase()
const names = [lowerCase]; const names = [lowerCase]
return ( return (
<Card title={t('providers.homebrew')}> <Card title={t('providers.homebrew')}>
@ -34,7 +34,7 @@ const HomebrewCard: React.FC<{query: string}> = ({query}) => {
)} )}
</Repeater> </Repeater>
</Card> </Card>
); )
}; }
export default HomebrewCard; export default HomebrewCard

View File

@ -1,15 +1,15 @@
import React from 'react'; import React from 'react'
import {useTranslation} from 'react-i18next'; import { useTranslation } from 'react-i18next'
import {FaInstagram} from 'react-icons/fa'; import { FaInstagram } from 'react-icons/fa'
import {Card, Repeater, ExistentialAvailability} from '../core'; import { Card, Repeater, ExistentialAvailability } from '../core'
const InstagramCard: React.FC<{query: string}> = ({query}) => { const InstagramCard: React.FC<{ query: string }> = ({ query }) => {
const {t} = useTranslation(); const { t } = useTranslation()
const lowerCase = query.toLowerCase(); const lowerCase = query.toLowerCase()
const names = [query]; const names = [query]
const moreNames = [`${lowerCase}app`, `${lowerCase}_hq`, `get.${lowerCase}`]; const moreNames = [`${lowerCase}app`, `${lowerCase}_hq`, `get.${lowerCase}`]
return ( return (
<Card title={t('providers.instagram')}> <Card title={t('providers.instagram')}>
@ -25,7 +25,7 @@ const InstagramCard: React.FC<{query: string}> = ({query}) => {
)} )}
</Repeater> </Repeater>
</Card> </Card>
); )
}; }
export default InstagramCard; export default InstagramCard

View File

@ -1,18 +1,16 @@
import React from 'react'; import React from 'react'
import {useTranslation} from 'react-i18next'; import { useTranslation } from 'react-i18next'
import {FaJsSquare} from 'react-icons/fa'; import { FaJsSquare } from 'react-icons/fa'
import {Card, Repeater, DedicatedAvailability} from '../core'; import { Card, Repeater, DedicatedAvailability } from '../core'
const JsOrgCard: React.FC<{query: string}> = ({query}) => { const JsOrgCard: React.FC<{ query: string }> = ({ query }) => {
const {t} = useTranslation(); const { t } = useTranslation()
const sanitizedQuery = query const sanitizedQuery = query.replace(/[^0-9a-zA-Z_-]/g, '').replace(/_/g, '-')
.replace(/[^0-9a-zA-Z_-]/g, '') const lowerCase = sanitizedQuery.toLowerCase()
.replace(/_/g, '-');
const lowerCase = sanitizedQuery.toLowerCase();
const names = [lowerCase]; const names = [lowerCase]
return ( return (
<Card title={t('providers.jsorg')}> <Card title={t('providers.jsorg')}>
@ -30,7 +28,7 @@ const JsOrgCard: React.FC<{query: string}> = ({query}) => {
)} )}
</Repeater> </Repeater>
</Card> </Card>
); )
}; }
export default JsOrgCard; export default JsOrgCard

View File

@ -1,15 +1,15 @@
import React from 'react'; import React from 'react'
import {useTranslation} from 'react-i18next'; import { useTranslation } from 'react-i18next'
import {DiUbuntu} from 'react-icons/di'; import { DiUbuntu } from 'react-icons/di'
import {DiDebian} from 'react-icons/di'; import { DiDebian } from 'react-icons/di'
import {Card, Repeater, DedicatedAvailability} from '../core'; import { Card, Repeater, DedicatedAvailability } from '../core'
const LinuxCard: React.FC<{query: string}> = ({query}) => { const LinuxCard: React.FC<{ query: string }> = ({ query }) => {
const {t} = useTranslation(); const { t } = useTranslation()
const lowerCase = query.toLowerCase(); const lowerCase = query.toLowerCase()
const names = [lowerCase]; const names = [lowerCase]
return ( return (
<Card title={t('providers.linux')}> <Card title={t('providers.linux')}>
@ -34,7 +34,7 @@ const LinuxCard: React.FC<{query: string}> = ({query}) => {
)} )}
</Repeater> </Repeater>
</Card> </Card>
); )
}; }
export default LinuxCard; export default LinuxCard

View File

@ -1,18 +1,16 @@
import React from 'react'; import React from 'react'
import {useTranslation} from 'react-i18next'; import { useTranslation } from 'react-i18next'
import {NetlifyIcon} from '../../Icons'; import { NetlifyIcon } from '../../Icons'
import {Card, Repeater, DedicatedAvailability} from '../core'; import { Card, Repeater, DedicatedAvailability } from '../core'
const NetlifyCard: React.FC<{query: string}> = ({query}) => { const NetlifyCard: React.FC<{ query: string }> = ({ query }) => {
const {t} = useTranslation(); const { t } = useTranslation()
const sanitizedQuery = query const sanitizedQuery = query.replace(/[^0-9a-zA-Z_-]/g, '').replace(/_/g, '-')
.replace(/[^0-9a-zA-Z_-]/g, '') const lowerCase = sanitizedQuery.toLowerCase()
.replace(/_/g, '-');
const lowerCase = sanitizedQuery.toLowerCase();
const names = [lowerCase]; const names = [lowerCase]
return ( return (
<Card title={t('providers.netlify')}> <Card title={t('providers.netlify')}>
@ -28,7 +26,7 @@ const NetlifyCard: React.FC<{query: string}> = ({query}) => {
)} )}
</Repeater> </Repeater>
</Card> </Card>
); )
}; }
export default NetlifyCard; export default NetlifyCard

View File

@ -1,15 +1,15 @@
import React from 'react'; import React from 'react'
import {useTranslation} from 'react-i18next'; import { useTranslation } from 'react-i18next'
import {FaNpm} from 'react-icons/fa'; import { FaNpm } from 'react-icons/fa'
import {Card, Repeater, DedicatedAvailability} from '../core'; import { Card, Repeater, DedicatedAvailability } from '../core'
const NpmCard: React.FC<{query: string}> = ({query}) => { const NpmCard: React.FC<{ query: string }> = ({ query }) => {
const {t} = useTranslation(); const { t } = useTranslation()
const lowerCase = query.toLowerCase(); const lowerCase = query.toLowerCase()
const names = [lowerCase, `${lowerCase}-js`]; const names = [lowerCase, `${lowerCase}-js`]
const moreNames = [`${lowerCase}js`]; const moreNames = [`${lowerCase}js`]
return ( return (
<Card title={t('providers.npm')}> <Card title={t('providers.npm')}>
@ -40,7 +40,7 @@ const NpmCard: React.FC<{query: string}> = ({query}) => {
)} )}
</Repeater> </Repeater>
</Card> </Card>
); )
}; }
export default NpmCard; export default NpmCard

View File

@ -1,18 +1,18 @@
import React from 'react'; import React from 'react'
import useFetch from 'fetch-suspense'; import useFetch from 'fetch-suspense'
import {useTranslation} from 'react-i18next'; import { useTranslation } from 'react-i18next'
import {FaBuilding, FaInfoCircle} from 'react-icons/fa'; import { FaBuilding, FaInfoCircle } from 'react-icons/fa'
import {Card, Result} from '../core'; import { Card, Result } from '../core'
const Search: React.FC<{query: string}> = ({query}) => { const Search: React.FC<{ query: string }> = ({ query }) => {
const {t} = useTranslation(); const { t } = useTranslation()
const term = encodeURIComponent(query); const term = encodeURIComponent(query)
const response = useFetch(`/api/services/nta/${term}`) as { const response = useFetch(`/api/services/nta/${term}`) as {
result: Array<{name: string; phoneticName: string}>; result: Array<{ name: string; phoneticName: string }>
}; }
const apps = response.result; const apps = response.result
return ( return (
<> <>
@ -29,17 +29,17 @@ const Search: React.FC<{query: string}> = ({query}) => {
<Result title={t('noResult')} icon={<FaInfoCircle />} /> <Result title={t('noResult')} icon={<FaInfoCircle />} />
)} )}
</> </>
); )
}; }
const NtaCard: React.FC<{query: string}> = ({query}) => { const NtaCard: React.FC<{ query: string }> = ({ query }) => {
const {t} = useTranslation(); const { t } = useTranslation()
return ( return (
<Card title={t('providers.nta')}> <Card title={t('providers.nta')}>
<Search query={query} /> <Search query={query} />
</Card> </Card>
); )
}; }
export default NtaCard; export default NtaCard

View File

@ -1,14 +1,14 @@
import React from 'react'; import React from 'react'
import {useTranslation} from 'react-i18next'; import { useTranslation } from 'react-i18next'
import {OcamlIcon} from '../../Icons'; import { OcamlIcon } from '../../Icons'
import {Card, Repeater, DedicatedAvailability} from '../core'; import { Card, Repeater, DedicatedAvailability } from '../core'
const OcamlCard: React.FC<{query: string}> = ({query}) => { const OcamlCard: React.FC<{ query: string }> = ({ query }) => {
const {t} = useTranslation(); const { t } = useTranslation()
const lowerCase = query.toLowerCase(); const lowerCase = query.toLowerCase()
const names = [lowerCase]; const names = [lowerCase]
return ( return (
<Card title={t('providers.ocaml')}> <Card title={t('providers.ocaml')}>
@ -25,7 +25,7 @@ const OcamlCard: React.FC<{query: string}> = ({query}) => {
)} )}
</Repeater> </Repeater>
</Card> </Card>
); )
}; }
export default OcamlCard; export default OcamlCard

View File

@ -1,15 +1,15 @@
import React from 'react'; import React from 'react'
import {useTranslation} from 'react-i18next'; import { useTranslation } from 'react-i18next'
import {FaPython} from 'react-icons/fa'; import { FaPython } from 'react-icons/fa'
import {capitalize} from '../../../util/text'; import { capitalize } from '../../../util/text'
import {Card, DedicatedAvailability, Repeater} from '../core'; import { Card, DedicatedAvailability, Repeater } from '../core'
const PypiCard: React.FC<{query: string}> = ({query}) => { const PypiCard: React.FC<{ query: string }> = ({ query }) => {
const {t} = useTranslation(); const { t } = useTranslation()
const names = [query]; const names = [query]
const moreNames = [`Py${capitalize(query)}`]; const moreNames = [`Py${capitalize(query)}`]
return ( return (
<Card title={t('providers.pypi')}> <Card title={t('providers.pypi')}>
@ -28,7 +28,7 @@ const PypiCard: React.FC<{query: string}> = ({query}) => {
)} )}
</Repeater> </Repeater>
</Card> </Card>
); )
}; }
export default PypiCard; export default PypiCard

View File

@ -1,14 +1,14 @@
import React from 'react'; import React from 'react'
import {useTranslation} from 'react-i18next'; import { useTranslation } from 'react-i18next'
import {FaGem} from 'react-icons/fa'; import { FaGem } from 'react-icons/fa'
import {Card, Repeater, DedicatedAvailability} from '../core'; import { Card, Repeater, DedicatedAvailability } from '../core'
const RubyGemsCard: React.FC<{query: string}> = ({query}) => { const RubyGemsCard: React.FC<{ query: string }> = ({ query }) => {
const {t} = useTranslation(); const { t } = useTranslation()
const names = [query]; const names = [query]
const moreNames = [`${query.toLowerCase()}-rb`]; const moreNames = [`${query.toLowerCase()}-rb`]
return ( return (
<Card title={t('providers.rubygems')}> <Card title={t('providers.rubygems')}>
@ -25,7 +25,7 @@ const RubyGemsCard: React.FC<{query: string}> = ({query}) => {
)} )}
</Repeater> </Repeater>
</Card> </Card>
); )
}; }
export default RubyGemsCard; export default RubyGemsCard

View File

@ -1,18 +1,16 @@
import React from 'react'; import React from 'react'
import {useTranslation} from 'react-i18next'; import { useTranslation } from 'react-i18next'
import {FaAws} from 'react-icons/fa'; import { FaAws } from 'react-icons/fa'
import {Card, DedicatedAvailability, Repeater} from '../core'; import { Card, DedicatedAvailability, Repeater } from '../core'
const S3Card: React.FC<{query: string}> = ({query}) => { const S3Card: React.FC<{ query: string }> = ({ query }) => {
const {t} = useTranslation(); const { t } = useTranslation()
const sanitizedQuery = query const sanitizedQuery = query.replace(/[^0-9a-zA-Z_-]/g, '').replace(/_/g, '-')
.replace(/[^0-9a-zA-Z_-]/g, '') const lowerCase = sanitizedQuery.toLowerCase()
.replace(/_/g, '-');
const lowerCase = sanitizedQuery.toLowerCase();
const names = [lowerCase]; const names = [lowerCase]
return ( return (
<Card title={t('providers.s3')}> <Card title={t('providers.s3')}>
@ -30,7 +28,7 @@ const S3Card: React.FC<{query: string}> = ({query}) => {
)} )}
</Repeater> </Repeater>
</Card> </Card>
); )
}; }
export default S3Card; export default S3Card

View File

@ -1,18 +1,16 @@
import React from 'react'; import React from 'react'
import {useTranslation} from 'react-i18next'; import { useTranslation } from 'react-i18next'
import {FaSlack} from 'react-icons/fa'; import { FaSlack } from 'react-icons/fa'
import {Card, DedicatedAvailability, Repeater} from '../core'; import { Card, DedicatedAvailability, Repeater } from '../core'
const SlackCard: React.FC<{query: string}> = ({query}) => { const SlackCard: React.FC<{ query: string }> = ({ query }) => {
const {t} = useTranslation(); const { t } = useTranslation()
const sanitizedQuery = query const sanitizedQuery = query.replace(/[^0-9a-zA-Z_-]/g, '').replace(/_/g, '-')
.replace(/[^0-9a-zA-Z_-]/g, '') const lowerCase = sanitizedQuery.toLowerCase()
.replace(/_/g, '-');
const lowerCase = sanitizedQuery.toLowerCase();
const names = [lowerCase]; const names = [lowerCase]
return ( return (
<Card title={t('providers.slack')}> <Card title={t('providers.slack')}>
@ -31,7 +29,7 @@ const SlackCard: React.FC<{query: string}> = ({query}) => {
)} )}
</Repeater> </Repeater>
</Card> </Card>
); )
}; }
export default SlackCard; export default SlackCard

View File

@ -1,11 +1,11 @@
import React from 'react'; import React from 'react'
import {useTranslation} from 'react-i18next'; import { useTranslation } from 'react-i18next'
import {Card, Repeater, DedicatedAvailability} from '../core'; import { Card, Repeater, DedicatedAvailability } from '../core'
import {SpectrumIcon} from '../../Icons'; import { SpectrumIcon } from '../../Icons'
const SpectrumCard: React.FC<{query: string}> = ({query}) => { const SpectrumCard: React.FC<{ query: string }> = ({ query }) => {
const {t} = useTranslation(); const { t } = useTranslation()
const names = [query]; const names = [query]
return ( return (
<Card title={t('providers.spectrum')}> <Card title={t('providers.spectrum')}>
@ -25,7 +25,7 @@ const SpectrumCard: React.FC<{query: string}> = ({query}) => {
)} )}
</Repeater> </Repeater>
</Card> </Card>
); )
}; }
export default SpectrumCard; export default SpectrumCard

View File

@ -1,20 +1,18 @@
import React from 'react'; import React from 'react'
import {useTranslation} from 'react-i18next'; import { useTranslation } from 'react-i18next'
import {FaTwitter} from 'react-icons/fa'; import { FaTwitter } from 'react-icons/fa'
import {capitalize} from '../../../util/text'; import { capitalize } from '../../../util/text'
import {Card, Repeater, DedicatedAvailability} from '../core'; import { Card, Repeater, DedicatedAvailability } from '../core'
const TwitterCard: React.FC<{query: string}> = ({query}) => { const TwitterCard: React.FC<{ query: string }> = ({ query }) => {
const {t} = useTranslation(); const { t } = useTranslation()
const sanitizedQuery = query const sanitizedQuery = query.replace(/[^0-9a-zA-Z_-]/g, '').replace(/-/g, '_')
.replace(/[^0-9a-zA-Z_-]/g, '') const lowerCase = sanitizedQuery.toLowerCase()
.replace(/-/g, '_'); const capitalCase = capitalize(sanitizedQuery)
const lowerCase = sanitizedQuery.toLowerCase();
const capitalCase = capitalize(sanitizedQuery);
const names = [sanitizedQuery, `${capitalCase}App`, `${lowerCase}hq`]; const names = [sanitizedQuery, `${capitalCase}App`, `${lowerCase}hq`]
const moreNames = [ const moreNames = [
`hey${lowerCase}`, `hey${lowerCase}`,
`${capitalCase}Team`, `${capitalCase}Team`,
@ -22,7 +20,7 @@ const TwitterCard: React.FC<{query: string}> = ({query}) => {
`${lowerCase}_org`, `${lowerCase}_org`,
`${lowerCase}_app`, `${lowerCase}_app`,
`${capitalCase}JS`, `${capitalCase}JS`,
]; ]
return ( return (
<Card title={t('providers.twitter')}> <Card title={t('providers.twitter')}>
@ -39,7 +37,7 @@ const TwitterCard: React.FC<{query: string}> = ({query}) => {
)} )}
</Repeater> </Repeater>
</Card> </Card>
); )
}; }
export default TwitterCard; export default TwitterCard

View File

@ -1,18 +1,16 @@
import React from 'react'; import React from 'react'
import {useTranslation} from 'react-i18next'; import { useTranslation } from 'react-i18next'
import {NowIcon} from '../../Icons'; import { NowIcon } from '../../Icons'
import {Card, Repeater, DedicatedAvailability} from '../core'; import { Card, Repeater, DedicatedAvailability } from '../core'
const VercelCard: React.FC<{query: string}> = ({query}) => { const VercelCard: React.FC<{ query: string }> = ({ query }) => {
const {t} = useTranslation(); const { t } = useTranslation()
const sanitizedQuery = query const sanitizedQuery = query.replace(/[^0-9a-zA-Z_-]/g, '').replace(/_/g, '-')
.replace(/[^0-9a-zA-Z_-]/g, '') const lowerCase = sanitizedQuery.toLowerCase()
.replace(/_/g, '-');
const lowerCase = sanitizedQuery.toLowerCase();
const names = [lowerCase]; const names = [lowerCase]
return ( return (
<Card title={t('providers.now')}> <Card title={t('providers.now')}>
@ -39,7 +37,7 @@ const VercelCard: React.FC<{query: string}> = ({query}) => {
)} )}
</Repeater> </Repeater>
</Card> </Card>
); )
}; }
export default VercelCard; export default VercelCard

View File

@ -1,28 +1,28 @@
import { StoreProvider } from 'easy-peasy'; import { StoreProvider } from 'easy-peasy'
import { createBrowserHistory } from 'history'; import { createBrowserHistory } from 'history'
import 'rc-tooltip/assets/bootstrap.css'; import 'rc-tooltip/assets/bootstrap.css'
import React from 'react'; import React from 'react'
import ReactDOM from 'react-dom'; import ReactDOM from 'react-dom'
import { Router } from 'react-router-dom'; import { Router } from 'react-router-dom'
import { toast, ToastContainer } from 'react-toastify'; import { toast, ToastContainer } from 'react-toastify'
import 'react-toastify/dist/ReactToastify.css'; import 'react-toastify/dist/ReactToastify.css'
import App from './App'; import App from './App'
import * as serviceWorker from './serviceWorker'; import * as serviceWorker from './serviceWorker'
import { store, wrapHistoryWithStoreHandler } from './store'; import { store, wrapHistoryWithStoreHandler } from './store'
import { initSentry, wrapHistoryWithGA } from './util/analytics'; import { initSentry, wrapHistoryWithGA } from './util/analytics'
import { compose } from './util/array'; import { compose } from './util/array'
import { initCrisp } from './util/crisp'; import { initCrisp } from './util/crisp'
import './util/i18n'; import './util/i18n'
import { FullScreenSuspense } from './util/suspense'; import { FullScreenSuspense } from './util/suspense'
initSentry(); initSentry()
initCrisp(); initCrisp()
const history = compose( const history = compose(
createBrowserHistory(), createBrowserHistory(),
wrapHistoryWithStoreHandler, wrapHistoryWithStoreHandler,
wrapHistoryWithGA, wrapHistoryWithGA
); )
ReactDOM.render( ReactDOM.render(
<StoreProvider store={store}> <StoreProvider store={store}>
@ -33,25 +33,25 @@ ReactDOM.render(
</FullScreenSuspense> </FullScreenSuspense>
<ToastContainer /> <ToastContainer />
</StoreProvider>, </StoreProvider>,
document.getElementById('root'), document.getElementById('root')
); )
serviceWorker.register({ serviceWorker.register({
onUpdate: (registration) => { onUpdate: (registration) => {
console.log('Update available'); console.log('Update available')
toast.dark('New version available! Click here to update.', { toast.dark('New version available! Click here to update.', {
onClose: () => { onClose: () => {
window.location.reload(); window.location.reload()
}, },
position: 'top-right', position: 'top-right',
autoClose: false, autoClose: false,
closeButton: false, closeButton: false,
closeOnClick: true, closeOnClick: true,
}); })
if (registration && registration.waiting) { if (registration && registration.waiting) {
registration.waiting.postMessage({ type: 'SKIP_WAITING' }); registration.waiting.postMessage({ type: 'SKIP_WAITING' })
} }
}, },
}); })

View File

@ -1,11 +1,11 @@
import React from 'react'; import React from 'react'
import { Helmet } from 'react-helmet'; import { Helmet } from 'react-helmet'
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next'
import { Content, Header } from '../theme'; import { Content, Header } from '../theme'
import Form from '../components/Form'; import Form from '../components/Form'
import Welcome from '../components/Welcome'; import Welcome from '../components/Welcome'
export default function Home() { export default function Home() {
const { t } = useTranslation(); const { t } = useTranslation()
return ( return (
<> <>
@ -21,5 +21,5 @@ export default function Home() {
<Welcome /> <Welcome />
</Content> </Content>
</> </>
); )
} }

View File

@ -1,28 +1,28 @@
import Tooltip from 'rc-tooltip'; import Tooltip from 'rc-tooltip'
import React from 'react'; import React from 'react'
import { Helmet } from 'react-helmet'; import { Helmet } from 'react-helmet'
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next'
import { IoIosFlash, IoIosRocket } from 'react-icons/io'; import { IoIosFlash, IoIosRocket } from 'react-icons/io'
import { useParams } from 'react-router-dom'; import { useParams } from 'react-router-dom'
import styled from 'styled-components'; import styled from 'styled-components'
import Cards from '../components/cards'; import Cards from '../components/cards'
import { import {
AvailableIcon, AvailableIcon,
COLORS as ResultColor, COLORS as ResultColor,
ResultIcon, ResultIcon,
ResultItem, ResultItem,
ResultName, ResultName,
} from '../components/cards/core'; } from '../components/cards/core'
import Form from '../components/Form'; import Form from '../components/Form'
import { useStoreState } from '../store'; import { useStoreState } from '../store'
import { Content, Header } from '../theme'; import { Content, Header } from '../theme'
import { mobile } from '../util/css'; import { mobile } from '../util/css'
import { sanitize } from '../util/text'; import { sanitize } from '../util/text'
export default function Search() { export default function Search() {
const { query } = useParams<{ query: string }>(); const { query } = useParams<{ query: string }>()
const currentQuery = sanitize(query); const currentQuery = sanitize(query)
const { t } = useTranslation(); const { t } = useTranslation()
return ( return (
<> <>
@ -54,24 +54,24 @@ export default function Search() {
<Cards query={currentQuery} /> <Cards query={currentQuery} />
</Content> </Content>
</> </>
); )
} }
function Stat() { function Stat() {
const totalCount = useStoreState((state) => state.stats.totalCount); const totalCount = useStoreState((state) => state.stats.totalCount)
const availableCount = useStoreState((state) => state.stats.availableCount); const availableCount = useStoreState((state) => state.stats.availableCount)
const { t } = useTranslation(); const { t } = useTranslation()
const uniqueness = availableCount !== 0 ? availableCount / totalCount : 0.0; const uniqueness = availableCount !== 0 ? availableCount / totalCount : 0.0
const uniquenessText = ((n) => { const uniquenessText = ((n) => {
if (n > 0.7 && n <= 1.0) { if (n > 0.7 && n <= 1.0) {
return t('uniqueness.high'); return t('uniqueness.high')
} else if (n > 0.4 && n <= 0.7) { } else if (n > 0.4 && n <= 0.7) {
return t('uniqueness.moderate'); return t('uniqueness.moderate')
} else { } else {
return t('uniqueness.low'); return t('uniqueness.low')
} }
})(uniqueness); })(uniqueness)
return ( return (
<UniquenessIndicator> <UniquenessIndicator>
@ -85,7 +85,7 @@ function Stat() {
</span> </span>
</Tooltip> </Tooltip>
</UniquenessIndicator> </UniquenessIndicator>
); )
} }
export const Legend = styled.div` export const Legend = styled.div`
@ -109,8 +109,8 @@ export const Legend = styled.div`
> * { > * {
margin: 0 10px 0; margin: 0 10px 0;
} }
`; `
export const UniquenessIndicator = styled.div` export const UniquenessIndicator = styled.div`
color: #7b7b7b; color: #7b7b7b;
`; `

View File

@ -18,23 +18,23 @@ const isLocalhost = Boolean(
window.location.hostname === '[::1]' || window.location.hostname === '[::1]' ||
// 127.0.0.0/8 are considered localhost for IPv4. // 127.0.0.0/8 are considered localhost for IPv4.
/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/.exec( /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/.exec(
window.location.hostname, window.location.hostname
), )
); )
type Config = { type Config = {
onSuccess?: (registration: ServiceWorkerRegistration) => void; onSuccess?: (registration: ServiceWorkerRegistration) => void
onUpdate?: (registration: ServiceWorkerRegistration) => void; onUpdate?: (registration: ServiceWorkerRegistration) => void
}; }
function registerValidSW(swUrl: string, config?: Config) { function registerValidSW(swUrl: string, config?: Config) {
navigator.serviceWorker navigator.serviceWorker
.register(swUrl) .register(swUrl)
.then((registration) => { .then((registration) => {
registration.onupdatefound = () => { registration.onupdatefound = () => {
const installingWorker = registration.installing; const installingWorker = registration.installing
if (installingWorker == null) { if (installingWorker == null) {
return; return
} }
installingWorker.onstatechange = () => { installingWorker.onstatechange = () => {
if (installingWorker.state === 'installed') { if (installingWorker.state === 'installed') {
@ -44,41 +44,41 @@ function registerValidSW(swUrl: string, config?: Config) {
// content until all client tabs are closed. // content until all client tabs are closed.
console.log( console.log(
'New content is available and will be used when all ' + 'New content is available and will be used when all ' +
'tabs for this page are closed. See https://bit.ly/CRA-PWA.', 'tabs for this page are closed. See https://bit.ly/CRA-PWA.'
); )
// Execute callback // Execute callback
if (config && config.onUpdate) { if (config && config.onUpdate) {
config.onUpdate(registration); config.onUpdate(registration)
} }
} else { } else {
// At this point, everything has been precached. // At this point, everything has been precached.
// It's the perfect time to display a // It's the perfect time to display a
// "Content is cached for offline use." message. // "Content is cached for offline use." message.
console.log('Content is cached for offline use.'); console.log('Content is cached for offline use.')
// Execute callback // Execute callback
if (config && config.onSuccess) { if (config && config.onSuccess) {
config.onSuccess(registration); config.onSuccess(registration)
} }
} }
} }
}; }
}; }
}) })
.catch((error) => { .catch((error) => {
console.error('Error during service worker registration:', error); console.error('Error during service worker registration:', error)
}); })
} }
function checkValidServiceWorker(swUrl: string, config?: Config) { function checkValidServiceWorker(swUrl: string, config?: Config) {
// Check if the service worker can be found. If it can't reload the page. // Check if the service worker can be found. If it can't reload the page.
fetch(swUrl, { fetch(swUrl, {
headers: {'Service-Worker': 'script'}, headers: { 'Service-Worker': 'script' },
}) })
.then((response) => { .then((response) => {
// Ensure service worker exists, and that we really are getting a JS file. // Ensure service worker exists, and that we really are getting a JS file.
const contentType = response.headers.get('content-type'); const contentType = response.headers.get('content-type')
if ( if (
response.status === 404 || response.status === 404 ||
(contentType != null && !contentType.includes('javascript')) (contentType != null && !contentType.includes('javascript'))
@ -86,52 +86,52 @@ function checkValidServiceWorker(swUrl: string, config?: Config) {
// No service worker found. Probably a different app. Reload the page. // No service worker found. Probably a different app. Reload the page.
navigator.serviceWorker.ready.then((registration) => { navigator.serviceWorker.ready.then((registration) => {
registration.unregister().then(() => { registration.unregister().then(() => {
window.location.reload(); window.location.reload()
}); })
}); })
} else { } else {
// Service worker found. Proceed as normal. // Service worker found. Proceed as normal.
registerValidSW(swUrl, config); registerValidSW(swUrl, config)
} }
}) })
.catch(() => { .catch(() => {
console.log( console.log(
'No internet connection found. App is running in offline mode.', 'No internet connection found. App is running in offline mode.'
); )
}); })
} }
export function register(config?: Config) { export function register(config?: Config) {
if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
// The URL constructor is available in all browsers that support SW. // The URL constructor is available in all browsers that support SW.
const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href); const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href)
if (publicUrl.origin !== window.location.origin) { if (publicUrl.origin !== window.location.origin) {
// Our service worker won't work if PUBLIC_URL is on a different origin // Our service worker won't work if PUBLIC_URL is on a different origin
// from what our page is served on. This might happen if a CDN is used to // from what our page is served on. This might happen if a CDN is used to
// serve assets; see https://github.com/facebook/create-react-app/issues/2374 // serve assets; see https://github.com/facebook/create-react-app/issues/2374
return; return
} }
window.addEventListener('load', () => { window.addEventListener('load', () => {
const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`
if (isLocalhost) { if (isLocalhost) {
// This is running on localhost. Let's check if a service worker still exists or not. // This is running on localhost. Let's check if a service worker still exists or not.
checkValidServiceWorker(swUrl, config); checkValidServiceWorker(swUrl, config)
// Add some additional logging to localhost, pointing developers to the // Add some additional logging to localhost, pointing developers to the
// service worker/PWA documentation. // service worker/PWA documentation.
navigator.serviceWorker.ready.then(() => { navigator.serviceWorker.ready.then(() => {
console.log( console.log(
'This web app is being served cache-first by a service ' + 'This web app is being served cache-first by a service ' +
'worker. To learn more, visit https://bit.ly/CRA-PWA', 'worker. To learn more, visit https://bit.ly/CRA-PWA'
); )
}); })
} else { } else {
// Is not localhost. Just register service worker // Is not localhost. Just register service worker
registerValidSW(swUrl, config); registerValidSW(swUrl, config)
} }
}); })
} }
} }
@ -139,10 +139,10 @@ export function unregister() {
if ('serviceWorker' in navigator) { if ('serviceWorker' in navigator) {
navigator.serviceWorker.ready navigator.serviceWorker.ready
.then((registration) => { .then((registration) => {
registration.unregister(); registration.unregister()
}) })
.catch((error) => { .catch((error) => {
console.error(error.message); console.error(error.message)
}); })
} }
} }

View File

@ -1,12 +1,12 @@
// this adds jest-dom's custom assertions // this adds jest-dom's custom assertions
import '@testing-library/jest-dom/extend-expect'; import '@testing-library/jest-dom/extend-expect'
// i18next // i18next
import {join} from 'path'; import { join } from 'path'
import i18n from 'i18next'; import i18n from 'i18next'
import Backend from 'i18next-node-fs-backend'; import Backend from 'i18next-node-fs-backend'
import LanguageDetector from 'i18next-browser-languagedetector'; import LanguageDetector from 'i18next-browser-languagedetector'
import {initReactI18next} from 'react-i18next'; import { initReactI18next } from 'react-i18next'
i18n i18n
.use(Backend) .use(Backend)
@ -23,4 +23,4 @@ i18n
interpolation: { interpolation: {
escapeValue: false, // not needed for react as it escapes by default escapeValue: false, // not needed for react as it escapes by default
}, },
}); })

View File

@ -1,47 +1,47 @@
import { action, Action, createStore, createTypedHooks } from 'easy-peasy'; import { action, Action, createStore, createTypedHooks } from 'easy-peasy'
import { History } from 'history'; import { History } from 'history'
interface StatsModel { interface StatsModel {
availableCount: number; availableCount: number
totalCount: number; totalCount: number
add: Action<StatsModel, boolean>; add: Action<StatsModel, boolean>
reset: Action<StatsModel, void>; reset: Action<StatsModel, void>
} }
interface StoreModel { interface StoreModel {
stats: StatsModel; stats: StatsModel
} }
const statsModel: StatsModel = { const statsModel: StatsModel = {
availableCount: 0, availableCount: 0,
totalCount: 0, totalCount: 0,
add: action((state, isAvailable) => { add: action((state, isAvailable) => {
state.totalCount += 1; state.totalCount += 1
if (isAvailable) { if (isAvailable) {
state.availableCount += 1; state.availableCount += 1
} }
}), }),
reset: action((state) => { reset: action((state) => {
state.totalCount = 0; state.totalCount = 0
state.availableCount = 0; state.availableCount = 0
}), }),
}; }
const storeModel: StoreModel = { const storeModel: StoreModel = {
stats: statsModel, stats: statsModel,
}; }
export const store = createStore(storeModel); export const store = createStore(storeModel)
export function wrapHistoryWithStoreHandler(history: History) { export function wrapHistoryWithStoreHandler(history: History) {
history.listen(() => { history.listen(() => {
// reset stats counter // reset stats counter
store.getActions().stats.reset(); store.getActions().stats.reset()
}); })
return history; return history
} }
const typedHooks = createTypedHooks<StoreModel>(); const typedHooks = createTypedHooks<StoreModel>()
export const useStoreActions = typedHooks.useStoreActions; export const useStoreActions = typedHooks.useStoreActions
export const useStoreDispatch = typedHooks.useStoreDispatch; export const useStoreDispatch = typedHooks.useStoreDispatch
export const useStoreState = typedHooks.useStoreState; export const useStoreState = typedHooks.useStoreState

View File

@ -1,5 +1,5 @@
import styled, { createGlobalStyle } from 'styled-components'; import styled, { createGlobalStyle } from 'styled-components'
import { mobile } from '../util/css'; import { mobile } from '../util/css'
export const GlobalStyle = createGlobalStyle` export const GlobalStyle = createGlobalStyle`
* { * {
@ -25,7 +25,7 @@ body {
background: #f5f5f5; background: #f5f5f5;
} }
} }
`; `
export const Content = styled.div` export const Content = styled.div`
padding-top: 100px; padding-top: 100px;
@ -33,7 +33,7 @@ export const Content = styled.div`
${mobile} { ${mobile} {
padding-top: 60px; padding-top: 60px;
} }
`; `
export const Header = styled.header` export const Header = styled.header`
padding: 0 40px; padding: 0 40px;
@ -42,7 +42,7 @@ export const Header = styled.header`
${mobile} { ${mobile} {
padding: 0 20px; padding: 0 20px;
} }
`; `
export const Section = styled.div` export const Section = styled.div`
padding: 100px 20vw; padding: 100px 20vw;
@ -50,4 +50,4 @@ export const Section = styled.div`
${mobile} { ${mobile} {
padding: 60px 40px; padding: 60px 40px;
} }
`; `

View File

@ -1,18 +1,18 @@
import ReactGA from 'react-ga'; import ReactGA from 'react-ga'
import * as Sentry from '@sentry/browser'; import * as Sentry from '@sentry/browser'
import {History} from 'history'; import { History } from 'history'
const isProduction = process.env.NODE_ENV !== 'development'; const isProduction = process.env.NODE_ENV !== 'development'
export function wrapHistoryWithGA(history: History) { export function wrapHistoryWithGA(history: History) {
if (isProduction) { if (isProduction) {
ReactGA.initialize('UA-28919359-15'); ReactGA.initialize('UA-28919359-15')
ReactGA.pageview(window.location.pathname + window.location.search); ReactGA.pageview(window.location.pathname + window.location.search)
history.listen((location) => { history.listen((location) => {
ReactGA.pageview(location.pathname + location.search); ReactGA.pageview(location.pathname + location.search)
}); })
} }
return history; return history
} }
export function trackEvent({ export function trackEvent({
@ -21,10 +21,10 @@ export function trackEvent({
label = undefined, label = undefined,
value = undefined, value = undefined,
}: { }: {
category: string; category: string
action: string; action: string
label?: string; label?: string
value?: number; value?: number
}) { }) {
if (isProduction) { if (isProduction) {
ReactGA.event({ ReactGA.event({
@ -32,35 +32,35 @@ export function trackEvent({
action, action,
label, label,
value, value,
}); })
} }
} }
export function sendQueryEvent(query: string): void { export function sendQueryEvent(query: string): void {
trackEvent({category: 'Search', action: 'Invoke New Search', label: query}); trackEvent({ category: 'Search', action: 'Invoke New Search', label: query })
} }
export function sendGettingStartedEvent(): void { export function sendGettingStartedEvent(): void {
trackEvent({category: 'Search', action: 'Getting Started'}); trackEvent({ category: 'Search', action: 'Getting Started' })
} }
export function sendExpandEvent(): void { export function sendExpandEvent(): void {
trackEvent({category: 'Result', action: 'Expand Card'}); trackEvent({ category: 'Result', action: 'Expand Card' })
} }
export function sendAcceptSuggestionEvent(): void { export function sendAcceptSuggestionEvent(): void {
trackEvent({category: 'Suggestion', action: 'Accept'}); trackEvent({ category: 'Suggestion', action: 'Accept' })
} }
export function sendShuffleSuggestionEvent(): void { export function sendShuffleSuggestionEvent(): void {
trackEvent({category: 'Suggestion', action: 'Shuffle'}); trackEvent({ category: 'Suggestion', action: 'Shuffle' })
} }
export function initSentry(): void { export function initSentry(): void {
if (isProduction) { if (isProduction) {
Sentry.init({ Sentry.init({
dsn: 'https://7ab2df74aead499b950ebef190cc40b7@sentry.io/1759299', dsn: 'https://7ab2df74aead499b950ebef190cc40b7@sentry.io/1759299',
}); })
} }
} }
@ -68,16 +68,16 @@ export function sendError(
error: Error, error: Error,
errorInfo: { errorInfo: {
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
[key: string]: any; [key: string]: any
}, }
): Promise<string> { ): Promise<string> {
return new Promise((resolve) => { return new Promise((resolve) => {
if (isProduction) { if (isProduction) {
Sentry.withScope((scope) => { Sentry.withScope((scope) => {
scope.setExtras(errorInfo); scope.setExtras(errorInfo)
const eventId = Sentry.captureException(error); const eventId = Sentry.captureException(error)
resolve(eventId); resolve(eventId)
}); })
} }
}); })
} }

View File

@ -1,25 +1,25 @@
export function shuffleArray<T>(array: T[]): T[] { export function shuffleArray<T>(array: T[]): T[] {
for (let i = array.length - 1; i > 0; i--) { for (let i = array.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1)); const j = Math.floor(Math.random() * (i + 1))
const temp = array[i]; const temp = array[i]
array[i] = array[j]; array[i] = array[j]
array[j] = temp; array[j] = temp
} }
return array; return array
} }
export function sampleFromArray<T>(array: T[], maximum: number): T[] { export function sampleFromArray<T>(array: T[], maximum: number): T[] {
return shuffleArray(array).slice(0, maximum); return shuffleArray(array).slice(0, maximum)
} }
export function fillArray<T>(array: T[], filler: string, maximum: number): T[] { export function fillArray<T>(array: T[], filler: string, maximum: number): T[] {
const deficit = maximum - array.length; const deficit = maximum - array.length
if (deficit > 0) { if (deficit > 0) {
array = [...array, ...Array(deficit).fill(filler)]; array = [...array, ...Array(deficit).fill(filler)]
} }
return array; return array
} }
export function compose<T>(arg: T, ...fn: ((arg: T) => T)[]): T { export function compose<T>(arg: T, ...fn: ((arg: T) => T)[]): T {
return fn.reduce((arg, f) => f(arg), arg); return fn.reduce((arg, f) => f(arg), arg)
} }

View File

@ -1,14 +1,14 @@
interface CrispWindow extends Window { interface CrispWindow extends Window {
$crisp: unknown[]; $crisp: unknown[]
CRISP_WEBSITE_ID: string; CRISP_WEBSITE_ID: string
} }
declare let window: CrispWindow; declare let window: CrispWindow
export function initCrisp(): void { export function initCrisp(): void {
window.$crisp = []; window.$crisp = []
window.CRISP_WEBSITE_ID = '92b2e096-6892-47dc-bf4a-057bad52d82e'; window.CRISP_WEBSITE_ID = '92b2e096-6892-47dc-bf4a-057bad52d82e'
const s = document.createElement('script'); const s = document.createElement('script')
s.src = 'https://client.crisp.chat/l.js'; s.src = 'https://client.crisp.chat/l.js'
s.async = true; s.async = true
document.getElementsByTagName('head')[0].appendChild(s); document.getElementsByTagName('head')[0].appendChild(s)
} }

View File

@ -1,7 +1,7 @@
import { keyframes } from 'styled-components'; import { keyframes } from 'styled-components'
export const mobile = '@media screen and (max-width: 800px)'; export const mobile = '@media screen and (max-width: 800px)'
export const tablet = '@media screen and (max-width: 1200px)'; export const tablet = '@media screen and (max-width: 1200px)'
export const slideUp = keyframes` export const slideUp = keyframes`
from { from {
@ -10,4 +10,4 @@ from {
to { to {
transform: translateY(0) skewY(0); transform: translateY(0) skewY(0);
} }
`; `

View File

@ -1,22 +1,22 @@
import { render, waitFor } from '@testing-library/react'; import { render, waitFor } from '@testing-library/react'
import 'mutationobserver-shim'; import 'mutationobserver-shim'
import React from 'react'; import React from 'react'
import { useDeferredState } from './hooks'; import { useDeferredState } from './hooks'
const App: React.FC = () => { const App: React.FC = () => {
const [value, setValue] = useDeferredState(500, 0); const [value, setValue] = useDeferredState(500, 0)
React.useEffect(() => { React.useEffect(() => {
setValue(1); setValue(1)
setValue(2); setValue(2)
setValue(3); setValue(3)
}, [setValue]); }, [setValue])
return <div data-testid="root">{value}</div>; return <div data-testid="root">{value}</div>
}; }
it('provoke state flow after certain time passed', async () => { it('provoke state flow after certain time passed', async () => {
const { getByTestId } = render(<App />); const { getByTestId } = render(<App />)
expect(getByTestId('root').textContent).toBe('0'); expect(getByTestId('root').textContent).toBe('0')
await waitFor(() => { await waitFor(() => {
expect(getByTestId('root').textContent).toBe('3'); expect(getByTestId('root').textContent).toBe('3')
}); })
}); })

View File

@ -1,24 +1,24 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react'
import { Helmet } from 'react-helmet'; import { Helmet } from 'react-helmet'
export function useDeferredState<T>( export function useDeferredState<T>(
duration = 1000, duration = 1000,
initialValue: T, initialValue: T
): [T, React.Dispatch<React.SetStateAction<T>>] { ): [T, React.Dispatch<React.SetStateAction<T>>] {
const [response, setResponse] = useState(initialValue); const [response, setResponse] = useState(initialValue)
const [innerValue, setInnerValue] = useState(initialValue); const [innerValue, setInnerValue] = useState(initialValue)
useEffect(() => { useEffect(() => {
const fn = setTimeout(() => { const fn = setTimeout(() => {
setResponse(innerValue); setResponse(innerValue)
}, duration); }, duration)
return (): void => { return (): void => {
clearTimeout(fn); clearTimeout(fn)
}; }
}, [duration, innerValue]); }, [duration, innerValue])
return [response, setInnerValue]; return [response, setInnerValue]
} }
export function useOpenSearch(xmlPath: string) { export function useOpenSearch(xmlPath: string) {
@ -31,5 +31,5 @@ export function useOpenSearch(xmlPath: string) {
href={xmlPath} href={xmlPath}
/> />
</Helmet> </Helmet>
); )
} }

View File

@ -1,11 +1,11 @@
import i18n from 'i18next'; import i18n from 'i18next'
import Backend from 'i18next-chained-backend'; import Backend from 'i18next-chained-backend'
import LocalStorageBackend from 'i18next-localstorage-backend'; import LocalStorageBackend from 'i18next-localstorage-backend'
import XHR from 'i18next-xhr-backend'; import XHR from 'i18next-xhr-backend'
import LanguageDetector from 'i18next-browser-languagedetector'; import LanguageDetector from 'i18next-browser-languagedetector'
import { initReactI18next } from 'react-i18next'; import { initReactI18next } from 'react-i18next'
const TRANSLATION_VERSION = '3'; const TRANSLATION_VERSION = '3'
i18n i18n
.use(Backend) .use(Backend)
@ -32,6 +32,6 @@ i18n
interpolation: { interpolation: {
escapeValue: false, // not needed for react as it escapes by default escapeValue: false, // not needed for react as it escapes by default
}, },
}); })
export default i18n; export default i18n

View File

@ -1,5 +1,5 @@
import {isStandalone} from './pwa'; import { isStandalone } from './pwa'
it('recognize standalone mode', () => { it('recognize standalone mode', () => {
expect(isStandalone()).toEqual(false); expect(isStandalone()).toEqual(false)
}); })

View File

@ -1,8 +1,8 @@
interface CustomNavigator extends Navigator { interface CustomNavigator extends Navigator {
standalone?: boolean; standalone?: boolean
} }
export function isStandalone(): boolean { export function isStandalone(): boolean {
const navigator: CustomNavigator = window.navigator; const navigator: CustomNavigator = window.navigator
return 'standalone' in navigator && navigator.standalone === true; return 'standalone' in navigator && navigator.standalone === true
} }

View File

@ -1,10 +1,10 @@
import React, {Suspense} from 'react'; import React, { Suspense } from 'react'
import styled from 'styled-components'; import styled from 'styled-components'
import BarLoader from 'react-spinners/BarLoader'; import BarLoader from 'react-spinners/BarLoader'
export const FullScreenSuspense: React.FC = ({children}) => { export const FullScreenSuspense: React.FC = ({ children }) => {
return <Suspense fallback={<Fallback />}>{children}</Suspense>; return <Suspense fallback={<Fallback />}>{children}</Suspense>
}; }
const Container = styled.div` const Container = styled.div`
width: 100vw; width: 100vw;
@ -13,10 +13,10 @@ const Container = styled.div`
flex-direction: column; flex-direction: column;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
`; `
const Fallback: React.FC = () => ( const Fallback: React.FC = () => (
<Container> <Container>
<BarLoader /> <BarLoader />
</Container> </Container>
); )

View File

@ -1,9 +1,9 @@
import {capitalize} from './text'; import { capitalize } from './text'
it('capitalize text', () => { it('capitalize text', () => {
expect(capitalize('test')).toEqual('Test'); expect(capitalize('test')).toEqual('Test')
expect(capitalize('Test')).toEqual('Test'); expect(capitalize('Test')).toEqual('Test')
expect(capitalize('tEST')).toEqual('Test'); expect(capitalize('tEST')).toEqual('Test')
expect(capitalize('TEST')).toEqual('Test'); expect(capitalize('TEST')).toEqual('Test')
expect(capitalize('')).toEqual(''); expect(capitalize('')).toEqual('')
}); })

View File

@ -1,37 +1,37 @@
export function capitalize(text: string): string { export function capitalize(text: string): string {
if (text.length === 0) return ''; if (text.length === 0) return ''
return text[0].toUpperCase() + text.slice(1).toLowerCase(); return text[0].toUpperCase() + text.slice(1).toLowerCase()
} }
export function sanitize(text: string): string { export function sanitize(text: string): string {
return text return text
.replace(/[\s@+!#$%^&*()[\]./<>{}]/g, '') .replace(/[\s@+!#$%^&*()[\]./<>{}]/g, '')
.normalize('NFD') .normalize('NFD')
.replace(/[\u0300-\u036f]/g, ''); .replace(/[\u0300-\u036f]/g, '')
} }
export function upper(word: string): string { export function upper(word: string): string {
return word.toUpperCase(); return word.toUpperCase()
} }
export function lower(word: string): string { export function lower(word: string): string {
return word.toLowerCase(); return word.toLowerCase()
} }
export function stem(word: string): string { export function stem(word: string): string {
return word.replace(/[aiueo]$/, ''); return word.replace(/[aiueo]$/, '')
} }
export function germanify(word: string): string { export function germanify(word: string): string {
return word.replace('c', 'k').replace('C', 'K'); return word.replace('c', 'k').replace('C', 'K')
} }
export function njoin( export function njoin(
lhs: string, lhs: string,
rhs: string, rhs: string,
{elision = true}: {elision?: boolean} = {}, { elision = true }: { elision?: boolean } = {}
): string { ): string {
return elision return elision
? lhs + rhs.replace(new RegExp(`^${lhs[-1]}`, 'i'), '') ? lhs + rhs.replace(new RegExp(`^${lhs[-1]}`, 'i'), '')
: lhs + rhs; : lhs + rhs
} }

View File

@ -1753,4 +1753,4 @@ export const zones = [
'삼성', '삼성',
'테스트', '테스트',
'한국', '한국',
]; ]

View File

@ -1,18 +1,18 @@
import nock from 'nock'; import nock from 'nock'
import provider from '../api/services/existence/[query]'; import provider from '../api/services/existence/[query]'
import { mockProvider } from '../util/testHelpers'; import { mockProvider } from '../util/testHelpers'
test('return false if name is taken', async () => { test('return false if name is taken', async () => {
const result = await mockProvider(provider, { query: 'github.com/uetchy' }); const result = await mockProvider(provider, { query: 'github.com/uetchy' })
expect(result).toStrictEqual({ availability: false }); expect(result).toStrictEqual({ availability: false })
}); })
test('return true if name is not taken', async () => { test('return true if name is not taken', async () => {
const result = await mockProvider(provider, { const result = await mockProvider(provider, {
query: 'github.com/uetchyasdf', query: 'github.com/uetchyasdf',
}); })
expect(result).toStrictEqual({ availability: true }); expect(result).toStrictEqual({ availability: true })
}); })
beforeEach(() => { beforeEach(() => {
nock('https://github.com:443', { encodedQueryParams: true }) nock('https://github.com:443', { encodedQueryParams: true })
@ -44,7 +44,7 @@ beforeEach(() => {
"default-src 'none'; base-uri 'self'; connect-src 'self'; form-action 'self'; img-src 'self' data:; script-src,'self'; style-src 'unsafe-inline'", "default-src 'none'; base-uri 'self'; connect-src 'self'; form-action 'self'; img-src 'self' data:; script-src,'self'; style-src 'unsafe-inline'",
'Content-Encoding': 'gzip', 'Content-Encoding': 'gzip',
'X-GitHub-Request-Id': 'BA06:51D6:125A0F:1A9B4A:5D53E806', 'X-GitHub-Request-Id': 'BA06:51D6:125A0F:1A9B4A:5D53E806',
}); })
nock('https://github.com:443', { encodedQueryParams: true }) nock('https://github.com:443', { encodedQueryParams: true })
.head('/uetchy') .head('/uetchy')
.reply(200, [], { .reply(200, [], {
@ -76,5 +76,5 @@ beforeEach(() => {
"default-src 'none'; base-uri 'self'; block-all-mixed-content; connect-src 'self' uploads.github.com www.githubstatus.com collector.githubapp.com api.github.com www.google-analytics.com github-cloud.s3.amazonaws.com github-production-repository-file-5c1aeb.s3.amazonaws.com github-production-upload-manifest-file-7fdce7.s3.amazonaws.com github-production-user-asset-6210df.s3.amazonaws.com wss://live.github.com; font-src github.githubassets.com; form-action 'self' github.com gist.github.com; frame-ancestors 'none'; frame-src render.githubusercontent.com; img-src 'self' data: github.githubassets.com identicons.github.com collector.githubapp.com github-cloud.s3.amazonaws.com *.githubusercontent.com; manifest-src 'self'; media-src 'none'; script-src github.githubassets.com; style-src 'unsafe-inline' github.githubassets.com", "default-src 'none'; base-uri 'self'; block-all-mixed-content; connect-src 'self' uploads.github.com www.githubstatus.com collector.githubapp.com api.github.com www.google-analytics.com github-cloud.s3.amazonaws.com github-production-repository-file-5c1aeb.s3.amazonaws.com github-production-upload-manifest-file-7fdce7.s3.amazonaws.com github-production-user-asset-6210df.s3.amazonaws.com wss://live.github.com; font-src github.githubassets.com; form-action 'self' github.com gist.github.com; frame-ancestors 'none'; frame-src render.githubusercontent.com; img-src 'self' data: github.githubassets.com identicons.github.com collector.githubapp.com github-cloud.s3.amazonaws.com *.githubusercontent.com; manifest-src 'self'; media-src 'none'; script-src github.githubassets.com; style-src 'unsafe-inline' github.githubassets.com",
'Content-Encoding': 'gzip', 'Content-Encoding': 'gzip',
'X-GitHub-Request-Id': 'A922:19B1:AD411:FB69F:5D53E5BC', 'X-GitHub-Request-Id': 'A922:19B1:AD411:FB69F:5D53E5BC',
}); })
}); })

View File

@ -1 +1 @@
declare module 'react-tippy'; declare module 'react-tippy'

View File

@ -1 +1 @@
declare module 'whois-json'; declare module 'whois-json'