mirror of
https://github.com/shlinkio/shlink-web-client.git
synced 2026-03-12 10:33:49 +00:00
Move some modules from src to shlink-web-component
This commit is contained in:
96
shlink-web-component/utils/helpers/hooks.ts
Normal file
96
shlink-web-component/utils/helpers/hooks.ts
Normal file
@@ -0,0 +1,96 @@
|
||||
import type { DependencyList, EffectCallback } from 'react';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { useLocation, useNavigate } from 'react-router-dom';
|
||||
import { useSwipeable as useReactSwipeable } from 'react-swipeable';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import { parseQuery, stringifyQuery } from './query';
|
||||
|
||||
const DEFAULT_DELAY = 2000;
|
||||
|
||||
export type TimeoutToggle = (initialValue?: boolean, delay?: number) => [boolean, () => void];
|
||||
|
||||
export const useTimeoutToggle = (
|
||||
setTimeout: (callback: Function, timeout: number) => number,
|
||||
clearTimeout: (timer: number) => void,
|
||||
): TimeoutToggle => (initialValue = false, delay = DEFAULT_DELAY) => {
|
||||
const [flag, setFlag] = useState<boolean>(initialValue);
|
||||
const timeout = useRef<number | undefined>(undefined);
|
||||
const callback = () => {
|
||||
setFlag(!initialValue);
|
||||
|
||||
if (timeout.current) {
|
||||
clearTimeout(timeout.current);
|
||||
}
|
||||
|
||||
timeout.current = setTimeout(() => setFlag(initialValue), delay);
|
||||
};
|
||||
|
||||
return [flag, callback];
|
||||
};
|
||||
|
||||
type ToggleResult = [boolean, () => void, () => void, () => void];
|
||||
|
||||
export const useToggle = (initialValue = false): ToggleResult => {
|
||||
const [flag, setFlag] = useState<boolean>(initialValue);
|
||||
return [flag, () => setFlag(!flag), () => setFlag(true), () => setFlag(false)];
|
||||
};
|
||||
|
||||
export const useSwipeable = (showSidebar: () => void, hideSidebar: () => void) => {
|
||||
const swipeMenuIfNoModalExists = (callback: () => void) => (e: any) => {
|
||||
const swippedOnVisitsTable = (e.event.composedPath() as HTMLElement[]).some(
|
||||
({ classList }) => classList?.contains('visits-table'),
|
||||
);
|
||||
|
||||
if (swippedOnVisitsTable || document.querySelector('.modal')) {
|
||||
return;
|
||||
}
|
||||
|
||||
callback();
|
||||
};
|
||||
|
||||
return useReactSwipeable({
|
||||
delta: 40,
|
||||
onSwipedLeft: swipeMenuIfNoModalExists(hideSidebar),
|
||||
onSwipedRight: swipeMenuIfNoModalExists(showSidebar),
|
||||
});
|
||||
};
|
||||
|
||||
export const useQueryState = <T>(paramName: string, initialState: T): [ T, (newValue: T) => void ] => {
|
||||
const [value, setValue] = useState(initialState);
|
||||
const setValueWithLocation = (valueToSet: T) => {
|
||||
const { location, history } = window;
|
||||
const query = parseQuery<any>(location.search);
|
||||
|
||||
query[paramName] = valueToSet;
|
||||
history.pushState(null, '', `${location.pathname}?${stringifyQuery(query)}`);
|
||||
setValue(valueToSet);
|
||||
};
|
||||
|
||||
return [value, setValueWithLocation];
|
||||
};
|
||||
|
||||
export const useEffectExceptFirstTime = (callback: EffectCallback, deps: DependencyList): void => {
|
||||
const isFirstLoad = useRef(true);
|
||||
|
||||
useEffect(() => {
|
||||
!isFirstLoad.current && callback();
|
||||
isFirstLoad.current = false;
|
||||
}, deps);
|
||||
};
|
||||
|
||||
export const useGoBack = () => {
|
||||
const navigate = useNavigate();
|
||||
return () => navigate(-1);
|
||||
};
|
||||
|
||||
export const useParsedQuery = <T>(): T => {
|
||||
const { search } = useLocation();
|
||||
return parseQuery<T>(search);
|
||||
};
|
||||
|
||||
export const useDomId = (): string => {
|
||||
const { current: id } = useRef(`dom-${uuid()}`);
|
||||
return id;
|
||||
};
|
||||
|
||||
export const useElementRef = <T>() => useRef<T | null>(null);
|
||||
5
shlink-web-component/utils/helpers/query.ts
Normal file
5
shlink-web-component/utils/helpers/query.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import qs from 'qs';
|
||||
|
||||
export const parseQuery = <T>(search: string) => qs.parse(search, { ignoreQueryPrefix: true }) as unknown as T;
|
||||
|
||||
export const stringifyQuery = (query: any): string => qs.stringify(query, { arrayFormat: 'brackets' });
|
||||
59
shlink-web-component/utils/services/ColorGenerator.ts
Normal file
59
shlink-web-component/utils/services/ColorGenerator.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import { isNil } from 'ramda';
|
||||
import type { LocalStorage } from '../../../src/utils/services/LocalStorage';
|
||||
import { rangeOf } from '../../../src/utils/utils';
|
||||
|
||||
const HEX_COLOR_LENGTH = 6;
|
||||
const HEX_DIGITS = '0123456789ABCDEF';
|
||||
const LIGHTNESS_BREAKPOINT = 128;
|
||||
|
||||
const { floor, random, sqrt, round } = Math;
|
||||
const buildRandomColor = () =>
|
||||
`#${rangeOf(HEX_COLOR_LENGTH, () => HEX_DIGITS[floor(random() * HEX_DIGITS.length)]).join('')}`;
|
||||
const normalizeKey = (key: string) => key.toLowerCase().trim();
|
||||
const hexColorToRgbArray = (colorHex: string): number[] =>
|
||||
(colorHex.match(/../g) ?? []).map((hex) => parseInt(hex, 16) || 0);
|
||||
// HSP by Darel Rex Finley https://alienryderflex.com/hsp.html
|
||||
const perceivedLightness = (r = 0, g = 0, b = 0) => round(sqrt(0.299 * r ** 2 + 0.587 * g ** 2 + 0.114 * b ** 2));
|
||||
|
||||
export class ColorGenerator {
|
||||
private readonly colors: Record<string, string>;
|
||||
private readonly lights: Record<string, boolean>;
|
||||
|
||||
public constructor(private readonly storage: LocalStorage) {
|
||||
this.colors = this.storage.get<Record<string, string>>('colors') ?? {};
|
||||
this.lights = {};
|
||||
}
|
||||
|
||||
public readonly getColorForKey = (key: string) => {
|
||||
const normalizedKey = normalizeKey(key);
|
||||
const color = this.colors[normalizedKey];
|
||||
|
||||
// If a color has not been set yet, generate a random one and save it
|
||||
if (!color) {
|
||||
return this.setColorForKey(normalizedKey, buildRandomColor());
|
||||
}
|
||||
|
||||
return color;
|
||||
};
|
||||
|
||||
public readonly setColorForKey = (key: string, color: string) => {
|
||||
const normalizedKey = normalizeKey(key);
|
||||
|
||||
this.colors[normalizedKey] = color;
|
||||
this.storage.set('colors', this.colors);
|
||||
|
||||
return color;
|
||||
};
|
||||
|
||||
public readonly isColorLightForKey = (key: string): boolean => {
|
||||
const colorHex = this.getColorForKey(key).substring(1);
|
||||
|
||||
if (isNil(this.lights[colorHex])) {
|
||||
const rgb = hexColorToRgbArray(colorHex);
|
||||
|
||||
this.lights[colorHex] = perceivedLightness(...rgb) >= LIGHTNESS_BREAKPOINT;
|
||||
}
|
||||
|
||||
return this.lights[colorHex];
|
||||
};
|
||||
}
|
||||
14
shlink-web-component/utils/services/LocalStorage.ts
Normal file
14
shlink-web-component/utils/services/LocalStorage.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
const PREFIX = 'shlink';
|
||||
const buildPath = (path: string) => `${PREFIX}.${path}`;
|
||||
|
||||
export class LocalStorage {
|
||||
public constructor(private readonly localStorage: Storage) {}
|
||||
|
||||
public readonly get = <T>(key: string): T | undefined => {
|
||||
const item = this.localStorage.getItem(buildPath(key));
|
||||
|
||||
return item ? JSON.parse(item) as T : undefined;
|
||||
};
|
||||
|
||||
public readonly set = (key: string, value: any) => this.localStorage.setItem(buildPath(key), JSON.stringify(value));
|
||||
}
|
||||
30
shlink-web-component/utils/services/ReportExporter.ts
Normal file
30
shlink-web-component/utils/services/ReportExporter.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import type { JsonToCsv } from '../../../src/utils/helpers/csvjson';
|
||||
import { saveCsv } from '../../../src/utils/helpers/files';
|
||||
import type { ExportableShortUrl } from '../../short-urls/data';
|
||||
import type { NormalizedVisit } from '../../visits/types';
|
||||
|
||||
export class ReportExporter {
|
||||
public constructor(private readonly window: Window, private readonly jsonToCsv: JsonToCsv) {
|
||||
}
|
||||
|
||||
public readonly exportVisits = (filename: string, visits: NormalizedVisit[]) => {
|
||||
if (!visits.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.exportCsv(filename, visits);
|
||||
};
|
||||
|
||||
public readonly exportShortUrls = (shortUrls: ExportableShortUrl[]) => {
|
||||
if (!shortUrls.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.exportCsv('short_urls.csv', shortUrls);
|
||||
};
|
||||
|
||||
private readonly exportCsv = (filename: string, rows: object[]) => {
|
||||
const csv = this.jsonToCsv(rows);
|
||||
saveCsv(this.window, csv, filename);
|
||||
};
|
||||
}
|
||||
14
shlink-web-component/utils/services/provideServices.ts
Normal file
14
shlink-web-component/utils/services/provideServices.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import type Bottle from 'bottlejs';
|
||||
import { useTimeoutToggle } from '../helpers/hooks';
|
||||
import { ColorGenerator } from './ColorGenerator';
|
||||
import { LocalStorage } from './LocalStorage';
|
||||
|
||||
export function provideServices(bottle: Bottle) {
|
||||
bottle.constant('localStorage', window.localStorage);
|
||||
bottle.service('Storage', LocalStorage, 'localStorage');
|
||||
bottle.service('ColorGenerator', ColorGenerator, 'Storage');
|
||||
|
||||
bottle.constant('setTimeout', window.setTimeout);
|
||||
bottle.constant('clearTimeout', window.clearTimeout);
|
||||
bottle.serviceFactory('useTimeoutToggle', useTimeoutToggle, 'setTimeout', 'clearTimeout');
|
||||
}
|
||||
Reference in New Issue
Block a user