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:
83
src/util/analytics.ts
Normal file
83
src/util/analytics.ts
Normal 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
25
src/util/array.ts
Normal 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
14
src/util/crip.ts
Normal 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
12
src/util/css.ts
Normal 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
22
src/util/hooks.test.tsx
Normal 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
21
src/util/hooks.ts
Normal 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
30
src/util/i18n.ts
Normal 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
5
src/util/pwa.test.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import {isStandalone} from './pwa';
|
||||
|
||||
it('recognize standalone mode', () => {
|
||||
expect(isStandalone()).toEqual(false);
|
||||
});
|
8
src/util/pwa.ts
Normal file
8
src/util/pwa.ts
Normal 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
22
src/util/suspense.tsx
Normal 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
9
src/util/text.test.ts
Normal 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
37
src/util/text.ts
Normal 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
1756
src/util/zones.ts
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user