1
0
mirror of https://github.com/uetchy/namae.git synced 2025-07-01 22:10:04 +09:00

feat: location based domain suggestion

This commit is contained in:
uetchy 2021-02-25 15:13:15 +09:00
parent 7fbc430949
commit 73de4dd7ca
31 changed files with 1674 additions and 1499 deletions

View File

@ -0,0 +1,30 @@
import { NowRequest, NowResponse } from '@vercel/node';
import fetch from 'cross-fetch';
import { send, sendError } from '../../../util/http';
export default async function handler(
req: NowRequest,
res: NowResponse
): Promise<void> {
const { query } = req.query;
if (!query || typeof query !== 'string') {
return sendError(res, new Error('No query given'));
}
try {
const response = await fetch(
`https://domainr.p.rapidapi.com/v2/search?query=${encodeURIComponent(
query
)}`,
{
headers: {
'X-RapidAPI-Key': process.env.DOMAINR_API_KEY,
},
}
).then((res) => res.json());
send(res, response);
} catch (err) {
sendError(res, err);
}
}

View File

@ -1,6 +1,7 @@
import { NowRequest, NowResponse } from '@vercel/node';
import 'cross-fetch';
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';
export default async function handler( export default async function handler(
req: NowRequest, req: NowRequest,

View File

@ -1,6 +1,6 @@
import { send, sendError } from '../../../util/http';
import { NowRequest, NowResponse } from '@vercel/node'; import { NowRequest, NowResponse } from '@vercel/node';
import nodeFetch from 'isomorphic-unfetch'; import nodeFetch from 'cross-fetch';
import { send, sendError } from '../../../util/http';
export default async function handler( export default async function handler(
req: NowRequest, req: NowRequest,

View File

@ -10,58 +10,57 @@
}, },
"dependencies": { "dependencies": {
"@sentry/browser": "^5.21.1", "@sentry/browser": "^5.21.1",
"cross-fetch": "^3.0.6",
"easy-peasy": "^3.3.1", "easy-peasy": "^3.3.1",
"fetch-suspense": "^1.2.2", "fetch-suspense": "^1.2.2",
"framer-motion": "^2.5.1", "framer-motion": "^2.5.1",
"i18next": ">=19.7.0", "i18next": ">=19.8.4",
"i18next-browser-languagedetector": "^6.0.1", "i18next-browser-languagedetector": "^6.0.1",
"i18next-chained-backend": "^2.0.1", "i18next-chained-backend": "^2.0.1",
"i18next-localstorage-backend": "^3.1.1", "i18next-localstorage-backend": "^3.1.2",
"i18next-xhr-backend": "^3.2.2", "i18next-xhr-backend": "^3.2.2",
"isomorphic-unfetch": "^3.0.0",
"npm-name": "^6.0.1", "npm-name": "^6.0.1",
"prop-types": "^15.7.2", "rc-tooltip": "^5.0.2",
"rc-tooltip": "^5.0.0",
"react": "^16.13.1", "react": "^16.13.1",
"react-dom": "^16.13.1", "react-dom": "^16.13.1",
"react-ga": "^3.1.1", "react-ga": "^3.3.0",
"react-helmet": "^6.0.0", "react-helmet": "^6.0.0",
"react-i18next": "11.7.2", "react-i18next": "11.8.5",
"react-icons": "^3.11.0", "react-icons": "^3.11.0",
"react-router": "^5.1.2", "react-router": "^5.1.2",
"react-router-dom": "^5.2.0", "react-router-dom": "^5.2.0",
"react-scripts": "3.4.3", "react-scripts": "3.4.3",
"react-spinners": "^0.9.0", "react-spinners": "^0.9.0",
"react-toastify": "^6.0.8", "react-toastify": "^6.2.0",
"styled-components": "^5.1.1", "styled-components": "^5.2.1",
"swr": "^0.3.0", "swr": "^0.3.2",
"validator": "^13.1.0", "validator": "^13.5.2",
"whois-json": "^2.0.4" "whois-json": "^2.0.4"
}, },
"devDependencies": { "devDependencies": {
"@sentry/cli": "^1.55.1", "@sentry/cli": "^1.61.0",
"@testing-library/jest-dom": "^5.11.3", "@testing-library/jest-dom": "^5.11.9",
"@testing-library/react": "^11.0.2", "@testing-library/react": "^11.2.3",
"@types/i18next-node-fs-backend": "^2.1.0", "@types/i18next-node-fs-backend": "^2.1.0",
"@types/jest": "26.0.13", "@types/jest": "26.0.20",
"@types/node": "^14.6.0", "@types/node": "^14.14.22",
"@types/react-dom": "^16.9.8", "@types/react-dom": "^16.9.8",
"@types/react-helmet": "^6.1.0", "@types/react-helmet": "^6.1.0",
"@types/react-router-dom": "^5.1.3", "@types/react-router-dom": "^5.1.7",
"@types/styled-components": "^5.1.2", "@types/styled-components": "^5.1.7",
"@types/validator": "^13.1.0", "@types/validator": "^13.1.3",
"@vercel/build-utils": "^2.4.2", "@vercel/build-utils": "^2.7.0",
"@vercel/node": "^1.7.4", "@vercel/node": "^1.9.0",
"codacy-coverage": "^3.4.0", "codacy-coverage": "^3.4.0",
"husky": "^4.3.0", "husky": "^4.3.8",
"i18next-node-fs-backend": "^2.1.3", "i18next-node-fs-backend": "^2.1.3",
"jest": "24.9.0", "jest": "24.9.0",
"mutationobserver-shim": "^0.3.5", "mutationobserver-shim": "^0.3.5",
"nock": "^13.0.4", "nock": "^13.0.6",
"prettier": "^2.1.1", "prettier": "^2.2.1",
"pretty-quick": "^3.0.0", "pretty-quick": "^3.1.0",
"ts-jest": "26.3.0", "ts-jest": "26.4.4",
"typescript": "^4.0.2" "typescript": "^4.1.3"
}, },
"husky": { "husky": {
"hooks": { "hooks": {

View File

@ -1,7 +1,7 @@
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 'cross-fetch';
import { TiArrowSync } from 'react-icons/ti'; import { TiArrowSync } from 'react-icons/ti';
import { motion } from 'framer-motion'; import { motion } from 'framer-motion';

View File

@ -7,9 +7,10 @@ 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 response = useFetch( const response = useFetch(
`/api/services/appstore/${term}?country=${t('countryCode')}` `/api/services/appstore/${encodeURIComponent(query)}?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 }>;
}; };

View File

@ -1,11 +1,13 @@
import React from 'react'; import React from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { SiRust } from 'react-icons/si'; import { SiRust } from 'react-icons/si';
import { normalize } from '../../../util/text';
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 normalizedQuery = normalize(query);
const lowerCase = normalizedQuery.toLowerCase();
const names = [lowerCase]; const names = [lowerCase];

View File

@ -1,17 +1,35 @@
import fetch from 'cross-fetch';
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 useSWR from 'swr';
import { Card, Repeater, DedicatedAvailability } from '../core'; import { normalize } from '../../../util/text';
import { zones } from '../../../util/zones'; import { zones } from '../../../util/zones';
import { Card, DedicatedAvailability, Repeater } from '../core';
export interface DomainrResponse {
results: {
domain: string;
host: string;
subdomain: string;
zone: string;
path: string;
registerURL: string;
}[];
}
function fetcher(url: string) {
return fetch(url, {}).then((res) => res.json());
}
const DomainCard: React.FC<{ query: string }> = ({ query }) => { const DomainCard: React.FC<{ query: string }> = ({ query }) => {
const { t } = useTranslation(); const { t } = useTranslation();
const sanitizedQuery = query const normalizedQuery = normalize(query, {
.replace(/[^0-9a-zA-Z_-]/g, '') alphanumeric: false,
.replace(/_/g, '-'); allowUnderscore: false,
const lowerCase = sanitizedQuery.toLowerCase(); });
const lowerCase = normalizedQuery.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)))
@ -23,37 +41,54 @@ const DomainCard: React.FC<{ query: string }> = ({ query }) => {
lowerCase.substring(m.index + 1) lowerCase.substring(m.index + 1)
); );
const names = [ const { data } = useSWR<DomainrResponse>(
`${lowerCase}.com`, `/api/list/domain/${encodeURIComponent(query)}`,
`${lowerCase}.org`, fetcher
`${lowerCase}.app`, );
`${lowerCase}.dev`,
`${lowerCase}.io`, const cctldSuggestions =
`${lowerCase}.sh`, data?.results
?.filter((res) => res.subdomain !== '' && res.path === '')
?.map((res) => res.domain) ?? [];
const names =
// use Set() to eliminate dupes
new Set([
...['com', 'org', 'app', 'io'].map((tld) => lowerCase + '.' + tld),
...domainHackSuggestions, ...domainHackSuggestions,
]; ...cctldSuggestions,
const moreNames = [ ]);
const moreNames = new Set([
`${lowerCase}app.com`, `${lowerCase}app.com`,
`get${lowerCase}.com`, `get${lowerCase}.com`,
`${lowerCase}.co`, ...[
`${lowerCase}.tools`, 'co',
`${lowerCase}.build`, 'dev',
`${lowerCase}.run`, 'sh',
`${lowerCase}.ai`, 'cloud',
`${lowerCase}.design`, 'tools',
`${lowerCase}.directory`, 'build',
`${lowerCase}.guru`, 'run',
`${lowerCase}.ninja`, 'ai',
`${lowerCase}.net`, 'design',
`${lowerCase}.info`, 'directory',
`${lowerCase}.biz`, 'guru',
`${lowerCase}.website`, 'ninja',
`${lowerCase}.eu`, 'info',
]; 'biz',
].map((tld) => lowerCase + '.' + tld),
]);
for (const name of moreNames) {
if (names.has(name)) {
moreNames.delete(name);
}
}
return ( return (
<Card title={t('providers.domains')}> <Card title={t('providers.domains')}>
<Repeater items={names} moreItems={moreNames}> <Repeater items={Array.from(names)} moreItems={Array.from(moreNames)}>
{(name) => ( {(name) => (
<DedicatedAvailability <DedicatedAvailability
name={name} name={name}

View File

@ -1,15 +1,16 @@
import React from 'react'; import React from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { SiFirebase } from 'react-icons/si'; import { SiFirebase } from 'react-icons/si';
import { normalize } from '../../../util/text';
import { Card, DedicatedAvailability, Repeater } from '../core'; import { Card, DedicatedAvailability, Repeater } from '../core';
const FirebaseCard: React.FC<{ query: string }> = ({ query }) => { const FirebaseCard: React.FC<{ query: string }> = ({ query }) => {
const { t } = useTranslation(); const { t } = useTranslation();
const sanitizedQuery = query const normalizedQuery = normalize(query, {
.replace(/[^0-9a-zA-Z_-]/g, '') allowUnderscore: false,
.replace(/_/g, '-'); });
const lowerCase = sanitizedQuery.toLowerCase(); const lowerCase = normalizedQuery.toLowerCase();
const names = [lowerCase]; const names = [lowerCase];

View File

@ -1,13 +1,15 @@
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 { normalize } from '../../../util/text';
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 normalizedQuery = normalize(query, {});
const lowerCase = normalizedQuery.toLowerCase();
const names = [query, `${lowerCase}-dev`, `${lowerCase}-org`]; const names = [normalizedQuery, `${lowerCase}-dev`, `${lowerCase}-org`];
const moreNames = [ const moreNames = [
`${lowerCase}hq`, `${lowerCase}hq`,
`${lowerCase}-team`, `${lowerCase}-team`,

View File

@ -7,10 +7,12 @@ 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 = `${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=${encodeURIComponent(
searchQuery
)}&per_page=${limit}`
) as { ) as {
items: Array<{ items: Array<{
full_name: string; full_name: string;

View File

@ -1,12 +1,16 @@
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 { normalize } from '../../../util/text';
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 normalizedQuery = normalize(query, {
allowUnderscore: false,
});
const lowerCase = normalizedQuery.toLowerCase();
const names = [lowerCase]; const names = [lowerCase];

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 { DiHeroku } from 'react-icons/di'; import { DiHeroku } from 'react-icons/di';
import { normalize } from '../../../util/text';
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 normalizedQuery = normalize(query, {
const sanitizedQuery = query allowUnderscore: false,
.replace(/[^0-9a-zA-Z_-]/g, '') });
.replace(/_/g, '-'); const lowerCase = normalizedQuery.toLowerCase();
const lowerCase = sanitizedQuery.toLowerCase();
const names = [lowerCase]; const names = [lowerCase];

View File

@ -1,12 +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 { normalize } from '../../../util/text';
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 normalizedQuery = normalize(query);
const lowerCase = normalizedQuery.toLowerCase();
const names = [lowerCase]; const names = [lowerCase];

View File

@ -1,14 +1,16 @@
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 { normalize } from '../../../util/text';
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 normalizedQuery = normalize(query, { allowHyphens: false });
const lowerCase = normalizedQuery.toLowerCase();
const names = [query]; const names = [normalizedQuery];
const moreNames = [`${lowerCase}app`, `${lowerCase}_hq`, `get.${lowerCase}`]; const moreNames = [`${lowerCase}app`, `${lowerCase}_hq`, `get.${lowerCase}`];
return ( return (

View File

@ -1,17 +1,17 @@
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 { normalize } from '../../../util/text';
import { Card, Repeater, DedicatedAvailability } from '../core'; import { Card, Repeater, DedicatedAvailability } from '../core';
const JsOrgCard: React.FC<{ query: string }> = ({ query }) => { const JsOrgCard: React.FC<{ query: string }> = ({ query }) => {
const { t } = useTranslation(); const { t } = useTranslation();
const sanitizedQuery = query const normalizedQuery = normalize(query, {
.replace(/[^0-9a-zA-Z_-]/g, '') allowUnderscore: false,
.replace(/_/g, '-'); });
const lowerCase = sanitizedQuery.toLowerCase(); const lowerCase = normalizedQuery.toLowerCase();
const names = [lowerCase]; const names = [lowerCase];
return ( return (

View File

@ -1,11 +1,13 @@
import React from 'react'; import React from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { SiDebian, SiUbuntu } from 'react-icons/si'; import { SiDebian, SiUbuntu } from 'react-icons/si';
import { normalize } from '../../../util/text';
import { Card, DedicatedAvailability, Repeater } from '../core'; import { Card, DedicatedAvailability, Repeater } 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 normalizedQuery = normalize(query);
const lowerCase = normalizedQuery.toLowerCase();
const names = [lowerCase]; const names = [lowerCase];

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 { normalize } from '../../../util/text';
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 normalizedQuery = normalize(query, {
const sanitizedQuery = query allowUnderscore: false,
.replace(/[^0-9a-zA-Z_-]/g, '') });
.replace(/_/g, '-'); const lowerCase = normalizedQuery.toLowerCase();
const lowerCase = sanitizedQuery.toLowerCase();
const names = [lowerCase]; const names = [lowerCase];

View File

@ -1,11 +1,15 @@
import React from 'react'; import React from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { RiNpmjsFill, RiNpmjsLine } from 'react-icons/ri'; import { RiNpmjsFill, RiNpmjsLine } from 'react-icons/ri';
import { normalize } from '../../../util/text';
import { Card, DedicatedAvailability, Repeater } from '../core'; import { Card, DedicatedAvailability, Repeater } 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 normalizedQuery = normalize(query, {
allowUnderscore: false,
});
const lowerCase = normalizedQuery.toLowerCase();
const names = [lowerCase, `${lowerCase}-js`]; const names = [lowerCase, `${lowerCase}-js`];
const moreNames = [`${lowerCase}js`]; const moreNames = [`${lowerCase}js`];

View File

@ -1,12 +1,14 @@
import React from 'react'; import React from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { normalize } from '../../../util/text';
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 normalizedQuery = normalize(query, { allowUnderscore: false });
const lowerCase = normalizedQuery.toLowerCase();
const names = [lowerCase]; const names = [lowerCase];

View File

@ -2,14 +2,15 @@ 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, normalize } 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 normalizedQuery = normalize(query);
const names = [query]; const capitalCase = capitalize(normalizedQuery);
const moreNames = [`Py${capitalize(query)}`]; const names = [normalizedQuery];
const moreNames = [`Py${capitalCase}`];
return ( return (
<Card title={t('providers.pypi')}> <Card title={t('providers.pypi')}>

View File

@ -1,13 +1,15 @@
import React from 'react'; import React from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { SiRubygems } from 'react-icons/si'; import { SiRubygems } from 'react-icons/si';
import { normalize } from '../../../util/text';
import { Card, DedicatedAvailability, Repeater } from '../core'; import { Card, DedicatedAvailability, Repeater } from '../core';
const RubyGemsCard: React.FC<{ query: string }> = ({ query }) => { const RubyGemsCard: React.FC<{ query: string }> = ({ query }) => {
const { t } = useTranslation(); const { t } = useTranslation();
const normalizedQuery = normalize(query);
const names = [query]; const lowerCase = normalizedQuery.toLowerCase();
const moreNames = [`${query.toLowerCase()}-rb`]; const names = [normalizedQuery];
const moreNames = [`${lowerCase}-rb`];
return ( return (
<Card title={t('providers.rubygems')}> <Card title={t('providers.rubygems')}>

View File

@ -1,16 +1,17 @@
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 { normalize } from '../../../util/text';
import { Card, DedicatedAvailability, Repeater } from '../core'; import { Card, DedicatedAvailability, Repeater } from '../core';
const S3Card: React.FC<{ query: string }> = ({ query }) => { const S3Card: React.FC<{ query: string }> = ({ query }) => {
const { t } = useTranslation(); const { t } = useTranslation();
const sanitizedQuery = query const normalizedQuery = normalize(query, {
.replace(/[^0-9a-zA-Z_-]/g, '') allowUnderscore: false,
.replace(/_/g, '-'); });
const lowerCase = sanitizedQuery.toLowerCase(); const lowerCase = normalizedQuery.toLowerCase();
const names = [lowerCase]; const names = [lowerCase];

View File

@ -1,16 +1,15 @@
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 { normalize } from '../../../util/text';
import { Card, DedicatedAvailability, Repeater } from '../core'; import { Card, DedicatedAvailability, Repeater } from '../core';
const SlackCard: React.FC<{ query: string }> = ({ query }) => { const SlackCard: React.FC<{ query: string }> = ({ query }) => {
const { t } = useTranslation(); const { t } = useTranslation();
const sanitizedQuery = query const normalizedQuery = normalize(query, { allowUnderscore: false });
.replace(/[^0-9a-zA-Z_-]/g, '') const lowerCase = normalizedQuery.toLowerCase();
.replace(/_/g, '-');
const lowerCase = sanitizedQuery.toLowerCase();
const names = [lowerCase]; const names = [lowerCase];

View File

@ -2,10 +2,12 @@ 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';
import { normalize } from '../../../util/text';
const SpectrumCard: React.FC<{ query: string }> = ({ query }) => { const SpectrumCard: React.FC<{ query: string }> = ({ query }) => {
const { t } = useTranslation(); const { t } = useTranslation();
const names = [query]; const normalizedQuery = normalize(query);
const names = [normalizedQuery];
return ( return (
<Card title={t('providers.spectrum')}> <Card title={t('providers.spectrum')}>

View File

@ -2,19 +2,17 @@ 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, normalize } from '../../../util/text';
import { Card, Repeater, DedicatedAvailability } from '../core'; import { Card, Repeater, DedicatedAvailability } from '../core';
const TwitterCard: React.FC<{ query: string }> = ({ query }) => { const TwitterCard: React.FC<{ query: string }> = ({ query }) => {
const { t } = useTranslation(); const { t } = useTranslation();
const sanitizedQuery = query const normalizedQuery = normalize(query, { allowHyphens: false });
.replace(/[^0-9a-zA-Z_-]/g, '') const lowerCase = normalizedQuery.toLowerCase();
.replace(/-/g, '_'); const capitalCase = capitalize(normalizedQuery);
const lowerCase = sanitizedQuery.toLowerCase();
const capitalCase = capitalize(sanitizedQuery);
const names = [sanitizedQuery, `${capitalCase}App`, `${lowerCase}hq`]; const names = [normalizedQuery, `${capitalCase}App`, `${lowerCase}hq`];
const moreNames = [ const moreNames = [
`hey${lowerCase}`, `hey${lowerCase}`,
`${capitalCase}Team`, `${capitalCase}Team`,

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 { normalize } from '../../../util/text';
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 normalizedQuery = normalize(query, {
const sanitizedQuery = query allowUnderscore: false,
.replace(/[^0-9a-zA-Z_-]/g, '') });
.replace(/_/g, '-'); const lowerCase = normalizedQuery.toLowerCase();
const lowerCase = sanitizedQuery.toLowerCase();
const names = [lowerCase]; const names = [lowerCase];

View File

@ -5,11 +5,43 @@ export function capitalize(text: string): string {
export function sanitize(text: string): string { export function sanitize(text: string): string {
return text return text
.replace(/[\s@+!#$%^&*()[\]./<>{}]/g, '') .replace(/[@+!#$%^&*()[\]./<>{}]/g, '')
.normalize('NFD') .normalize('NFD')
.replace(/[\u0300-\u036f]/g, ''); .replace(/[\u0300-\u036f]/g, '');
} }
export interface NormalizeOptions {
alphanumeric?: boolean;
allowSpaces?: boolean;
allowUnderscore?: boolean;
allowHyphens?: boolean;
}
export function normalize(
text: string,
{
alphanumeric = true,
allowSpaces = false,
allowUnderscore = true,
allowHyphens = true,
}: NormalizeOptions = {}
): string {
if (alphanumeric) {
text = text.replace(/[^0-9a-zA-Z-_\s]/g, '');
}
if (!allowUnderscore) {
text = text.replace(/_/g, '-');
}
if (!allowHyphens) {
text = text.replace(/-/g, allowUnderscore ? '_' : ' ');
}
if (!allowSpaces) {
text = text.replace(/\s/g, '');
}
text = text.replace(/[-_\s]+$/, '');
return text;
}
export function upper(word: string): string { export function upper(word: string): string {
return word.toUpperCase(); return word.toUpperCase();
} }

View File

@ -1,6 +1,6 @@
{ {
"compilerOptions": { "compilerOptions": {
"target": "es5", "target": "es2015",
"lib": ["dom", "dom.iterable", "ESNext"], "lib": ["dom", "dom.iterable", "ESNext"],
"skipLibCheck": true, "skipLibCheck": true,
"esModuleInterop": true, "esModuleInterop": true,
@ -14,7 +14,8 @@
"isolatedModules": true, "isolatedModules": true,
"noEmit": true, "noEmit": true,
"jsx": "react", "jsx": "react",
"allowJs": true "allowJs": true,
"downlevelIteration": true
}, },
"include": ["src", "types"] "include": ["src", "types"]
} }

View File

@ -1,5 +1,5 @@
import { NowResponse } from '@vercel/node'; import { NowResponse } from '@vercel/node';
import nodeFetch from 'isomorphic-unfetch'; import nodeFetch from 'cross-fetch';
export type HttpMethod = export type HttpMethod =
| 'GET' | 'GET'

2792
yarn.lock

File diff suppressed because it is too large Load Diff