1
0
mirror of https://github.com/uetchy/namae.git synced 2025-03-16 12:10:32 +09:00

style: prettier

This commit is contained in:
uetchy 2020-08-31 08:41:53 +09:00 committed by uetchy
parent 9278b7a2ee
commit f5f7b51fcb
73 changed files with 1014 additions and 5220 deletions

View File

@ -1,4 +1,3 @@
{ {
"semi": false,
"singleQuote": true "singleQuote": true
} }

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,18 +1,18 @@
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 {
@ -21,10 +21,10 @@ export default async function handler(
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,32 +1,32 @@
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
@ -36,7 +36,7 @@ export default async function handler(
) )
// 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);
} }
} }

File diff suppressed because it is too large Load Diff

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(
@ -11,7 +11,7 @@ it('renders welcome message', async () => {
<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>
@ -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,10 +144,10 @@ 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[]> {
@ -156,10 +156,10 @@ async function findSynonyms(word: string): Promise<string[]> {
`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(
@ -169,58 +169,58 @@ async function findSynonyms(word: string): Promise<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,13 +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, DedicatedAvailability, Repeater } from '../core';
const CratesioCard: React.FC<{ query: string }> = ({ query }) => { const CratesioCard: 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.rust')}> <Card title={t('providers.rust')}>
@ -24,7 +24,7 @@ const CratesioCard: React.FC<{ query: string }> = ({ query }) => {
)} )}
</Repeater> </Repeater>
</Card> </Card>
) );
} };
export default CratesioCard export default CratesioCard;

View File

@ -1,15 +1,17 @@
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.replace(/[^0-9a-zA-Z_-]/g, '').replace(/_/g, '-') const sanitizedQuery = query
const lowerCase = sanitizedQuery.toLowerCase() .replace(/[^0-9a-zA-Z_-]/g, '')
.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)))
@ -19,7 +21,7 @@ const DomainCard: React.FC<{ query: string }> = ({ query }) => {
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`,
@ -29,7 +31,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`,
@ -42,7 +44,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')}>
@ -58,7 +60,7 @@ const DomainCard: React.FC<{ query: string }> = ({ query }) => {
)} )}
</Repeater> </Repeater>
</Card> </Card>
) );
} };
export default DomainCard export default DomainCard;

View File

@ -1,16 +1,18 @@
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.replace(/[^0-9a-zA-Z_-]/g, '').replace(/_/g, '-') const sanitizedQuery = query
const lowerCase = sanitizedQuery.toLowerCase() .replace(/[^0-9a-zA-Z_-]/g, '')
.replace(/_/g, '-');
const lowerCase = sanitizedQuery.toLowerCase();
const names = [lowerCase] const names = [lowerCase];
return ( return (
<Card title={t('providers.firebase')}> <Card title={t('providers.firebase')}>
@ -27,7 +29,7 @@ const FirebaseCard: React.FC<{ query: string }> = ({ query }) => {
)} )}
</Repeater> </Repeater>
</Card> </Card>
) );
} };
export default FirebaseCard export default FirebaseCard;

View File

@ -1,19 +1,19 @@
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, DedicatedAvailability, Repeater } 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 = [
`${lowerCase}hq`, `${lowerCase}hq`,
`${lowerCase}-team`, `${lowerCase}-team`,
`${lowerCase}js`, `${lowerCase}js`,
`${lowerCase}-rs`, `${lowerCase}-rs`,
] ];
return ( return (
<Card title={t('providers.github')}> <Card title={t('providers.github')}>
@ -31,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,16 +1,18 @@
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.replace(/[^0-9a-zA-Z_-]/g, '').replace(/_/g, '-') const sanitizedQuery = query
const lowerCase = sanitizedQuery.toLowerCase() .replace(/[^0-9a-zA-Z_-]/g, '')
.replace(/_/g, '-');
const lowerCase = sanitizedQuery.toLowerCase();
const names = [lowerCase] const names = [lowerCase];
return ( return (
<Card title={t('providers.heroku')}> <Card title={t('providers.heroku')}>
@ -26,7 +28,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,16 +1,18 @@
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.replace(/[^0-9a-zA-Z_-]/g, '').replace(/_/g, '-') const sanitizedQuery = query
const lowerCase = sanitizedQuery.toLowerCase() .replace(/[^0-9a-zA-Z_-]/g, '')
.replace(/_/g, '-');
const lowerCase = sanitizedQuery.toLowerCase();
const names = [lowerCase] const names = [lowerCase];
return ( return (
<Card title={t('providers.jsorg')}> <Card title={t('providers.jsorg')}>
@ -28,7 +30,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,16 +1,18 @@
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.replace(/[^0-9a-zA-Z_-]/g, '').replace(/_/g, '-') const sanitizedQuery = query
const lowerCase = sanitizedQuery.toLowerCase() .replace(/[^0-9a-zA-Z_-]/g, '')
.replace(/_/g, '-');
const lowerCase = sanitizedQuery.toLowerCase();
const names = [lowerCase] const names = [lowerCase];
return ( return (
<Card title={t('providers.netlify')}> <Card title={t('providers.netlify')}>
@ -26,7 +28,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,16 +1,18 @@
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.replace(/[^0-9a-zA-Z_-]/g, '').replace(/_/g, '-') const sanitizedQuery = query
const lowerCase = sanitizedQuery.toLowerCase() .replace(/[^0-9a-zA-Z_-]/g, '')
.replace(/_/g, '-');
const lowerCase = sanitizedQuery.toLowerCase();
const names = [lowerCase] const names = [lowerCase];
return ( return (
<Card title={t('providers.s3')}> <Card title={t('providers.s3')}>
@ -28,7 +30,7 @@ const S3Card: React.FC<{ query: string }> = ({ query }) => {
)} )}
</Repeater> </Repeater>
</Card> </Card>
) );
} };
export default S3Card export default S3Card;

View File

@ -1,16 +1,18 @@
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.replace(/[^0-9a-zA-Z_-]/g, '').replace(/_/g, '-') const sanitizedQuery = query
const lowerCase = sanitizedQuery.toLowerCase() .replace(/[^0-9a-zA-Z_-]/g, '')
.replace(/_/g, '-');
const lowerCase = sanitizedQuery.toLowerCase();
const names = [lowerCase] const names = [lowerCase];
return ( return (
<Card title={t('providers.slack')}> <Card title={t('providers.slack')}>
@ -29,7 +31,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,18 +1,20 @@
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.replace(/[^0-9a-zA-Z_-]/g, '').replace(/-/g, '_') const sanitizedQuery = query
const lowerCase = sanitizedQuery.toLowerCase() .replace(/[^0-9a-zA-Z_-]/g, '')
const capitalCase = capitalize(sanitizedQuery) .replace(/-/g, '_');
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`,
@ -20,7 +22,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')}>
@ -37,7 +39,7 @@ const TwitterCard: React.FC<{ query: string }> = ({ query }) => {
)} )}
</Repeater> </Repeater>
</Card> </Card>
) );
} };
export default TwitterCard export default TwitterCard;

View File

@ -1,16 +1,18 @@
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.replace(/[^0-9a-zA-Z_-]/g, '').replace(/_/g, '-') const sanitizedQuery = query
const lowerCase = sanitizedQuery.toLowerCase() .replace(/[^0-9a-zA-Z_-]/g, '')
.replace(/_/g, '-');
const lowerCase = sanitizedQuery.toLowerCase();
const names = [lowerCase] const names = [lowerCase];
return ( return (
<Card title={t('providers.now')}> <Card title={t('providers.now')}>
@ -37,7 +39,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}>
@ -34,24 +34,24 @@ ReactDOM.render(
<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

@ -20,21 +20,21 @@ const isLocalhost = Boolean(
/^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') {
@ -45,30 +45,30 @@ function registerValidSW(swUrl: string, config?: Config) {
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) {
@ -78,7 +78,7 @@ function checkValidServiceWorker(swUrl: string, config?: Config) {
}) })
.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,38 +86,38 @@ 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.
@ -125,13 +125,13 @@ export function register(config?: Config) {
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,29 +1,29 @@
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(
@ -33,5 +33,5 @@ export function njoin(
): 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';

View File

@ -13,7 +13,7 @@ export type HttpMethod =
export function fetch( export function fetch(
url: string, url: string,
method: HttpMethod = 'HEAD', method: HttpMethod = 'HEAD'
): Promise<Response> { ): Promise<Response> {
return nodeFetch(url, { method: method }); return nodeFetch(url, { method: method });
} }

View File

@ -1,6 +1,6 @@
export async function mockProvider( export async function mockProvider(
provider: any, provider: any,
query: unknown, query: unknown
): Promise<string> { ): Promise<string> {
const req = { const req = {
query, query,