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

fix: support new vercel style

This commit is contained in:
2020-06-19 16:15:53 +09:00
parent a5f9074303
commit f95f8ef551
106 changed files with 4523 additions and 1390 deletions

83
src/util/analytics.ts Normal file
View File

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

25
src/util/array.ts Normal file
View File

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

14
src/util/crip.ts Normal file
View File

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

12
src/util/css.ts Normal file
View File

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

22
src/util/hooks.test.tsx Normal file
View File

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

21
src/util/hooks.ts Normal file
View File

@@ -0,0 +1,21 @@
import {useState, useEffect} from 'react';
export function useDeferredState<T>(
duration = 1000,
initialValue: T,
): [T, React.Dispatch<React.SetStateAction<T>>] {
const [response, setResponse] = useState(initialValue);
const [innerValue, setInnerValue] = useState(initialValue);
useEffect(() => {
const fn = setTimeout(() => {
setResponse(innerValue);
}, duration);
return (): void => {
clearTimeout(fn);
};
}, [duration, innerValue]);
return [response, setInnerValue];
}

30
src/util/i18n.ts Normal file
View File

@@ -0,0 +1,30 @@
import i18n from 'i18next';
import Backend from 'i18next-chained-backend';
import LocalStorageBackend from 'i18next-localstorage-backend';
import XHR from 'i18next-xhr-backend';
import LanguageDetector from 'i18next-browser-languagedetector';
import {initReactI18next} from 'react-i18next';
const TRANSLATION_VERSION = '1.16';
i18n
.use(Backend)
.use(LanguageDetector)
.use(initReactI18next)
.init({
backend: {
backends: [LocalStorageBackend, XHR],
backendOptions: [
{
versions: {en: TRANSLATION_VERSION, ja: TRANSLATION_VERSION},
},
],
},
fallbackLng: 'en',
debug: false,
interpolation: {
escapeValue: false, // not needed for react as it escapes by default
},
});
export default i18n;

5
src/util/pwa.test.ts Normal file
View File

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

8
src/util/pwa.ts Normal file
View File

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

22
src/util/suspense.tsx Normal file
View File

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

9
src/util/text.test.ts Normal file
View File

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

37
src/util/text.ts Normal file
View File

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

1756
src/util/zones.ts Normal file

File diff suppressed because it is too large Load Diff