Move some modules from src to shlink-web-component

This commit is contained in:
Alejandro Celaya
2023-07-27 22:23:46 +02:00
parent 0169060de7
commit 275745fd3a
51 changed files with 212 additions and 133 deletions

View File

@@ -4,10 +4,10 @@ import classNames from 'classnames';
import type { FC } from 'react';
import { useEffect } from 'react';
import { Navigate, Route, Routes, useLocation } from 'react-router-dom';
import { AsideMenu } from '../src/common/AsideMenu';
import { NotFound } from '../src/common/NotFound';
import { useSwipeable, useToggle } from '../src/utils/helpers/hooks';
import { AsideMenu } from './common/AsideMenu';
import { useFeature } from './utils/features';
import { useSwipeable, useToggle } from './utils/helpers/hooks';
import { useRoutesPrefix } from './utils/routesPrefix';
export const Main = (

View File

@@ -0,0 +1,63 @@
@import '../../src/utils/base';
@import '../../src/utils/mixins/vertical-align';
.aside-menu {
width: $asideMenuWidth;
background-color: var(--primary-color);
box-shadow: rgb(0 0 0 / .05) 0 8px 15px;
position: fixed !important;
padding-top: 13px;
padding-bottom: 10px;
top: $headerHeight;
bottom: 0;
left: 0;
display: block;
z-index: 1010;
overflow-x: hidden;
overflow-y: auto;
@media (min-width: $mdMin) {
padding: 30px 15px 15px;
}
@media (max-width: $smMax) {
transition: left 300ms;
top: $headerHeight - 3px;
box-shadow: -10px 0 50px 11px rgb(0 0 0 / .55);
}
}
.aside-menu--hidden {
@media (max-width: $smMax) {
left: -($asideMenuWidth + 35px);
}
}
.aside-menu__nav {
height: 100%;
}
.aside-menu__item {
padding: 10px 20px;
margin: 0 -15px;
text-decoration: none !important;
cursor: pointer;
@media (max-width: $smMax) {
margin: 0;
}
}
.aside-menu__item:hover {
background-color: var(--secondary-color);
}
.aside-menu__item--selected,
.aside-menu__item--selected:hover {
color: #ffffff;
background-color: var(--brand-color);
}
.aside-menu__item-text {
margin-left: 8px;
}

View File

@@ -0,0 +1,71 @@
import {
faGlobe as domainsIcon,
faHome as overviewIcon,
faLink as createIcon,
faList as listIcon,
faTags as tagsIcon,
} from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import classNames from 'classnames';
import type { FC } from 'react';
import type { NavLinkProps } from 'react-router-dom';
import { NavLink, useLocation } from 'react-router-dom';
import './AsideMenu.scss';
export interface AsideMenuProps {
routePrefix: string;
showOnMobile?: boolean;
}
interface AsideMenuItemProps extends NavLinkProps {
to: string;
className?: string;
}
const AsideMenuItem: FC<AsideMenuItemProps> = ({ children, to, className, ...rest }) => (
<NavLink
className={({ isActive }) => classNames('aside-menu__item', className, { 'aside-menu__item--selected': isActive })}
to={to}
{...rest}
>
{children}
</NavLink>
);
export const AsideMenu: FC<AsideMenuProps> = ({ routePrefix, showOnMobile = false }) => {
const { pathname } = useLocation();
const asideClass = classNames('aside-menu', {
'aside-menu--hidden': !showOnMobile,
});
const buildPath = (suffix: string) => `${routePrefix}${suffix}`;
return (
<aside className={asideClass}>
<nav className="nav flex-column aside-menu__nav">
<AsideMenuItem to={buildPath('/overview')}>
<FontAwesomeIcon fixedWidth icon={overviewIcon} />
<span className="aside-menu__item-text">Overview</span>
</AsideMenuItem>
<AsideMenuItem
to={buildPath('/list-short-urls/1')}
className={classNames({ 'aside-menu__item--selected': pathname.match('/list-short-urls') !== null })}
>
<FontAwesomeIcon fixedWidth icon={listIcon} />
<span className="aside-menu__item-text">List short URLs</span>
</AsideMenuItem>
<AsideMenuItem to={buildPath('/create-short-url')}>
<FontAwesomeIcon fixedWidth icon={createIcon} flip="horizontal" />
<span className="aside-menu__item-text">Create short URL</span>
</AsideMenuItem>
<AsideMenuItem to={buildPath('/manage-tags')}>
<FontAwesomeIcon fixedWidth icon={tagsIcon} />
<span className="aside-menu__item-text">Manage tags</span>
</AsideMenuItem>
<AsideMenuItem to={buildPath('/manage-domains')}>
<FontAwesomeIcon fixedWidth icon={domainsIcon} />
<span className="aside-menu__item-text">Manage domains</span>
</AsideMenuItem>
</nav>
</aside>
);
};

View File

@@ -1,19 +1,17 @@
import type { IContainer } from 'bottlejs';
import Bottle from 'bottlejs';
import { pick } from 'ramda';
import { connect as reduxConnect } from 'react-redux/es/exports';
import { connect as reduxConnect } from 'react-redux';
import { HttpClient } from '../../src/common/services/HttpClient';
import { ImageDownloader } from '../../src/common/services/ImageDownloader';
import { ReportExporter } from '../../src/common/services/ReportExporter';
import { csvToJson, jsonToCsv } from '../../src/utils/helpers/csvjson';
import { useTimeoutToggle } from '../../src/utils/helpers/hooks';
import { ColorGenerator } from '../../src/utils/services/ColorGenerator';
import { LocalStorage } from '../../src/utils/services/LocalStorage';
import { provideServices as provideDomainsServices } from '../domains/services/provideServices';
import { provideServices as provideMercureServices } from '../mercure/services/provideServices';
import { provideServices as provideOverviewServices } from '../overview/services/provideServices';
import { provideServices as provideShortUrlsServices } from '../short-urls/services/provideServices';
import { provideServices as provideTagsServices } from '../tags/services/provideServices';
import { provideServices as provideUtilsServices } from '../utils/services/provideServices';
import { ReportExporter } from '../utils/services/ReportExporter';
import { provideServices as provideVisitsServices } from '../visits/services/provideServices';
import { provideServices as provideWebComponentServices } from './provideServices';
@@ -38,15 +36,16 @@ const connect: ConnectDecorator = (propsFromState: string[] | null, actionServic
actionServiceNames.reduce(mapActionService, {}),
);
provideWebComponentServices(bottle, connect);
provideWebComponentServices(bottle);
provideShortUrlsServices(bottle, connect);
provideTagsServices(bottle, connect);
provideVisitsServices(bottle, connect);
provideMercureServices(bottle);
provideDomainsServices(bottle, connect);
provideOverviewServices(bottle, connect);
provideUtilsServices(bottle);
// TODO Check which of these can be moved to shlink-web-component, and which are needed by the app too
// FIXME Check which of these can be moved to shlink-web-component, and which are needed by the app too
bottle.constant('window', window);
bottle.constant('console', console);
bottle.constant('fetch', window.fetch.bind(window));
@@ -55,13 +54,5 @@ bottle.service('HttpClient', HttpClient, 'fetch');
bottle.service('ImageDownloader', ImageDownloader, 'HttpClient', 'window');
bottle.service('ReportExporter', ReportExporter, 'window', 'jsonToCsv');
bottle.constant('localStorage', window.localStorage);
bottle.service('Storage', LocalStorage, 'localStorage');
bottle.service('ColorGenerator', ColorGenerator, 'Storage');
bottle.constant('csvToJson', csvToJson);
bottle.constant('jsonToCsv', jsonToCsv);
bottle.constant('setTimeout', window.setTimeout);
bottle.constant('clearTimeout', window.clearTimeout);
bottle.serviceFactory('useTimeoutToggle', useTimeoutToggle, 'setTimeout', 'clearTimeout');

View File

@@ -5,7 +5,7 @@ import { useEffect } from 'react';
import type { InputProps } from 'reactstrap';
import { Button, DropdownItem, Input, InputGroup, UncontrolledTooltip } from 'reactstrap';
import { DropdownBtn } from '../../src/utils/DropdownBtn';
import { useToggle } from '../../src/utils/helpers/hooks';
import { useToggle } from '../utils/helpers/hooks';
import type { DomainsList } from './reducers/domainsList';
import './DomainSelector.scss';

View File

@@ -3,9 +3,9 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import type { FC } from 'react';
import { Link } from 'react-router-dom';
import { DropdownItem } from 'reactstrap';
import { useToggle } from '../../../src/utils/helpers/hooks';
import { RowDropdownBtn } from '../../../src/utils/RowDropdownBtn';
import { useFeature } from '../../utils/features';
import { useToggle } from '../../utils/helpers/hooks';
import { useRoutesPrefix } from '../../utils/routesPrefix';
import { DEFAULT_DOMAIN } from '../../visits/reducers/domainVisits';
import type { Domain } from '../data';

View File

@@ -7,9 +7,9 @@ import { useLocation, useParams } from 'react-router-dom';
import { Button, Card } from 'reactstrap';
import { ShlinkApiError } from '../../src/api/ShlinkApiError';
import { useGoBack } from '../../src/utils/helpers/hooks';
import { parseQuery } from '../../src/utils/helpers/query';
import { Message } from '../../src/utils/Message';
import { Result } from '../../src/utils/Result';
import { parseQuery } from '../utils/helpers/query';
import { useSetting } from '../utils/settings';
import type { ShortUrlIdentifier } from './data';
import { shortUrlDataFromShortUrl, urlDecodeShortCode } from './helpers';

View File

@@ -1,7 +1,7 @@
import { faInfoCircle as infoIcon } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { Modal, ModalBody, ModalHeader } from 'reactstrap';
import { useToggle } from '../../src/utils/helpers/hooks';
import { useToggle } from '../utils/helpers/hooks';
import './UseExistingIfFoundInfoIcon.scss';
const InfoModal = ({ isOpen, toggle }: { isOpen: boolean; toggle: () => void }) => (

View File

@@ -1,9 +1,9 @@
import type { FC } from 'react';
import { useCallback } from 'react';
import type { ReportExporter } from '../../../src/common/services/ReportExporter';
import { ExportBtn } from '../../../src/utils/ExportBtn';
import { useToggle } from '../../../src/utils/helpers/hooks';
import type { ShlinkApiClient } from '../../api-contract';
import { useToggle } from '../../utils/helpers/hooks';
import type { ReportExporter } from '../../utils/services/ReportExporter';
import type { ShortUrl } from '../data';
import { useShortUrlsQuery } from './hooks';

View File

@@ -4,7 +4,7 @@ import { ExternalLink } from 'react-external-link';
import { CopyToClipboardIcon } from '../../../src/utils/CopyToClipboardIcon';
import { Time } from '../../../src/utils/dates/Time';
import type { TimeoutToggle } from '../../../src/utils/helpers/hooks';
import type { ColorGenerator } from '../../../src/utils/services/ColorGenerator';
import type { ColorGenerator } from '../../utils/services/ColorGenerator';
import { useSetting } from '../../utils/settings';
import type { ShortUrl } from '../data';
import { useShortUrlsQuery } from './hooks';

View File

@@ -7,8 +7,8 @@ import {
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import type { FC } from 'react';
import { DropdownItem } from 'reactstrap';
import { useToggle } from '../../../src/utils/helpers/hooks';
import { RowDropdownBtn } from '../../../src/utils/RowDropdownBtn';
import { useToggle } from '../../utils/helpers/hooks';
import type { ShortUrl, ShortUrlModalProps } from '../data';
import { ShortUrlDetailLink } from './ShortUrlDetailLink';

View File

@@ -1,7 +1,7 @@
import { isEmpty } from 'ramda';
import type { FC } from 'react';
import type { ColorGenerator } from '../../../src/utils/services/ColorGenerator';
import { Tag } from '../../tags/helpers/Tag';
import type { ColorGenerator } from '../../utils/services/ColorGenerator';
interface TagsProps {
tags: string[];

View File

@@ -2,10 +2,10 @@ import { isEmpty, pipe } from 'ramda';
import { useMemo } from 'react';
import { useLocation, useNavigate } from 'react-router-dom';
import { orderToString, stringToOrder } from '../../../src/utils/helpers/ordering';
import { parseQuery, stringifyQuery } from '../../../src/utils/helpers/query';
import type { BooleanString } from '../../../src/utils/utils';
import { parseOptionalBooleanToString } from '../../../src/utils/utils';
import type { TagsFilteringMode } from '../../api-contract';
import { parseQuery, stringifyQuery } from '../../utils/helpers/query';
import { useRoutesPrefix } from '../../utils/routesPrefix';
import type { ShortUrlsOrder, ShortUrlsOrderableFields } from '../data';

View File

@@ -3,10 +3,10 @@ import type { FC } from 'react';
import { useEffect, useRef } from 'react';
import { useLocation } from 'react-router-dom';
import { SimplePaginator } from '../../src/common/SimplePaginator';
import { useQueryState } from '../../src/utils/helpers/hooks';
import { parseQuery } from '../../src/utils/helpers/query';
import { SimpleCard } from '../../src/utils/SimpleCard';
import { TableOrderIcon } from '../../src/utils/table/TableOrderIcon';
import { useQueryState } from '../utils/helpers/hooks';
import { parseQuery } from '../utils/helpers/query';
import type { TagsListChildrenProps, TagsOrder, TagsOrderableFields } from './data/TagsListChildrenProps';
import type { TagsTableRowProps } from './TagsTableRow';
import './TagsTable.scss';

View File

@@ -3,11 +3,11 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import type { FC } from 'react';
import { Link } from 'react-router-dom';
import { DropdownItem } from 'reactstrap';
import { useToggle } from '../../src/utils/helpers/hooks';
import { prettify } from '../../src/utils/helpers/numbers';
import { RowDropdownBtn } from '../../src/utils/RowDropdownBtn';
import type { ColorGenerator } from '../../src/utils/services/ColorGenerator';
import { useToggle } from '../utils/helpers/hooks';
import { useRoutesPrefix } from '../utils/routesPrefix';
import type { ColorGenerator } from '../utils/services/ColorGenerator';
import type { SimplifiedTag, TagModalProps } from './data';
import { TagBullet } from './helpers/TagBullet';

View File

@@ -5,10 +5,10 @@ import { useState } from 'react';
import { HexColorPicker } from 'react-colorful';
import { Button, Input, InputGroup, Modal, ModalBody, ModalFooter, ModalHeader, Popover } from 'reactstrap';
import { ShlinkApiError } from '../../../src/api/ShlinkApiError';
import { useToggle } from '../../../src/utils/helpers/hooks';
import { Result } from '../../../src/utils/Result';
import type { ColorGenerator } from '../../../src/utils/services/ColorGenerator';
import { handleEventPreventingDefault } from '../../../src/utils/utils';
import { useToggle } from '../../utils/helpers/hooks';
import type { ColorGenerator } from '../../utils/services/ColorGenerator';
import type { TagModalProps } from '../data';
import type { EditTag, TagEdition } from '../reducers/tagEdit';
import './EditTagModal.scss';

View File

@@ -1,6 +1,6 @@
import classNames from 'classnames';
import type { FC, MouseEventHandler, PropsWithChildren } from 'react';
import type { ColorGenerator } from '../../../src/utils/services/ColorGenerator';
import type { ColorGenerator } from '../../utils/services/ColorGenerator';
import './Tag.scss';
type TagProps = PropsWithChildren<{

View File

@@ -1,4 +1,4 @@
import type { ColorGenerator } from '../../../src/utils/services/ColorGenerator';
import type { ColorGenerator } from '../../utils/services/ColorGenerator';
import './TagBullet.scss';
interface TagBulletProps {

View File

@@ -1,7 +1,7 @@
import { useEffect } from 'react';
import type { SuggestionComponentProps, TagComponentProps } from 'react-tag-autocomplete';
import ReactTags from 'react-tag-autocomplete';
import type { ColorGenerator } from '../../../src/utils/services/ColorGenerator';
import type { ColorGenerator } from '../../utils/services/ColorGenerator';
import { useSetting } from '../../utils/settings';
import type { TagsList } from '../reducers/tagsList';
import { Tag } from './Tag';

View File

@@ -1,10 +1,10 @@
import type { PayloadAction } from '@reduxjs/toolkit';
import { createAction, createSlice } from '@reduxjs/toolkit';
import { pick } from 'ramda';
import type { ColorGenerator } from '../../../src/utils/services/ColorGenerator';
import type { ProblemDetailsError, ShlinkApiClient } from '../../api-contract';
import { parseApiError } from '../../api-contract/utils';
import { createAsyncThunk } from '../../utils/redux';
import type { ColorGenerator } from '../../utils/services/ColorGenerator';
const REDUCER_PREFIX = 'shlink/tagEdit';

View 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);

View 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' });

View 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];
};
}

View 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));
}

View 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);
};
}

View 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');
}

View File

@@ -1,9 +1,9 @@
import { useParams } from 'react-router-dom';
import type { ReportExporter } from '../../src/common/services/ReportExporter';
import { useGoBack } from '../../src/utils/helpers/hooks';
import type { ShlinkVisitsParams } from '../api-contract';
import { boundToMercureHub } from '../mercure/helpers/boundToMercureHub';
import { Topics } from '../mercure/helpers/Topics';
import type { ReportExporter } from '../utils/services/ReportExporter';
import type { DomainVisits as DomainVisitsState, LoadDomainVisits } from './reducers/domainVisits';
import type { NormalizedVisit } from './types';
import { toApiParams } from './types/helpers';

View File

@@ -1,7 +1,7 @@
import type { ReportExporter } from '../../src/common/services/ReportExporter';
import { useGoBack } from '../../src/utils/helpers/hooks';
import { boundToMercureHub } from '../mercure/helpers/boundToMercureHub';
import { Topics } from '../mercure/helpers/Topics';
import type { ReportExporter } from '../utils/services/ReportExporter';
import type { LoadVisits, VisitsInfo } from './reducers/types';
import type { NormalizedVisit, VisitsParams } from './types';
import { toApiParams } from './types/helpers';

View File

@@ -1,7 +1,7 @@
import type { ReportExporter } from '../../src/common/services/ReportExporter';
import { useGoBack } from '../../src/utils/helpers/hooks';
import { boundToMercureHub } from '../mercure/helpers/boundToMercureHub';
import { Topics } from '../mercure/helpers/Topics';
import type { ReportExporter } from '../utils/services/ReportExporter';
import type { LoadOrphanVisits } from './reducers/orphanVisits';
import type { VisitsInfo } from './reducers/types';
import type { NormalizedVisit, VisitsParams } from './types';

View File

@@ -1,13 +1,13 @@
import { useEffect } from 'react';
import { useLocation, useParams } from 'react-router-dom';
import type { ReportExporter } from '../../src/common/services/ReportExporter';
import { useGoBack } from '../../src/utils/helpers/hooks';
import { parseQuery } from '../../src/utils/helpers/query';
import { boundToMercureHub } from '../mercure/helpers/boundToMercureHub';
import { Topics } from '../mercure/helpers/Topics';
import type { ShortUrlIdentifier } from '../short-urls/data';
import { urlDecodeShortCode } from '../short-urls/helpers';
import type { ShortUrlDetail } from '../short-urls/reducers/shortUrlDetail';
import { parseQuery } from '../utils/helpers/query';
import type { ReportExporter } from '../utils/services/ReportExporter';
import type { LoadShortUrlVisits, ShortUrlVisits as ShortUrlVisitsState } from './reducers/shortUrlVisits';
import { ShortUrlVisitsHeader } from './ShortUrlVisitsHeader';
import type { NormalizedVisit, VisitsParams } from './types';

View File

@@ -1,10 +1,10 @@
import { useParams } from 'react-router-dom';
import type { ShlinkVisitsParams } from '../../api/types';
import type { ReportExporter } from '../../src/common/services/ReportExporter';
import { useGoBack } from '../../src/utils/helpers/hooks';
import type { ColorGenerator } from '../../src/utils/services/ColorGenerator';
import type { ShlinkVisitsParams } from '../api-contract';
import { boundToMercureHub } from '../mercure/helpers/boundToMercureHub';
import { Topics } from '../mercure/helpers/Topics';
import type { ColorGenerator } from '../utils/services/ColorGenerator';
import type { ReportExporter } from '../utils/services/ReportExporter';
import type { LoadTagVisits, TagVisits as TagVisitsState } from './reducers/tagVisits';
import { TagVisitsHeader } from './TagVisitsHeader';
import type { NormalizedVisit } from './types';

View File

@@ -1,5 +1,5 @@
import type { ColorGenerator } from '../../src/utils/services/ColorGenerator';
import { Tag } from '../tags/helpers/Tag';
import type { ColorGenerator } from '../utils/services/ColorGenerator';
import type { TagVisits } from './reducers/tagVisits';
import { VisitsHeader } from './VisitsHeader';
import './ShortUrlVisitsHeader.scss';

View File

@@ -25,11 +25,11 @@ import {
} from 'reactstrap';
import { pointerOnHover, renderChartLabel } from '../../../src/utils/helpers/charts';
import { STANDARD_DATE_FORMAT } from '../../../src/utils/helpers/date';
import { useToggle } from '../../../src/utils/helpers/hooks';
import { prettify } from '../../../src/utils/helpers/numbers';
import { HIGHLIGHTED_COLOR, MAIN_COLOR } from '../../../src/utils/theme';
import { ToggleSwitch } from '../../../src/utils/ToggleSwitch';
import { rangeOf } from '../../../src/utils/utils';
import { useToggle } from '../../utils/helpers/hooks';
import type { NormalizedVisit, Stats } from '../types';
import { fillTheGaps } from '../utils';
import './LineChartCard.scss';

View File

@@ -2,7 +2,7 @@ import { faMapMarkedAlt as mapIcon } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { useState } from 'react';
import { Button, Dropdown, DropdownItem, DropdownMenu, UncontrolledTooltip } from 'reactstrap';
import { useDomId, useToggle } from '../../../src/utils/helpers/hooks';
import { useDomId, useToggle } from '../../utils/helpers/hooks';
import type { CityStats } from '../types';
import { MapModal } from './MapModal';
import './OpenMapModalBtn.scss';

View File

@@ -5,9 +5,9 @@ import { useLocation, useNavigate } from 'react-router-dom';
import { formatIsoDate } from '../../../src/utils/helpers/date';
import type { DateRange } from '../../../src/utils/helpers/dateIntervals';
import { datesToDateRange } from '../../../src/utils/helpers/dateIntervals';
import { parseQuery, stringifyQuery } from '../../../src/utils/helpers/query';
import type { BooleanString } from '../../../src/utils/utils';
import { parseBooleanToString } from '../../../src/utils/utils';
import { parseQuery, stringifyQuery } from '../../utils/helpers/query';
import type { OrphanVisitType, VisitsFilter } from '../types';
interface VisitsQuery {