1
0
mirror of https://github.com/uetchy/namae.git synced 2025-08-20 09:58:13 +09:00

chore: cosmetic changes

This commit is contained in:
2019-09-17 14:30:26 +09:00
parent 2438518e3c
commit 6c84493360
56 changed files with 724 additions and 729 deletions

View File

@@ -4,4 +4,4 @@ module.exports = {
testEnvironment: 'node', testEnvironment: 'node',
preset: 'ts-jest', preset: 'ts-jest',
testPathIgnorePatterns: ['/dist/'], testPathIgnorePatterns: ['/dist/'],
} };

View File

@@ -1,38 +1,38 @@
import { send, sendError, fetch, NowRequest, NowResponse } from '../util/http' import {send, sendError, fetch, NowRequest, NowResponse} from '../util/http';
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<{query: string; country: string}>, req: NowRequest<{query: string; country: string}>,
res: NowResponse res: NowResponse,
) { ) {
const { query, country } = req.query const {query, country} = req.query;
if (!query) { if (!query) {
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,
@@ -40,9 +40,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,21 +1,21 @@
import { send, sendError, fetch, NowRequest, NowResponse } from '../util/http' import {send, sendError, fetch, NowRequest, NowResponse} from '../util/http';
export default async function handler(req: NowRequest, res: NowResponse) { export default async function handler(req: NowRequest, res: NowResponse) {
const { query } = req.query const {query} = req.query;
if (!query) { if (!query) {
return sendError(res, new Error('no query given')) return sendError(res, new Error('no query given'));
} }
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,30 +1,30 @@
import dns from 'dns' import dns from 'dns';
import { send, sendError, NowRequest, NowResponse } from '../util/http' import {send, sendError, NowRequest, NowResponse} from '../util/http';
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(req: NowRequest, res: NowResponse) { export default async function handler(req: NowRequest, res: NowResponse) {
const { query } = req.query const {query} = req.query;
if (!query) { if (!query) {
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 res.status(200).json({ availability: true }) return res.status(200).json({availability: true});
} }
sendError(res, err) sendError(res, err);
} }
} }

View File

@@ -1,18 +1,18 @@
import whois from 'whois-json' import whois from 'whois-json';
import { send, sendError, NowRequest, NowResponse } from '../util/http' import {send, sendError, NowRequest, NowResponse} from '../util/http';
export default async function handler(req: NowRequest, res: NowResponse) { export default async function handler(req: NowRequest, res: NowResponse) {
const { query } = req.query const {query} = req.query;
if (!query) { if (!query) {
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,25 +1,25 @@
import { send, sendError, fetch, NowRequest, NowResponse } from '../util/http' import {send, sendError, fetch, NowRequest, NowResponse} from '../util/http';
export default async function handler(req: NowRequest, res: NowResponse) { export default async function handler(req: NowRequest, res: NowResponse) {
const { query } = req.query const {query} = req.query;
if (!query) { if (!query) {
return sendError(res, new Error('no query given')) return sendError(res, new Error('no query given'));
} }
if ( if (
!/^[(http(s)?):\/\/(www\.)?a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,6}\b([-a-zA-Z0-9@:%_\+.~#?&//=]*)$/.test( !/^[(http(s)?):\/\/(www\.)?a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,6}\b([-a-zA-Z0-9@:%_\+.~#?&//=]*)$/.test(
query query,
) )
) { ) {
return sendError(res, new Error('invalid characters')) return sendError(res, new Error('invalid characters'));
} }
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) {
sendError(res, err) sendError(res, err);
} }
} }

View File

@@ -1,19 +1,19 @@
import nock from 'nock' import nock from 'nock';
import { mockProvider } from '../util/testHelpers' import {mockProvider} from '../util/testHelpers';
import provider from './existence' import provider from './existence';
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})
@@ -45,7 +45,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, [], {
@@ -77,5 +77,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,22 +1,22 @@
import { send, sendError, fetch, NowRequest, NowResponse } from '../util/http' import {send, sendError, fetch, NowRequest, NowResponse} from '../util/http';
export default async function handler(req: NowRequest, res: NowResponse) { export default async function handler(req: NowRequest, res: NowResponse) {
const { query } = req.query const {query} = req.query;
if (!query) { if (!query) {
return sendError(res, new Error('no query given')) return sendError(res, new Error('no query given'));
} }
try { try {
const response = await fetch( const response = await fetch(
`https://api.launchpad.net/devel/ubuntu/+source/${encodeURIComponent( `https://api.launchpad.net/devel/ubuntu/+source/${encodeURIComponent(
query query,
)}`, )}`,
'GET' 'GET',
) );
const availability = response.status !== 200 const availability = response.status !== 200;
send(res, { availability }) send(res, {availability});
} catch (err) { } catch (err) {
sendError(res, err) sendError(res, err);
} }
} }

View File

@@ -1,17 +1,17 @@
import npmName from 'npm-name' import npmName from 'npm-name';
import { send, sendError, fetch, NowRequest, NowResponse } from '../util/http' import {send, sendError, fetch, NowRequest, NowResponse} from '../util/http';
export default async function handler(req: NowRequest, res: NowResponse) { export default async function handler(req: NowRequest, res: NowResponse) {
const { query } = req.query const {query} = req.query;
if (!query) { if (!query) {
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,17 +1,17 @@
import npmName from 'npm-name' import npmName from 'npm-name';
import { send, sendError, NowRequest, NowResponse } from '../util/http' import {send, sendError, NowRequest, NowResponse} from '../util/http';
export default async function handler(req: NowRequest, res: NowResponse) { export default async function handler(req: NowRequest, res: NowResponse) {
const { query } = req.query const {query} = req.query;
if (!query) { if (!query) {
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,39 +1,39 @@
import { send, sendError, fetch, NowRequest, NowResponse } from '../util/http' import {send, sendError, fetch, NowRequest, NowResponse} from '../util/http';
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<{query: string; country: string}>, req: NowRequest<{query: string; country: string}>,
res: NowResponse res: NowResponse,
) { ) {
const { query } = req.query const {query} = req.query;
if (!query) { if (!query) {
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);
const header = body.shift()!.split(',') const header = body.shift()!.split(',');
const result = body.map((csv) => { const result = body.map((csv) => {
const entry = csv.split(',').map((item) => const entry = csv.split(',').map((item) =>
item item
.replace(/(^"|"$)/g, '') .replace(/(^"|"$)/g, '')
.replace(/[A-Za-z0-9]/g, (str) => .replace(/[A-Za-z0-9]/g, (str) =>
String.fromCharCode(str.charCodeAt(0) - 0xfee0) String.fromCharCode(str.charCodeAt(0) - 0xfee0),
)
.replace(/ /g, ' ')
) )
.replace(/ /g, ' '),
);
return { return {
index: entry[0], index: entry[0],
@@ -70,8 +70,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: {
@@ -88,8 +88,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,23 +1,23 @@
import { send, sendError, fetch, NowRequest, NowResponse } from '../util/http' import {send, sendError, fetch, NowRequest, NowResponse} from '../util/http';
export default async function handler(req: NowRequest, res: NowResponse) { export default async function handler(req: NowRequest, res: NowResponse) {
const { query } = req.query const {query} = req.query;
if (!query) { if (!query) {
return sendError(res, new Error('no query given')) return sendError(res, new Error('no query given'));
} }
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) {
if (err.code === 'ENOTFOUND') { if (err.code === 'ENOTFOUND') {
send(res, { availability: true }) send(res, {availability: true});
} else { } else {
sendError(res, err) sendError(res, err);
} }
} }
} }

View File

@@ -1,23 +1,23 @@
import { send, sendError, fetch, NowResponse, NowRequest } from '../util/http' import {send, sendError, fetch, NowResponse, NowRequest} from '../util/http';
export default async function handler(req: NowRequest, res: NowResponse) { export default async function handler(req: NowRequest, res: NowResponse) {
const { query } = req.query const {query} = req.query;
if (!query) { if (!query) {
return sendError(res, new Error('no query given')) return sendError(res, new Error('no query given'));
} }
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,5 +1,5 @@
import nock from 'nock' import nock from 'nock';
nock.disableNetConnect() nock.disableNetConnect();
// nock.recorder.rec() // nock.recorder.rec()

View File

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

View File

@@ -1,13 +1,13 @@
export async function mockProvider(provider: any, query: any) { export async function mockProvider(provider: any, query: any) {
const req = { const req = {
query, query,
} };
const res = { const res = {
status: jest.fn().mockReturnThis(), status: jest.fn().mockReturnThis(),
send: jest.fn().mockReturnThis(), send: jest.fn().mockReturnThis(),
json: jest.fn().mockReturnThis(), json: jest.fn().mockReturnThis(),
setHeader: jest.fn(), setHeader: jest.fn(),
} };
await provider(req, res) await provider(req, res);
return res.json.mock.calls[0][0] return res.json.mock.calls[0][0];
} }

View File

@@ -9,7 +9,7 @@
"test": "CI=true yarn --cwd web test && yarn --cwd api test" "test": "CI=true yarn --cwd web test && yarn --cwd api test"
}, },
"dependencies": { "dependencies": {
"now": "^16.1.2" "now": "^16.2.0"
}, },
"devDependencies": { "devDependencies": {
"prettier": "^1.18.2" "prettier": "^1.18.2"

View File

@@ -1,14 +1,14 @@
import React, { Suspense } from 'react' import React, {Suspense} from 'react';
import { render, waitForElement } from '@testing-library/react' import {render, waitForElement} from '@testing-library/react';
import App from './App' import App from './App';
it('renders welcome message', async () => { it('renders welcome message', async () => {
const {getByText} = render( const {getByText} = render(
<Suspense fallback={<div>loading</div>}> <Suspense fallback={<div>loading</div>}>
<App /> <App />
</Suspense> </Suspense>,
) );
const text = await waitForElement(() => getByText('name new project')) const text = await waitForElement(() => getByText('name new project'));
expect(text).toBeTruthy() expect(text).toBeTruthy();
}) });

View File

@@ -1,22 +1,22 @@
import React, { useState } from 'react' import React, {useState} from 'react';
import styled, { createGlobalStyle } from 'styled-components' import styled, {createGlobalStyle} from 'styled-components';
import { Helmet } from 'react-helmet' import {Helmet} from 'react-helmet';
import { useTranslation } from 'react-i18next' import {useTranslation} from 'react-i18next';
import Welcome from './components/Welcome' import Welcome from './components/Welcome';
import Form from './components/Form' import Form from './components/Form';
import Cards from './components/cards' import Cards from './components/cards';
import Footer from './components/Footer' import Footer from './components/Footer';
import { mobile } from './util/css' import {mobile} from './util/css';
import { isStandalone } from './util/pwa' import {isStandalone} from './util/pwa';
export default function App() { export default function App() {
const [query, setQuery] = useState('') const [query, setQuery] = useState('');
const { t } = useTranslation() const {t} = useTranslation();
function onQuery(query: string) { function onQuery(query: string) {
setQuery(query) setQuery(query);
} }
return ( return (
@@ -37,7 +37,7 @@ export default function App() {
</Content> </Content>
<Footer /> <Footer />
</> </>
) );
} }
const GlobalStyle = createGlobalStyle` const GlobalStyle = createGlobalStyle`
@@ -58,11 +58,12 @@ body {
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
background: #ffffff; background: #ffffff;
${mobile} { ${mobile} {
background: #f5f5f5; background: #f5f5f5;
} }
} }
` `;
const Content = styled.div` const Content = styled.div`
padding-top: 100px; padding-top: 100px;
@@ -70,7 +71,7 @@ const Content = styled.div`
${mobile} { ${mobile} {
padding-top: 60px; padding-top: 60px;
} }
` `;
const Header = styled.header` const Header = styled.header`
padding: 0 40px; padding: 0 40px;
@@ -79,4 +80,4 @@ const Header = styled.header`
${mobile} { ${mobile} {
padding: 0 20px; padding: 0 20px;
} }
` `;

View File

@@ -1,12 +1,12 @@
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 { FaTwitter, FaGithubAlt } from 'react-icons/fa' import {FaTwitter, FaGithubAlt} from 'react-icons/fa';
import { ExternalLink } from './Links' import {ExternalLink} from './Links';
export default function Footer() { export default function Footer() {
const { t } = useTranslation() const {t} = useTranslation();
return ( return (
<Container> <Container>
@@ -30,7 +30,7 @@ export default function Footer() {
<ExternalLink <ExternalLink
aria-label="Tweet this page" aria-label="Tweet this page"
href={`https://twitter.com/intent/tweet?text=${encodeURIComponent( href={`https://twitter.com/intent/tweet?text=${encodeURIComponent(
`namae — ${t('title')}` `namae — ${t('title')}`,
)}&url=${encodeURIComponent('https://namae.dev')}`}> )}&url=${encodeURIComponent('https://namae.dev')}`}>
<FaTwitter /> <FaTwitter />
</ExternalLink> </ExternalLink>
@@ -50,7 +50,7 @@ export default function Footer() {
</a> </a>
</Box> </Box>
</Container> </Container>
) );
} }
const Container = styled.div` const Container = styled.div`
@@ -66,7 +66,7 @@ const Container = styled.div`
color: black; color: black;
text-decoration: none; text-decoration: none;
} }
` `;
const Box = styled.footer` const Box = styled.footer`
margin-bottom: 10px; margin-bottom: 10px;
@@ -74,7 +74,7 @@ const Box = styled.footer`
flex-direction: row; flex-direction: row;
justify-content: center; justify-content: center;
line-height: 1em; line-height: 1em;
` `;
const Links = styled.div` const Links = styled.div`
margin-left: 15px; margin-left: 15px;
@@ -84,8 +84,8 @@ const Links = styled.div`
${ExternalLink} { ${ExternalLink} {
margin-right: 5px; margin-right: 5px;
} }
` `;
const Bold = styled.span` const Bold = styled.span`
font-weight: bold; font-weight: bold;
` `;

View File

@@ -1,50 +1,50 @@
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 { useDeferredState } from '../util/hooks' import {useDeferredState} from '../util/hooks';
import { mobile } from '../util/css' import {mobile} from '../util/css';
import Suggestion from './Suggestion' import Suggestion from './Suggestion';
const Form: React.FC<{onQuery: (query: string) => void}> = ({onQuery}) => { const Form: React.FC<{onQuery: (query: string) => void}> = ({onQuery}) => {
const [query, setQuery] = useDeferredState(800, '') const [query, setQuery] = useDeferredState(800, '');
const [inputValue, setInputValue] = useState('') const [inputValue, setInputValue] = useState('');
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();
// set input value // set input value
function onInputChange(e: React.FormEvent<HTMLInputElement>) { function onInputChange(e: React.FormEvent<HTMLInputElement>) {
const value = e.currentTarget.value const value = e.currentTarget.value;
setInputValue(value) setInputValue(value);
} }
// clear input form and focus on it // clear input form and focus on it
function onLogoClick(e: React.MouseEvent<HTMLDivElement>) { function onLogoClick(e: React.MouseEvent<HTMLDivElement>) {
setInputValue('') setInputValue('');
inputRef.current!.focus() inputRef.current!.focus();
} }
// invoke when user clicked one of the suggested items // invoke when user clicked one of the suggested items
function onSuggestionCompleted(name: string) { function onSuggestionCompleted(name: string) {
setInputValue(name) setInputValue(name);
setSuggested(true) setSuggested(true);
} }
const queryGiven = query && query.length > 0 const queryGiven = query && query.length > 0;
useEffect(() => { useEffect(() => {
if (query.length === 0) { if (query.length === 0) {
setSuggested(false) setSuggested(false);
} }
onQuery(query) onQuery(query);
}, [query, onQuery]) }, [query, onQuery]);
useEffect(() => { useEffect(() => {
const modifiedValue = inputValue.replace(/[\s@+!#$%^&*()[\]]/g, '') const modifiedValue = inputValue.replace(/[\s@+!#$%^&*()[\]]/g, '');
setQuery(modifiedValue) setQuery(modifiedValue);
}, [inputValue, setQuery]) }, [inputValue, setQuery]);
return ( return (
<InputContainer> <InputContainer>
@@ -60,10 +60,10 @@ const Form: React.FC<{ onQuery: (query: string) => void }> = ({ onQuery }) => {
<Suggestion onSubmit={onSuggestionCompleted} query={query} /> <Suggestion onSubmit={onSuggestionCompleted} query={query} />
) : null} ) : null}
</InputContainer> </InputContainer>
) );
} };
export default Form export default Form;
const InputContainer = styled.div` const InputContainer = styled.div`
transform: translateY(40px); transform: translateY(40px);
@@ -75,7 +75,7 @@ const InputContainer = styled.div`
${mobile} { ${mobile} {
transform: translateY(20px); transform: translateY(20px);
} }
` `;
const Logo = styled.div` const Logo = styled.div`
margin-bottom: 5px; margin-bottom: 5px;
@@ -89,7 +89,7 @@ const Logo = styled.div`
${mobile} { ${mobile} {
font-size: 15px; font-size: 15px;
} }
` `;
const InputView = styled.input.attrs({ const InputView = styled.input.attrs({
type: 'text', type: 'text',
@@ -109,4 +109,4 @@ const InputView = styled.input.attrs({
${mobile} { ${mobile} {
font-size: 2rem; font-size: 2rem;
} }
` `;

View File

@@ -1,4 +1,4 @@
import React from 'react' import React from 'react';
export const SpectrumIcon = () => ( export const SpectrumIcon = () => (
<svg <svg
@@ -11,7 +11,7 @@ export const SpectrumIcon = () => (
xmlns="http://www.w3.org/2000/svg"> xmlns="http://www.w3.org/2000/svg">
<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 = () => ( export const NowIcon = () => (
<svg <svg
@@ -26,4 +26,4 @@ export const NowIcon = () => (
<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>
) );

View File

@@ -1,6 +1,6 @@
import styled from 'styled-components' import styled from 'styled-components';
export const ExternalLink = styled.a.attrs({ export const ExternalLink = styled.a.attrs({
target: '_blank', target: '_blank',
rel: 'noopener noreferrer', rel: 'noopener noreferrer',
})`` })``;

View File

@@ -1,15 +1,15 @@
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 { capitalize } from '../util/text' import {capitalize} from '../util/text';
import { mobile } from '../util/css' import {mobile} from '../util/css';
type Modifier = (word: string) => string type Modifier = (word: string) => string;
const maximumCount = 3 const maximumCount = 3;
const modifiers: Modifier[] = [ const modifiers: Modifier[] = [
(word) => `${capitalize(word)}ify`, (word) => `${capitalize(word)}ify`,
(word) => `lib${lower(word)}`, (word) => `lib${lower(word)}`,
@@ -43,98 +43,98 @@ const modifiers: Modifier[] = [
(word) => `In${capitalize(word)}`, (word) => `In${capitalize(word)}`,
(word) => `Uni${lower(word)}`, (word) => `Uni${lower(word)}`,
(word) => `${capitalize(word)}`, (word) => `${capitalize(word)}`,
] ];
function lower(word: string) { function lower(word: string) {
return word.toLowerCase() return word.toLowerCase();
} }
function shuffleArray(array: any[]) { function shuffleArray(array: any[]) {
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;
} }
function sampleFromArray(array: any[], maximum: number) { function sampleFromArray(array: any[], maximum: number) {
return shuffleArray(array).slice(0, maximum) return shuffleArray(array).slice(0, maximum);
} }
function modifyWord(word: string) { function modifyWord(word: string) {
return modifiers[Math.floor(Math.random() * modifiers.length)](word) return modifiers[Math.floor(Math.random() * modifiers.length)](word);
} }
function fillArray(array: any[], filler: string, maximum: number) { function fillArray(array: any[], filler: string, maximum: number) {
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;
} }
async function findSynonyms(word: string) { async function findSynonyms(word: string) {
try { try {
const response = await fetch( const response = await fetch(
`https://translate.googleapis.com/translate_a/single?client=gtx&sl=auto&dt=ss&ie=UTF-8&oe=UTF-8&dj=1&q=${encodeURIComponent( `https://translate.googleapis.com/translate_a/single?client=gtx&sl=auto&dt=ss&ie=UTF-8&oe=UTF-8&dj=1&q=${encodeURIComponent(
word word,
)}` )}`,
) );
const json: { const json: {
synsets: Array<{ entry: Array<{ synonym: string[] }> }> synsets: Array<{entry: Array<{synonym: string[]}>}>;
} = await response.json() } = await response.json();
const synonyms = Array.from( const synonyms = Array.from(
new Set<string>( new Set<string>(
json.synsets.reduce( json.synsets.reduce(
(sum, synset) => [...sum, ...synset.entry.map((e) => e.synonym[0])], (sum, synset) => [...sum, ...synset.entry.map((e) => e.synonym[0])],
[] as string[] [] as string[],
) ),
) ),
).filter((word) => !word.match(/[\s-]/)) ).filter((word) => !word.match(/[\s-]/));
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() { function shuffle() {
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) { function applyQuery(name: string) {
onSubmit(name) onSubmit(name);
} }
useEffect(() => { useEffect(() => {
const fn = async () => { const fn = async () => {
if (query && query.length > 0) { if (query && query.length > 0) {
const synonyms = await findSynonyms(query) const synonyms = await findSynonyms(query);
synonymRef.current = synonyms synonymRef.current = synonyms;
const best = fillArray( const best = fillArray(
sampleFromArray(synonyms, maximumCount), sampleFromArray(synonyms, maximumCount),
query, query,
maximumCount maximumCount,
).map((word) => modifyWord(word)) ).map((word) => modifyWord(word));
setBestWords(best) setBestWords(best);
} }
} };
fn() fn();
}, [query]) }, [query]);
return ( return (
<Container> <Container>
@@ -151,10 +151,10 @@ const Suggestion: React.FC<{
</Icon> </Icon>
</Items> </Items>
</Container> </Container>
) );
} };
export default Suggestion export default Suggestion;
const Container = styled.div` const Container = styled.div`
margin-bottom: 10px; margin-bottom: 10px;
@@ -163,7 +163,7 @@ const Container = styled.div`
align-items: center; align-items: center;
flex-wrap: wrap; flex-wrap: wrap;
justify-content: center; justify-content: center;
` `;
const Title = styled.div` const Title = styled.div`
margin-top: 15px; margin-top: 15px;
@@ -171,7 +171,7 @@ const Title = styled.div`
color: gray; color: gray;
border: 1px solid gray; border: 1px solid gray;
border-radius: 2em; border-radius: 2em;
` `;
const Items = styled.div` const Items = styled.div`
margin-top: 2px; margin-top: 2px;
@@ -184,7 +184,7 @@ const Items = styled.div`
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
} }
` `;
const Item = styled.div` const Item = styled.div`
margin-top: 8px; margin-top: 8px;
@@ -198,10 +198,10 @@ const Item = styled.div`
${mobile} { ${mobile} {
margin-right: 0; margin-right: 0;
} }
` `;
const Icon = styled(Item)` const Icon = styled(Item)`
display: flex; display: flex;
align-items: center; align-items: center;
border-bottom: none; border-bottom: none;
` `;

View File

@@ -1,6 +1,6 @@
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 { import {
FaMapSigns, FaMapSigns,
@@ -15,15 +15,15 @@ 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 } from 'react-icons/di' import {DiRust, DiHeroku} from 'react-icons/di';
import { SpectrumIcon, NowIcon } from './Icons' import {SpectrumIcon, NowIcon} from './Icons';
import { mobile } from '../util/css' import {mobile} from '../util/css';
export default function Welcome() { export default function Welcome() {
const { t } = useTranslation() const {t} = useTranslation();
return ( return (
<Container> <Container>
@@ -88,7 +88,7 @@ export default function Welcome() {
</ListItem> </ListItem>
</List> </List>
</Container> </Container>
) );
} }
const Container = styled.div` const Container = styled.div`
@@ -105,7 +105,7 @@ const Container = styled.div`
padding-left: 40px; padding-left: 40px;
font-size: 1.2rem; font-size: 1.2rem;
} }
` `;
const Header = styled.h1` const Header = styled.h1`
font-size: 3.5em; font-size: 3.5em;
@@ -115,14 +115,14 @@ const Header = styled.h1`
${mobile} { ${mobile} {
font-size: 3em; font-size: 3em;
} }
` `;
const Text = styled.p` const Text = styled.p`
font-size: 1.2em; font-size: 1.2em;
color: #3c3c3c; color: #3c3c3c;
` `;
const Hero = styled.div`` const Hero = styled.div``;
const List = styled.div` const List = styled.div`
margin-top: 50px; margin-top: 50px;
@@ -139,7 +139,7 @@ const List = styled.div`
${mobile} { ${mobile} {
flex-direction: column; flex-direction: column;
} }
` `;
const ListItem = styled.div` const ListItem = styled.div`
margin: 15px; margin: 15px;
@@ -155,4 +155,4 @@ const ListItem = styled.div`
svg { svg {
margin-right: 5px; margin-right: 5px;
} }
` `;

View File

@@ -1,19 +1,19 @@
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 { 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 (
<> <>
@@ -31,17 +31,17 @@ const Search: React.FC<{ query: string }> = ({ query }) => {
<Result title={t('noResult')} icon={<FaInfoCircle />} /> <Result title={t('noResult')} icon={<FaInfoCircle />} />
)} )}
</> </>
) );
} };
const AppStoreCard: React.FC<{query: string}> = ({query}) => { const AppStoreCard: React.FC<{query: string}> = ({query}) => {
const { t } = useTranslation() const {t} = useTranslation();
return ( return (
<Card title={t('providers.appStore')}> <Card title={t('providers.appStore')}>
<Search query={query} /> <Search query={query} />
</Card> </Card>
) );
} };
export default AppStoreCard export default AppStoreCard;

View File

@@ -1,14 +1,14 @@
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, Repeater, DedicatedAvailability } from './core' import {Card, Repeater, DedicatedAvailability} 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,21 +1,21 @@
import React from 'react' import React from 'react';
import { useTranslation } from 'react-i18next' import {useTranslation} from 'react-i18next';
import { FaMapSigns } from 'react-icons/fa' import {FaMapSigns} from 'react-icons/fa';
import { Card, Repeater, DedicatedAvailability } from './core' import {Card, Repeater, DedicatedAvailability} from './core';
const DomainCard: React.FC<{query: string}> = ({query}) => { const DomainCard: React.FC<{query: string}> = ({query}) => {
const { t } = useTranslation() const {t} = useTranslation();
const lowerCase = query.toLowerCase() const lowerCase = query.toLowerCase();
const names = [`${lowerCase}.com`, `${lowerCase}.app`] const names = [`${lowerCase}.com`, `${lowerCase}.app`];
const moreNames = [ const moreNames = [
`${lowerCase}app.com`, `${lowerCase}app.com`,
`get${lowerCase}.com`, `get${lowerCase}.com`,
`${lowerCase}.dev`, `${lowerCase}.dev`,
`${lowerCase}.io`, `${lowerCase}.io`,
`${lowerCase}.tools`, `${lowerCase}.tools`,
] ];
return ( return (
<Card title={t('providers.domains')}> <Card title={t('providers.domains')}>
@@ -31,7 +31,7 @@ const DomainCard: React.FC<{ query: string }> = ({ query }) => {
)} )}
</Repeater> </Repeater>
</Card> </Card>
) );
} };
export default DomainCard export default DomainCard;

View File

@@ -1,20 +1,20 @@
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, Repeater, DedicatedAvailability } from './core' import {Card, Repeater, DedicatedAvailability} from './core';
const GithubCard: React.FC<{query: string}> = ({query}) => { const GithubCard: React.FC<{query: string}> = ({query}) => {
const { t } = useTranslation() const {t} = useTranslation();
const lowerCase = query.toLowerCase() const lowerCase = query.toLowerCase();
const names = [query] const names = [query];
const moreNames = [ const moreNames = [
`${lowerCase}hq`, `${lowerCase}hq`,
`${lowerCase}-team`, `${lowerCase}-team`,
`${lowerCase}-org`, `${lowerCase}-org`,
`${lowerCase}-js`, `${lowerCase}-js`,
] ];
return ( return (
<Card title={t('providers.github')}> <Card title={t('providers.github')}>
@@ -34,7 +34,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 { 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 lowerCase = query.toLowerCase() const lowerCase = query.toLowerCase();
const names = [lowerCase] const names = [lowerCase];
return ( return (
<Card title={t('providers.heroku')}> <Card title={t('providers.heroku')}>
@@ -24,7 +24,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')}>
@@ -38,7 +38,7 @@ const HomebrewCard: React.FC<{ query: string }> = ({ query }) => {
)} )}
</Repeater> </Repeater>
</Card> </Card>
) );
} };
export default HomebrewCard export default HomebrewCard;

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 { 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 lowerCase = query.toLowerCase() const lowerCase = query.toLowerCase();
const names = [lowerCase] const names = [lowerCase];
return ( return (
<Card title={t('providers.jsorg')}> <Card title={t('providers.jsorg')}>
@@ -26,7 +26,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')}>
@@ -32,7 +32,7 @@ const LinuxCard: React.FC<{ query: string }> = ({ query }) => {
)} )}
</Repeater> </Repeater>
</Card> </Card>
) );
} };
export default LinuxCard export default LinuxCard;

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 { NowIcon } from '../Icons' import {NowIcon} from '../Icons';
import { Card, Repeater, DedicatedAvailability } from './core' import {Card, Repeater, DedicatedAvailability} from './core';
const NowCard: React.FC<{query: string}> = ({query}) => { const NowCard: 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.now')}> <Card title={t('providers.now')}>
@@ -24,7 +24,7 @@ const NowCard: React.FC<{ query: string }> = ({ query }) => {
)} )}
</Repeater> </Repeater>
</Card> </Card>
) );
} };
export default NowCard export default NowCard;

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] const names = [lowerCase];
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,17 +1,17 @@
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(`/availability/nta/${term}`) as { const response = useFetch(`/availability/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 (
<> <>
@@ -28,17 +28,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 { 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')}>
@@ -27,7 +27,7 @@ const RubyGemsCard: React.FC<{ query: string }> = ({ query }) => {
)} )}
</Repeater> </Repeater>
</Card> </Card>
) );
} };
export default RubyGemsCard export default RubyGemsCard;

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 { 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 lowerCase = query.toLowerCase() const lowerCase = query.toLowerCase();
const names = [lowerCase] const names = [lowerCase];
return ( return (
<Card title={t('providers.s3')}> <Card title={t('providers.s3')}>
@@ -28,7 +28,7 @@ const S3Card: React.FC<{ query: string }> = ({ query }) => {
)} )}
</Repeater> </Repeater>
</Card> </Card>
) );
} };
export default S3Card export default S3Card;

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 { 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 lowerCase = query.toLowerCase() const lowerCase = query.toLowerCase();
const names = [lowerCase] const names = [lowerCase];
return ( return (
<Card title={t('providers.slack')}> <Card title={t('providers.slack')}>
@@ -27,7 +27,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,16 +1,16 @@
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 lowerCase = query.toLowerCase() const lowerCase = query.toLowerCase();
const capitalCase = capitalize(query) const capitalCase = capitalize(query);
const names = [query] const names = [query];
const moreNames = [ const moreNames = [
`${lowerCase}app`, `${lowerCase}app`,
`hey${lowerCase}`, `hey${lowerCase}`,
@@ -18,7 +18,7 @@ const TwitterCard: React.FC<{ query: string }> = ({ query }) => {
`${capitalCase}HQ`, `${capitalCase}HQ`,
`${lowerCase}_official`, `${lowerCase}_official`,
`${lowerCase}-support`, `${lowerCase}-support`,
] ];
return ( return (
<Card title={t('providers.twitter')}> <Card title={t('providers.twitter')}>
@@ -36,7 +36,7 @@ const TwitterCard: React.FC<{ query: string }> = ({ query }) => {
)} )}
</Repeater> </Repeater>
</Card> </Card>
) );
} };
export default TwitterCard export default TwitterCard;

View File

@@ -1,20 +1,20 @@
import React, { useState, useEffect, Suspense } from 'react' import React, {useState, useEffect, Suspense} from 'react';
import styled from 'styled-components' import styled from 'styled-components';
import useFetch from 'fetch-suspense' import useFetch from 'fetch-suspense';
import { Tooltip } from 'react-tippy' import {Tooltip} from 'react-tippy';
import 'react-tippy/dist/tippy.css' import 'react-tippy/dist/tippy.css';
import BarLoader from 'react-spinners/BarLoader' import BarLoader from 'react-spinners/BarLoader';
import { GoInfo } from 'react-icons/go' import {GoInfo} from 'react-icons/go';
import { useTranslation } from 'react-i18next' import {useTranslation} from 'react-i18next';
import { mobile } from '../../util/css' import {mobile} from '../../util/css';
import { ExternalLink } from '../Links' import {ExternalLink} from '../Links';
const COLORS = { 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 (
@@ -33,24 +33,24 @@ export const Card: React.FC<{ title: string }> = ({ title, children }) => {
</ErrorBoundary> </ErrorBoundary>
</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() {
setRevealAlternatives(true) setRevealAlternatives(true);
} }
useEffect(() => { useEffect(() => {
setRevealAlternatives(false) setRevealAlternatives(false);
}, [items, moreItems]) }, [items, moreItems]);
return ( return (
<> <>
@@ -67,25 +67,25 @@ 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;
} }
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,
@@ -99,11 +99,11 @@ export const DedicatedAvailability: React.FC<{
icon, icon,
}) => { }) => {
const response = useFetch( const response = useFetch(
`/availability/${service}/${encodeURIComponent(query || name)}` `/availability/${service}/${encodeURIComponent(query || name)}`,
) as Response ) as Response;
if (response.error) { if (response.error) {
throw new Error(`${service}: ${response.error}`) throw new Error(`${service}: ${response.error}`);
} }
return ( return (
@@ -116,19 +116,19 @@ export const DedicatedAvailability: React.FC<{
prefix={prefix} prefix={prefix}
suffix={suffix} suffix={suffix}
/> />
) );
} };
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 = '',
@@ -140,13 +140,13 @@ export const ExistentialAvailability: React.FC<{
suffix = '', suffix = '',
icon, icon,
}) => { }) => {
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 Error(`${name}: ${response.status}`) throw new Error(`${name}: ${response.status}`);
} }
const availability = response.status === 404 const availability = response.status === 404;
return ( return (
<Result <Result
@@ -158,17 +158,17 @@ export const ExistentialAvailability: React.FC<{
prefix={prefix} prefix={prefix}
suffix={suffix} suffix={suffix}
/> />
) );
} };
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;
color?: string color?: string;
prefix?: string prefix?: string;
suffix?: string suffix?: string;
}> = ({ }> = ({
title, title,
message = '', message = '',
@@ -184,7 +184,7 @@ export const Result: React.FC<{
{title} {title}
{suffix} {suffix}
</> </>
) );
return ( return (
<ResultContainer> <ResultContainer>
<Tooltip <Tooltip
@@ -205,20 +205,20 @@ export const Result: React.FC<{
</ResultItem> </ResultItem>
</Tooltip> </Tooltip>
</ResultContainer> </ResultContainer>
) );
} };
class ErrorBoundary extends React.Component< class ErrorBoundary extends React.Component<
{}, {},
{hasError: boolean; message: string} {hasError: boolean; message: string}
> { > {
constructor(props: {}) { constructor(props: {}) {
super(props) super(props);
this.state = { hasError: false, message: '' } this.state = {hasError: false, message: ''};
} }
static getDerivedStateFromError(error: Error) { static getDerivedStateFromError(error: Error) {
return { hasError: true, message: error.message } return {hasError: true, message: error.message};
} }
render() { render() {
@@ -239,9 +239,9 @@ class ErrorBoundary extends React.Component<
</ResultItem> </ResultItem>
</ResultContainer> </ResultContainer>
</Tooltip> </Tooltip>
) );
} }
return this.props.children return this.props.children;
} }
} }
@@ -256,7 +256,7 @@ const CellError: React.FC = ({ children }) => (
{children} {children}
</Suspense> </Suspense>
</ErrorBoundary> </ErrorBoundary>
) );
const CardContainer = styled.div` const CardContainer = styled.div`
padding: 40px; padding: 40px;
@@ -265,7 +265,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;
@@ -275,7 +275,7 @@ const CardTitle = styled.div`
${mobile} { ${mobile} {
padding-left: 20px; padding-left: 20px;
} }
` `;
const CardContent = styled.div` const CardContent = styled.div`
border-radius: 2px; border-radius: 2px;
@@ -286,7 +286,7 @@ const CardContent = styled.div`
background: white; background: white;
border-radius: 0; border-radius: 0;
} }
` `;
const Button = styled.div` const Button = styled.div`
margin-top: 5px; margin-top: 5px;
@@ -297,17 +297,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;
margin-top: 8px; margin-top: 8px;
` `;
const ResultIcon = styled.div` const ResultIcon = styled.div`
width: 1em; width: 1em;
` `;
const ResultItem = styled.div` const ResultItem = styled.div`
display: flex; display: flex;
@@ -315,7 +315,7 @@ 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};
` `;
const ResultName = styled.div` const ResultName = styled.div`
margin-left: 6px; margin-left: 6px;
@@ -327,4 +327,4 @@ const ResultName = styled.div`
text-decoration: none; text-decoration: none;
color: inherit; color: inherit;
} }
` `;

View File

@@ -1,31 +1,31 @@
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 { mobile } from '../../util/css' import {mobile} from '../../util/css';
import DomainCard from './Domains' import DomainCard from './Domains';
import GithubCard from './GitHubRepository' import GithubCard from './GitHubRepository';
import NpmCard from './Npm' import NpmCard from './Npm';
import PypiCard from './PyPI' import PypiCard from './PyPI';
import RubyGemsCard from './RubyGems' import RubyGemsCard from './RubyGems';
import CratesioCard from './Cratesio' import CratesioCard from './Cratesio';
import HomebrewCard from './Homebrew' import HomebrewCard from './Homebrew';
import LinuxCard from './Linux' import LinuxCard from './Linux';
import TwitterCard from './Twitter' import TwitterCard from './Twitter';
import SpectrumCard from './Spectrum' import SpectrumCard from './Spectrum';
import SlackCard from './Slack' import SlackCard from './Slack';
import S3Card from './S3' import S3Card from './S3';
import JsOrgCard from './JsOrg' import JsOrgCard from './JsOrg';
import GithubSearchCard from './GitHubSearch' import GithubSearchCard from './GitHubSearch';
import AppStoreCard from './AppStore' import AppStoreCard from './AppStore';
import HerokuCard from './Heroku' import HerokuCard from './Heroku';
import NowCard from './Now' import NowCard from './Now';
import NtaCard from './Nta' import NtaCard from './Nta';
const Index: React.FC<{query: string}> = ({query}) => { const Index: React.FC<{query: string}> = ({query}) => {
const { const {
i18n: {language}, i18n: {language},
} = useTranslation() } = useTranslation();
return ( return (
<> <>
@@ -52,10 +52,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;
@@ -66,4 +66,4 @@ const Cards = styled.div`
${mobile} { ${mobile} {
flex-direction: column; flex-direction: column;
} }
` `;

View File

@@ -1,24 +1,24 @@
import React from 'react' import React from 'react';
import ReactDOM from 'react-dom' import ReactDOM from 'react-dom';
import App from './App' import App from './App';
import * as serviceWorker from './serviceWorker' import * as serviceWorker from './serviceWorker';
import { FullScreenSuspense } from './util/suspense' import {FullScreenSuspense} from './util/suspense';
import './util/i18n' import './util/i18n';
const Container = () => ( const Container = () => (
<FullScreenSuspense> <FullScreenSuspense>
<App /> <App />
</FullScreenSuspense> </FullScreenSuspense>
) );
ReactDOM.render(<Container />, document.getElementById('root')) ReactDOM.render(<Container />, document.getElementById('root'));
// register Google Analytics // register Google Analytics
if (process.env.NODE_ENV !== 'development') { if (process.env.NODE_ENV !== 'development') {
import('react-ga').then((ReactGA) => { import('react-ga').then((ReactGA) => {
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);
}) });
} }
serviceWorker.register({}) serviceWorker.register({});

View File

@@ -16,49 +16,49 @@ const isLocalhost = Boolean(
window.location.hostname === '[::1]' || window.location.hostname === '[::1]' ||
// 127.0.0.1/8 is considered localhost for IPv4. // 127.0.0.1/8 is considered localhost for IPv4.
window.location.hostname.match( window.location.hostname.match(
/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/,
) ),
) );
type Config = { type Config = {
onSuccess?: (registration: ServiceWorkerRegistration) => void onSuccess?: (registration: ServiceWorkerRegistration) => void;
onUpdate?: (registration: ServiceWorkerRegistration) => void onUpdate?: (registration: ServiceWorkerRegistration) => void;
} };
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( const publicUrl = new URL(
(process as {env: {[key: string]: string}}).env.PUBLIC_URL, (process as {env: {[key: string]: string}}).env.PUBLIC_URL,
window.location.href window.location.href,
) );
if (publicUrl.origin !== window.location.origin) { if (publicUrl.origin !== window.location.origin) {
// Our service worker won't work if PUBLIC_URL is on a different origin // Our service worker won't work if PUBLIC_URL is on a different origin
// from what our page is served on. This might happen if a CDN is used to // from what our page is served on. This might happen if a CDN is used to
// serve assets; see https://github.com/facebook/create-react-app/issues/2374 // serve assets; see https://github.com/facebook/create-react-app/issues/2374
return return;
} }
window.addEventListener('load', () => { window.addEventListener('load', () => {
const swUrl = `${process.env.PUBLIC_URL}/service-worker.js` const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
if (isLocalhost) { if (isLocalhost) {
// This is running on localhost. Let's check if a service worker still exists or not. // This is running on localhost. Let's check if a service worker still exists or not.
checkValidServiceWorker(swUrl, config) checkValidServiceWorker(swUrl, config);
// Add some additional logging to localhost, pointing developers to the // Add some additional logging to localhost, pointing developers to the
// service worker/PWA documentation. // service worker/PWA documentation.
navigator.serviceWorker.ready.then(() => { navigator.serviceWorker.ready.then(() => {
console.log( console.log(
'This web app is being served cache-first by a service ' + 'This web app is being served cache-first by a service ' +
'worker. To learn more, visit https://bit.ly/CRA-PWA' 'worker. To learn more, visit https://bit.ly/CRA-PWA',
) );
}) });
} else { } else {
// Is not localhost. Just register service worker // Is not localhost. Just register service worker
registerValidSW(swUrl, config) registerValidSW(swUrl, config);
} }
}) });
} }
} }
@@ -67,9 +67,9 @@ function registerValidSW(swUrl: string, config?: Config) {
.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') {
@@ -79,31 +79,31 @@ function registerValidSW(swUrl: string, config?: Config) {
// content until all client tabs are closed. // content until all client tabs are closed.
console.log( console.log(
'New content is available and will be used when all ' + 'New content is available and will be used when all ' +
'tabs for this page are closed. See https://bit.ly/CRA-PWA.' 'tabs for this page are closed. See https://bit.ly/CRA-PWA.',
) );
// Execute callback // Execute callback
if (config && config.onUpdate) { if (config && config.onUpdate) {
config.onUpdate(registration) config.onUpdate(registration);
} }
} else { } else {
// At this point, everything has been precached. // At this point, everything has been precached.
// It's the perfect time to display a // It's the perfect time to display a
// "Content is cached for offline use." message. // "Content is cached for offline use." message.
console.log('Content is cached for offline use.') console.log('Content is cached for offline use.');
// Execute callback // Execute callback
if (config && config.onSuccess) { if (config && config.onSuccess) {
config.onSuccess(registration) config.onSuccess(registration);
}
}
} }
} }
} }
};
};
}) })
.catch((error) => { .catch((error) => {
console.error('Error during service worker registration:', error) console.error('Error during service worker registration:', error);
}) });
} }
function checkValidServiceWorker(swUrl: string, config?: Config) { function checkValidServiceWorker(swUrl: string, config?: Config) {
@@ -111,7 +111,7 @@ function checkValidServiceWorker(swUrl: string, config?: Config) {
fetch(swUrl) fetch(swUrl)
.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.indexOf('javascript') === -1) (contentType != null && contentType.indexOf('javascript') === -1)
@@ -119,25 +119,25 @@ 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 unregister() { export function unregister() {
if ('serviceWorker' in navigator) { if ('serviceWorker' in navigator) {
navigator.serviceWorker.ready.then((registration) => { navigator.serviceWorker.ready.then((registration) => {
registration.unregister() registration.unregister();
}) });
} }
} }

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 +1 @@
declare module 'react-tippy' declare module 'react-tippy';

View File

@@ -1 +1 @@
export const mobile = '@media screen and (max-width: 800px)' export const mobile = '@media screen and (max-width: 800px)';

View File

@@ -1,21 +1,21 @@
import { useState, useEffect } from 'react' import {useState, useEffect} from 'react';
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 () => { return () => {
clearTimeout(fn) clearTimeout(fn);
} };
}, [duration, innerValue]) }, [duration, innerValue]);
return [response, setInnerValue] return [response, setInnerValue];
} }

View File

@@ -1,9 +1,9 @@
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';
i18n i18n
.use(Backend) .use(Backend)
@@ -23,6 +23,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,8 +1,8 @@
interface CustomNavigator extends Navigator { interface CustomNavigator extends Navigator {
standalone?: boolean standalone?: boolean;
} }
export function isStandalone() { export function isStandalone() {
const navigator: CustomNavigator = window.navigator const navigator: CustomNavigator = window.navigator;
return 'standalone' in navigator && navigator.standalone return 'standalone' in navigator && navigator.standalone;
} }

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 = () => ( const Fallback = () => (
<Container> <Container>
<BarLoader /> <BarLoader />
</Container> </Container>
) );

View File

@@ -1,3 +1,3 @@
export function capitalize(text: string) { export function capitalize(text: string) {
return text[0].toUpperCase() + text.slice(1).toLowerCase() return text[0].toUpperCase() + text.slice(1).toLowerCase();
} }

View File

@@ -1,11 +1,7 @@
{ {
"compilerOptions": { "compilerOptions": {
"target": "es5", "target": "es5",
"lib": [ "lib": ["dom", "dom.iterable", "esnext"],
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true, "allowJs": true,
"skipLibCheck": true, "skipLibCheck": true,
"esModuleInterop": true, "esModuleInterop": true,
@@ -19,7 +15,5 @@
"noEmit": true, "noEmit": true,
"jsx": "react" "jsx": "react"
}, },
"include": [ "include": ["src"]
"src"
]
} }

View File

@@ -2,10 +2,10 @@
# yarn lockfile v1 # yarn lockfile v1
now@^16.1.2: now@^16.2.0:
version "16.1.2" version "16.2.0"
resolved "https://registry.yarnpkg.com/now/-/now-16.1.2.tgz#9c05dc83852a4ff80534fe3830b53fdf807ff974" resolved "https://registry.yarnpkg.com/now/-/now-16.2.0.tgz#5f778a335e4da2ffa1f0e3754cc6c765b6685a99"
integrity sha512-Wqbox4tfh1kglgKOOHRE49bsZoy1TMHsVSnrnEbC98LU+aXmuh853kKheyDXckqIaTGDAgs7HYV12ONgppLwUw== integrity sha512-yzPtLJj7O1ngH3uNqEN6Eq3pR4rfBvq9qYRzG+whfhhQ8NA8molLwLVTPIlCLJuPu8zQnv48bJs2+EYs+n1NOA==
prettier@^1.18.2: prettier@^1.18.2:
version "1.18.2" version "1.18.2"