mirror of
https://github.com/shlinkio/shlink-web-client.git
synced 2026-05-30 17:16:17 +00:00
First shlink-frontend-kit iteration
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import { isEmpty, isNil, reject } from 'ramda';
|
||||
import { orderToString } from '../../../shlink-frontend-kit/src';
|
||||
import type {
|
||||
ShlinkApiClient as BaseShlinkApiClient,
|
||||
ShlinkDomainRedirects,
|
||||
@@ -20,7 +21,6 @@ import { isRegularNotFound, parseApiError } from '../../../shlink-web-component/
|
||||
import type { ShortUrl, ShortUrlData } from '../../../shlink-web-component/short-urls/data';
|
||||
import { stringifyQuery } from '../../../shlink-web-component/utils/helpers/query';
|
||||
import type { HttpClient } from '../../common/services/HttpClient';
|
||||
import { orderToString } from '../../utils/helpers/ordering';
|
||||
import { replaceAuthorityFromUri } from '../../utils/helpers/uri';
|
||||
import type { OptionalString } from '../../utils/utils';
|
||||
|
||||
|
||||
@@ -2,8 +2,7 @@ import { faSyncAlt as reloadIcon } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import type { FC, MouseEventHandler } from 'react';
|
||||
import { Alert, Button } from 'reactstrap';
|
||||
import { useToggle } from '../utils/helpers/hooks';
|
||||
import { SimpleCard } from '../utils/SimpleCard';
|
||||
import { SimpleCard, useToggle } from '../../shlink-frontend-kit/src';
|
||||
import './AppUpdateBanner.scss';
|
||||
|
||||
interface AppUpdateBannerProps {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { ReactNode } from 'react';
|
||||
import { Component } from 'react';
|
||||
import { Button } from 'reactstrap';
|
||||
import { SimpleCard } from '../utils/SimpleCard';
|
||||
import { SimpleCard } from '../../shlink-frontend-kit/src';
|
||||
|
||||
interface ErrorHandlerState {
|
||||
hasError: boolean;
|
||||
|
||||
@@ -5,7 +5,7 @@ import type { FC } from 'react';
|
||||
import { useEffect } from 'react';
|
||||
import { Link, useLocation } from 'react-router-dom';
|
||||
import { Collapse, Nav, Navbar, NavbarBrand, NavbarToggler, NavItem, NavLink } from 'reactstrap';
|
||||
import { useToggle } from '../utils/helpers/hooks';
|
||||
import { useToggle } from '../../shlink-frontend-kit/src';
|
||||
import { ShlinkLogo } from './img/ShlinkLogo';
|
||||
import './MainHeader.scss';
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { FC, PropsWithChildren } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { SimpleCard } from '../utils/SimpleCard';
|
||||
import { SimpleCard } from '../../shlink-frontend-kit/src';
|
||||
|
||||
type NotFoundProps = PropsWithChildren<{ to?: string }>;
|
||||
|
||||
|
||||
@@ -3,10 +3,10 @@ import { useEffect, useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { Button } from 'reactstrap';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import { Result, useToggle } from '../../shlink-frontend-kit/src';
|
||||
import { NoMenuLayout } from '../common/NoMenuLayout';
|
||||
import type { TimeoutToggle } from '../utils/helpers/hooks';
|
||||
import { useGoBack, useToggle } from '../utils/helpers/hooks';
|
||||
import { Result } from '../utils/Result';
|
||||
import { useGoBack } from '../utils/helpers/hooks';
|
||||
import type { ServerData, ServersMap, ServerWithId } from './data';
|
||||
import { DuplicatedServersModal } from './helpers/DuplicatedServersModal';
|
||||
import type { ImportServersBtnProps } from './helpers/ImportServersBtn';
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { faMinusCircle as deleteIcon } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import type { FC, PropsWithChildren } from 'react';
|
||||
import { useToggle } from '../utils/helpers/hooks';
|
||||
import { useToggle } from '../../shlink-frontend-kit/src';
|
||||
import type { ServerWithId } from './data';
|
||||
import type { DeleteServerModalProps } from './DeleteServerModal';
|
||||
|
||||
|
||||
@@ -4,11 +4,9 @@ import type { FC } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Button, Row } from 'reactstrap';
|
||||
import { Result, SearchField, SimpleCard } from '../../shlink-frontend-kit/src';
|
||||
import { NoMenuLayout } from '../common/NoMenuLayout';
|
||||
import type { TimeoutToggle } from '../utils/helpers/hooks';
|
||||
import { Result } from '../utils/Result';
|
||||
import { SearchField } from '../utils/SearchField';
|
||||
import { SimpleCard } from '../utils/SimpleCard';
|
||||
import type { ServersMap } from './data';
|
||||
import type { ImportServersBtnProps } from './helpers/ImportServersBtn';
|
||||
import type { ManageServersRowProps } from './ManageServersRow';
|
||||
|
||||
@@ -9,8 +9,7 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import type { FC } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { DropdownItem } from 'reactstrap';
|
||||
import { useToggle } from '../utils/helpers/hooks';
|
||||
import { RowDropdownBtn } from '../utils/RowDropdownBtn';
|
||||
import { RowDropdownBtn, useToggle } from '../../shlink-frontend-kit/src';
|
||||
import type { ServerWithId } from './data';
|
||||
import type { DeleteServerModalProps } from './DeleteServerModal';
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import { complement, pipe } from 'ramda';
|
||||
import type { ChangeEvent, FC, PropsWithChildren } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Button, UncontrolledTooltip } from 'reactstrap';
|
||||
import { useElementRef, useToggle } from '../../utils/helpers/hooks';
|
||||
import { useElementRef, useToggle } from '../../../shlink-frontend-kit/src';
|
||||
import type { ServerData, ServersMap } from '../data';
|
||||
import type { ServersImporter } from '../services/ServersImporter';
|
||||
import { DuplicatedServersModal } from './DuplicatedServersModal';
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { FC } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Message } from '../../../shlink-frontend-kit/src';
|
||||
import { NoMenuLayout } from '../../common/NoMenuLayout';
|
||||
import { Message } from '../../utils/Message';
|
||||
import type { SelectedServer, ServersMap } from '../data';
|
||||
import { isServerWithId } from '../data';
|
||||
import type { DeleteServerButtonProps } from '../DeleteServerButton';
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import type { FC, PropsWithChildren, ReactNode } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { InputFormGroup } from '../../utils/forms/InputFormGroup';
|
||||
import { SimpleCard } from '../../utils/SimpleCard';
|
||||
import { InputFormGroup, SimpleCard } from '../../../shlink-frontend-kit/src';
|
||||
import { handleEventPreventingDefault } from '../../utils/utils';
|
||||
import type { ServerData } from '../data';
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import type { FC } from 'react';
|
||||
import { useEffect } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { Message } from '../../../shlink-frontend-kit/src';
|
||||
import { NoMenuLayout } from '../../common/NoMenuLayout';
|
||||
import { Message } from '../../utils/Message';
|
||||
import type { SelectedServer } from '../data';
|
||||
import { isNotFoundServer } from '../data';
|
||||
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
import classNames from 'classnames';
|
||||
import { FormGroup, Input } from 'reactstrap';
|
||||
import { LabeledFormGroup, SimpleCard, ToggleSwitch, useDomId } from '../../shlink-frontend-kit/src';
|
||||
import type { Settings } from '../../shlink-web-component';
|
||||
import { FormText } from '../utils/forms/FormText';
|
||||
import { LabeledFormGroup } from '../utils/forms/LabeledFormGroup';
|
||||
import { useDomId } from '../utils/helpers/hooks';
|
||||
import { SimpleCard } from '../utils/SimpleCard';
|
||||
import { ToggleSwitch } from '../utils/ToggleSwitch';
|
||||
|
||||
type RealTimeUpdatesProps = {
|
||||
settings: Settings;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { FC, ReactNode } from 'react';
|
||||
import { Navigate, Route, Routes } from 'react-router-dom';
|
||||
import { NavPillItem, NavPills } from '../../shlink-frontend-kit/src';
|
||||
import { NoMenuLayout } from '../common/NoMenuLayout';
|
||||
import { NavPillItem, NavPills } from '../utils/NavPills';
|
||||
|
||||
const SettingsSections: FC<{ items: ReactNode[] }> = ({ items }) => (
|
||||
<>
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import type { FC, ReactNode } from 'react';
|
||||
import { DropdownItem, FormGroup } from 'reactstrap';
|
||||
import { DropdownBtn } from '../utils/DropdownBtn';
|
||||
import { DropdownBtn, LabeledFormGroup, SimpleCard, ToggleSwitch } from '../../shlink-frontend-kit/src';
|
||||
import type { Settings } from '../../shlink-web-component';
|
||||
import { FormText } from '../utils/forms/FormText';
|
||||
import { LabeledFormGroup } from '../utils/forms/LabeledFormGroup';
|
||||
import { SimpleCard } from '../utils/SimpleCard';
|
||||
import { ToggleSwitch } from '../utils/ToggleSwitch';
|
||||
import type { Settings, ShortUrlCreationSettings as ShortUrlsSettings, TagFilteringMode } from './reducers/settings';
|
||||
import type { Defined } from '../utils/types';
|
||||
|
||||
type ShortUrlsSettings = Defined<Settings['shortUrlCreation']>;
|
||||
type TagFilteringMode = Defined<ShortUrlsSettings['tagFilteringMode']>;
|
||||
|
||||
interface ShortUrlCreationProps {
|
||||
settings: Settings;
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import type { FC } from 'react';
|
||||
import { LabeledFormGroup, OrderingDropdown, SimpleCard } from '../../shlink-frontend-kit/src';
|
||||
import type { Settings } from '../../shlink-web-component';
|
||||
import { SHORT_URLS_ORDERABLE_FIELDS } from '../../shlink-web-component/short-urls/data';
|
||||
import { LabeledFormGroup } from '../utils/forms/LabeledFormGroup';
|
||||
import { OrderingDropdown } from '../utils/OrderingDropdown';
|
||||
import { SimpleCard } from '../utils/SimpleCard';
|
||||
import type { Settings, ShortUrlsListSettings as ShortUrlsSettings } from './reducers/settings';
|
||||
import type { Defined } from '../utils/types';
|
||||
import { DEFAULT_SHORT_URLS_ORDERING } from './reducers/settings';
|
||||
|
||||
type ShortUrlsSettings = Defined<Settings['shortUrlsList']>;
|
||||
|
||||
interface ShortUrlsListSettingsProps {
|
||||
settings: Settings;
|
||||
setShortUrlsListSettings: (settings: ShortUrlsSettings) => void;
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import type { FC } from 'react';
|
||||
import { LabeledFormGroup, OrderingDropdown, SimpleCard } from '../../shlink-frontend-kit/src';
|
||||
import type { Settings } from '../../shlink-web-component';
|
||||
import { TAGS_ORDERABLE_FIELDS } from '../../shlink-web-component/tags/data/TagsListChildrenProps';
|
||||
import { LabeledFormGroup } from '../utils/forms/LabeledFormGroup';
|
||||
import { OrderingDropdown } from '../utils/OrderingDropdown';
|
||||
import { SimpleCard } from '../utils/SimpleCard';
|
||||
import type { Settings, TagsSettings as TagsSettingsOptions } from './reducers/settings';
|
||||
import type { Defined } from '../utils/types';
|
||||
|
||||
type TagsSettingsOptions = Defined<Settings['tags']>;
|
||||
|
||||
interface TagsProps {
|
||||
settings: Settings;
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
import { faMoon, faSun } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import type { FC } from 'react';
|
||||
import { SimpleCard } from '../utils/SimpleCard';
|
||||
import { SimpleCard, ToggleSwitch } from '../../shlink-frontend-kit/src';
|
||||
import type { Settings } from '../../shlink-web-component';
|
||||
import type { Theme } from '../utils/theme';
|
||||
import { changeThemeInMarkup } from '../utils/theme';
|
||||
import { ToggleSwitch } from '../utils/ToggleSwitch';
|
||||
import type { Settings, UiSettings } from './reducers/settings';
|
||||
import type { Defined } from '../utils/types';
|
||||
import './UserInterfaceSettings.scss';
|
||||
|
||||
type UiSettings = Defined<Settings['ui']>;
|
||||
|
||||
interface UserInterfaceProps {
|
||||
settings: Settings;
|
||||
setUiSettings: (settings: UiSettings) => void;
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
import type { FC } from 'react';
|
||||
import { FormGroup } from 'reactstrap';
|
||||
import { LabeledFormGroup, SimpleCard, ToggleSwitch } from '../../shlink-frontend-kit/src';
|
||||
import type { Settings } from '../../shlink-web-component';
|
||||
import type { DateInterval } from '../../shlink-web-component/utils/dates/helpers/dateIntervals';
|
||||
import { DateIntervalSelector } from '../utils/dates/DateIntervalSelector';
|
||||
import { FormText } from '../utils/forms/FormText';
|
||||
import { LabeledFormGroup } from '../utils/forms/LabeledFormGroup';
|
||||
import { SimpleCard } from '../utils/SimpleCard';
|
||||
import { ToggleSwitch } from '../utils/ToggleSwitch';
|
||||
|
||||
type VisitsSettingsConfig = Settings['visits'];
|
||||
|
||||
|
||||
@@ -2,8 +2,9 @@ import type { PayloadAction, PrepareAction } from '@reduxjs/toolkit';
|
||||
import { createSlice } from '@reduxjs/toolkit';
|
||||
import { mergeDeepRight } from 'ramda';
|
||||
import type { Settings } from '../../../shlink-web-component';
|
||||
import type { Defined } from '../../utils/types';
|
||||
|
||||
type ShortUrlsOrder = Exclude<Exclude<Settings['shortUrlsList'], undefined>['defaultOrdering'], undefined>;
|
||||
type ShortUrlsOrder = Defined<Defined<Settings['shortUrlsList']>['defaultOrdering']>;
|
||||
|
||||
export const DEFAULT_SHORT_URLS_ORDERING: ShortUrlsOrder = {
|
||||
field: 'dateCreated',
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
import classNames from 'classnames';
|
||||
import { identity } from 'ramda';
|
||||
import type { ChangeEvent, FC, PropsWithChildren } from 'react';
|
||||
import { useDomId } from './helpers/hooks';
|
||||
|
||||
export type BooleanControlProps = PropsWithChildren<{
|
||||
checked?: boolean;
|
||||
onChange?: (checked: boolean, e: ChangeEvent<HTMLInputElement>) => void;
|
||||
className?: string;
|
||||
inline?: boolean;
|
||||
}>;
|
||||
|
||||
interface BooleanControlWithTypeProps extends BooleanControlProps {
|
||||
type: 'switch' | 'checkbox';
|
||||
}
|
||||
|
||||
export const BooleanControl: FC<BooleanControlWithTypeProps> = (
|
||||
{ checked = false, onChange = identity, className, children, type, inline = false },
|
||||
) => {
|
||||
const id = useDomId();
|
||||
const onChecked = (e: ChangeEvent<HTMLInputElement>) => onChange(e.target.checked, e);
|
||||
const typeClasses = {
|
||||
'form-switch': type === 'switch',
|
||||
'form-checkbox': type === 'checkbox',
|
||||
};
|
||||
const style = inline ? { display: 'inline-block' } : {};
|
||||
|
||||
return (
|
||||
<span className={classNames('form-check', typeClasses, className)} style={style}>
|
||||
<input type="checkbox" className="form-check-input" id={id} checked={checked} onChange={onChecked} />
|
||||
<label className="form-check-label" htmlFor={id}>{children}</label>
|
||||
</span>
|
||||
);
|
||||
};
|
||||
@@ -1,5 +0,0 @@
|
||||
import type { FC } from 'react';
|
||||
import type { BooleanControlProps } from './BooleanControl';
|
||||
import { BooleanControl } from './BooleanControl';
|
||||
|
||||
export const Checkbox: FC<BooleanControlProps> = (props) => <BooleanControl type="checkbox" {...props} />;
|
||||
@@ -1,42 +0,0 @@
|
||||
/* stylelint-disable no-descending-specificity */
|
||||
|
||||
@import '../utils/mixins/vertical-align';
|
||||
|
||||
.dropdown-btn__toggle.dropdown-btn__toggle {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.dropdown-btn__toggle.dropdown-btn__toggle--with-caret {
|
||||
padding-right: 1.75rem;
|
||||
}
|
||||
|
||||
.dropdown-btn__toggle.dropdown-btn__toggle,
|
||||
.dropdown-btn__toggle.dropdown-btn__toggle:not(:disabled):not(.disabled).active,
|
||||
.dropdown-btn__toggle.dropdown-btn__toggle:not(:disabled):not(.disabled):active,
|
||||
.dropdown-btn__toggle.dropdown-btn__toggle:not(:disabled):not(.disabled):focus,
|
||||
.dropdown-btn__toggle.dropdown-btn__toggle:not(:disabled):not(.disabled):hover,
|
||||
.show > .dropdown-btn__toggle.dropdown-btn__toggle.dropdown-toggle {
|
||||
color: var(--input-text-color);
|
||||
background-color: var(--primary-color);
|
||||
border-color: var(--input-border-color);
|
||||
}
|
||||
|
||||
.card .dropdown-btn__toggle.dropdown-btn__toggle,
|
||||
.card .dropdown-btn__toggle.dropdown-btn__toggle:not(:disabled):not(.disabled).active,
|
||||
.card .dropdown-btn__toggle.dropdown-btn__toggle:not(:disabled):not(.disabled):active,
|
||||
.card .dropdown-btn__toggle.dropdown-btn__toggle:not(:disabled):not(.disabled):focus,
|
||||
.card .dropdown-btn__toggle.dropdown-btn__toggle:not(:disabled):not(.disabled):hover,
|
||||
.show > .card .dropdown-btn__toggle.dropdown-btn__toggle.dropdown-toggle {
|
||||
background-color: var(--input-color);
|
||||
}
|
||||
|
||||
.dropdown-btn__toggle.dropdown-btn__toggle.disabled,
|
||||
.dropdown-btn__toggle.dropdown-btn__toggle:disabled {
|
||||
background-color: var(--input-disabled-color);
|
||||
}
|
||||
|
||||
.dropdown-btn__toggle.dropdown-btn__toggle:after {
|
||||
@include vertical-align();
|
||||
|
||||
right: .75rem;
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
import classNames from 'classnames';
|
||||
import type { FC, PropsWithChildren, ReactNode } from 'react';
|
||||
import { Dropdown, DropdownMenu, DropdownToggle } from 'reactstrap';
|
||||
import type { DropdownToggleProps } from 'reactstrap/types/lib/DropdownToggle';
|
||||
import { useToggle } from './helpers/hooks';
|
||||
import './DropdownBtn.scss';
|
||||
|
||||
export type DropdownBtnProps = PropsWithChildren<Omit<DropdownToggleProps, 'caret' | 'size' | 'outline'> & {
|
||||
text: ReactNode;
|
||||
noCaret?: boolean;
|
||||
className?: string;
|
||||
dropdownClassName?: string;
|
||||
inline?: boolean;
|
||||
minWidth?: number;
|
||||
size?: 'sm' | 'md' | 'lg';
|
||||
}>;
|
||||
|
||||
export const DropdownBtn: FC<DropdownBtnProps> = ({
|
||||
text,
|
||||
disabled = false,
|
||||
className,
|
||||
children,
|
||||
dropdownClassName,
|
||||
noCaret,
|
||||
end = false,
|
||||
minWidth,
|
||||
inline,
|
||||
size,
|
||||
}) => {
|
||||
const [isOpen, toggle] = useToggle();
|
||||
const toggleClasses = classNames('dropdown-btn__toggle', className, {
|
||||
'btn-block': !inline,
|
||||
'dropdown-btn__toggle--with-caret': !noCaret,
|
||||
});
|
||||
const menuStyle = { minWidth: minWidth && `${minWidth}px` };
|
||||
|
||||
return (
|
||||
<Dropdown isOpen={isOpen} toggle={toggle} disabled={disabled} className={dropdownClassName}>
|
||||
<DropdownToggle size={size} caret={!noCaret} className={toggleClasses} color="primary">
|
||||
{text}
|
||||
</DropdownToggle>
|
||||
<DropdownMenu className="w-100" end={end} style={menuStyle}>{children}</DropdownMenu>
|
||||
</Dropdown>
|
||||
);
|
||||
};
|
||||
@@ -1,54 +0,0 @@
|
||||
import { faCircleNotch as preloader } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import classNames from 'classnames';
|
||||
import type { FC, PropsWithChildren } from 'react';
|
||||
import { Card, Row } from 'reactstrap';
|
||||
|
||||
type MessageType = 'default' | 'error';
|
||||
|
||||
const getClassForType = (type: MessageType) => {
|
||||
const map: Record<MessageType, string> = {
|
||||
error: 'border-danger',
|
||||
default: '',
|
||||
};
|
||||
|
||||
return map[type];
|
||||
};
|
||||
const getTextClassForType = (type: MessageType) => {
|
||||
const map: Record<MessageType, string> = {
|
||||
error: 'text-danger',
|
||||
default: 'text-muted',
|
||||
};
|
||||
|
||||
return map[type];
|
||||
};
|
||||
|
||||
export type MessageProps = PropsWithChildren<{
|
||||
className?: string;
|
||||
loading?: boolean;
|
||||
fullWidth?: boolean;
|
||||
type?: MessageType;
|
||||
}>;
|
||||
|
||||
export const Message: FC<MessageProps> = (
|
||||
{ className, children, loading = false, type = 'default', fullWidth = false },
|
||||
) => {
|
||||
const classes = classNames({
|
||||
'col-md-12': fullWidth,
|
||||
'col-md-10 offset-md-1': !fullWidth,
|
||||
});
|
||||
|
||||
return (
|
||||
<Row className={classNames('g-0', className)}>
|
||||
<div className={classes}>
|
||||
<Card className={getClassForType(type)} body>
|
||||
<h3 className={classNames('text-center mb-0', getTextClassForType(type))}>
|
||||
{loading && <FontAwesomeIcon icon={preloader} spin />}
|
||||
{loading && <span className="ms-2">{children ?? 'Loading...'}</span>}
|
||||
{!loading && children}
|
||||
</h3>
|
||||
</Card>
|
||||
</div>
|
||||
</Row>
|
||||
);
|
||||
};
|
||||
@@ -1,31 +0,0 @@
|
||||
@import './base';
|
||||
|
||||
.nav-pills__nav {
|
||||
position: sticky !important;
|
||||
top: $headerHeight - 1px;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.nav-pills__nav-link.nav-pills__nav-link {
|
||||
border-radius: 0 !important;
|
||||
padding-bottom: calc(.5rem - 3px) !important;
|
||||
border-bottom: 3px solid transparent !important;
|
||||
color: #5d6778;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
|
||||
@media (min-width: $smMin) and (max-width: $mdMax) {
|
||||
font-size: 89%;
|
||||
}
|
||||
}
|
||||
|
||||
.nav-pills__nav-link:hover {
|
||||
color: $mainColor !important;
|
||||
}
|
||||
|
||||
.nav-pills__nav-link.active {
|
||||
border-color: $mainColor !important;
|
||||
background-color: var(--primary-color) !important;
|
||||
color: $mainColor !important;
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
import type { FC, PropsWithChildren } from 'react';
|
||||
import { Children, isValidElement } from 'react';
|
||||
import { NavLink as RouterNavLink } from 'react-router-dom';
|
||||
import { Card, Nav, NavLink } from 'reactstrap';
|
||||
import './NavPills.scss';
|
||||
|
||||
type NavPillsProps = PropsWithChildren<{
|
||||
fill?: boolean;
|
||||
className?: string;
|
||||
}>;
|
||||
|
||||
type NavPillProps = PropsWithChildren<{
|
||||
to: string;
|
||||
replace?: boolean;
|
||||
}>;
|
||||
|
||||
export const NavPillItem: FC<NavPillProps> = ({ children, ...rest }) => (
|
||||
<NavLink className="nav-pills__nav-link" tag={RouterNavLink} {...rest}>
|
||||
{children}
|
||||
</NavLink>
|
||||
);
|
||||
|
||||
export const NavPills: FC<NavPillsProps> = ({ children, fill = false, className = '' }) => (
|
||||
<Card className={`nav-pills__nav p-0 overflow-hidden ${className}`} body>
|
||||
<Nav pills fill={fill}>
|
||||
{Children.map(children, (child) => {
|
||||
if (!isValidElement(child) || child.type !== NavPillItem) {
|
||||
throw new Error('Only NavPillItem children are allowed inside NavPills.');
|
||||
}
|
||||
|
||||
return child;
|
||||
})}
|
||||
</Nav>
|
||||
</Card>
|
||||
);
|
||||
@@ -1,8 +0,0 @@
|
||||
.ordering-dropdown__menu--link.ordering-dropdown__menu--link {
|
||||
min-width: 11rem;
|
||||
}
|
||||
|
||||
.ordering-dropdown__sort-icon {
|
||||
margin: 3.5px 0 0;
|
||||
float: right;
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
import { faSortAmountDown as sortDescIcon, faSortAmountUp as sortAscIcon } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import classNames from 'classnames';
|
||||
import { toPairs } from 'ramda';
|
||||
import { DropdownItem, DropdownMenu, DropdownToggle, UncontrolledDropdown } from 'reactstrap';
|
||||
import type { Order, OrderDir } from './helpers/ordering';
|
||||
import { determineOrderDir } from './helpers/ordering';
|
||||
import './OrderingDropdown.scss';
|
||||
|
||||
export interface OrderingDropdownProps<T extends string = string> {
|
||||
items: Record<T, string>;
|
||||
order: Order<T>;
|
||||
onChange: (orderField?: T, orderDir?: OrderDir) => void;
|
||||
isButton?: boolean;
|
||||
right?: boolean;
|
||||
prefixed?: boolean;
|
||||
}
|
||||
|
||||
export function OrderingDropdown<T extends string = string>(
|
||||
{ items, order, onChange, isButton = true, right = false, prefixed = true }: OrderingDropdownProps<T>,
|
||||
) {
|
||||
const handleItemClick = (fieldKey: T) => () => {
|
||||
const newOrderDir = determineOrderDir(fieldKey, order.field, order.dir);
|
||||
onChange(newOrderDir ? fieldKey : undefined, newOrderDir);
|
||||
};
|
||||
|
||||
return (
|
||||
<UncontrolledDropdown>
|
||||
<DropdownToggle
|
||||
caret
|
||||
color={isButton ? 'primary' : 'link'}
|
||||
className={classNames({
|
||||
'dropdown-btn__toggle btn-block pe-4 overflow-hidden': isButton,
|
||||
'btn-sm p-0': !isButton,
|
||||
})}
|
||||
>
|
||||
{!isButton && <>Order by</>}
|
||||
{isButton && !order.field && <i>Order by...</i>}
|
||||
{isButton && order.field && <>{prefixed && 'Order by: '}{items[order.field]} - <small>{order.dir ?? 'DESC'}</small></>}
|
||||
</DropdownToggle>
|
||||
<DropdownMenu
|
||||
end={right}
|
||||
className={classNames('w-100', { 'ordering-dropdown__menu--link': !isButton })}
|
||||
>
|
||||
{toPairs(items).map(([fieldKey, fieldValue]) => (
|
||||
<DropdownItem key={fieldKey} active={order.field === fieldKey} onClick={handleItemClick(fieldKey as T)}>
|
||||
{fieldValue}
|
||||
{order.field === fieldKey && (
|
||||
<FontAwesomeIcon
|
||||
icon={order.dir === 'ASC' ? sortAscIcon : sortDescIcon}
|
||||
className="ordering-dropdown__sort-icon"
|
||||
/>
|
||||
)}
|
||||
</DropdownItem>
|
||||
))}
|
||||
<DropdownItem divider />
|
||||
<DropdownItem disabled={!order.field} onClick={() => onChange()}>
|
||||
<i>Clear selection</i>
|
||||
</DropdownItem>
|
||||
</DropdownMenu>
|
||||
</UncontrolledDropdown>
|
||||
);
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
import classNames from 'classnames';
|
||||
import type { FC, PropsWithChildren } from 'react';
|
||||
import { Row } from 'reactstrap';
|
||||
import { SimpleCard } from './SimpleCard';
|
||||
|
||||
export type ResultType = 'success' | 'error' | 'warning';
|
||||
|
||||
export type ResultProps = PropsWithChildren<{
|
||||
type: ResultType;
|
||||
className?: string;
|
||||
small?: boolean;
|
||||
}>;
|
||||
|
||||
export const Result: FC<ResultProps> = ({ children, type, className, small = false }) => (
|
||||
<Row className={className}>
|
||||
<div className={classNames({ 'col-md-10 offset-md-1': !small, 'col-12': small })}>
|
||||
<SimpleCard
|
||||
role="document"
|
||||
className={classNames('text-center', {
|
||||
'bg-main': type === 'success',
|
||||
'bg-danger': type === 'error',
|
||||
'bg-warning': type === 'warning',
|
||||
'text-white': type !== 'warning',
|
||||
})}
|
||||
bodyClassName={classNames({ 'p-2': small })}
|
||||
>
|
||||
{children}
|
||||
</SimpleCard>
|
||||
</div>
|
||||
</Row>
|
||||
);
|
||||
@@ -1,21 +0,0 @@
|
||||
import { faEllipsisV as menuIcon } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import type { FC, PropsWithChildren } from 'react';
|
||||
import { DropdownBtn } from './DropdownBtn';
|
||||
|
||||
export type DropdownBtnMenuProps = PropsWithChildren<{
|
||||
minWidth?: number;
|
||||
}>;
|
||||
|
||||
export const RowDropdownBtn: FC<DropdownBtnMenuProps> = ({ children, minWidth }) => (
|
||||
<DropdownBtn
|
||||
text={<FontAwesomeIcon className="px-1" icon={menuIcon} />}
|
||||
size="sm"
|
||||
minWidth={minWidth}
|
||||
end
|
||||
noCaret
|
||||
inline
|
||||
>
|
||||
{children}
|
||||
</DropdownBtn>
|
||||
);
|
||||
@@ -1,33 +0,0 @@
|
||||
@import '../utils/mixins/vertical-align';
|
||||
|
||||
.search-field {
|
||||
position: relative;
|
||||
|
||||
&:focus-within {
|
||||
z-index: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.search-field__input.search-field__input {
|
||||
padding-left: 40px;
|
||||
padding-right: 40px;
|
||||
}
|
||||
|
||||
.search-field__input--no-border.search-field__input--no-border {
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.search-field__icon {
|
||||
@include vertical-align();
|
||||
|
||||
left: 15px;
|
||||
color: #6c757d;
|
||||
}
|
||||
|
||||
.search-field__close {
|
||||
@include vertical-align();
|
||||
|
||||
right: 10px;
|
||||
cursor: pointer;
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
import { faSearch as searchIcon } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import classNames from 'classnames';
|
||||
import { useState } from 'react';
|
||||
import './SearchField.scss';
|
||||
|
||||
const DEFAULT_SEARCH_INTERVAL = 500;
|
||||
let timer: NodeJS.Timeout | null;
|
||||
|
||||
interface SearchFieldProps {
|
||||
onChange: (value: string) => void;
|
||||
className?: string;
|
||||
large?: boolean;
|
||||
noBorder?: boolean;
|
||||
initialValue?: string;
|
||||
}
|
||||
|
||||
export const SearchField = ({ onChange, className, large = true, noBorder = false, initialValue = '' }: SearchFieldProps) => {
|
||||
const [searchTerm, setSearchTerm] = useState(initialValue);
|
||||
|
||||
const resetTimer = () => {
|
||||
timer && clearTimeout(timer);
|
||||
timer = null;
|
||||
};
|
||||
const searchTermChanged = (newSearchTerm: string, timeout = DEFAULT_SEARCH_INTERVAL) => {
|
||||
setSearchTerm(newSearchTerm);
|
||||
|
||||
resetTimer();
|
||||
|
||||
timer = setTimeout(() => {
|
||||
onChange(newSearchTerm);
|
||||
resetTimer();
|
||||
}, timeout);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={classNames('search-field', className)}>
|
||||
<input
|
||||
type="text"
|
||||
className={classNames('form-control search-field__input', {
|
||||
'form-control-lg': large,
|
||||
'search-field__input--no-border': noBorder,
|
||||
})}
|
||||
placeholder="Search..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => searchTermChanged(e.target.value)}
|
||||
/>
|
||||
<FontAwesomeIcon icon={searchIcon} className="search-field__icon" />
|
||||
<div
|
||||
className="close search-field__close btn-close"
|
||||
hidden={searchTerm === ''}
|
||||
id="search-field__close"
|
||||
onClick={() => searchTermChanged('', 0)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,15 +0,0 @@
|
||||
import type { ReactNode } from 'react';
|
||||
import type { CardProps } from 'reactstrap';
|
||||
import { Card, CardBody, CardHeader } from 'reactstrap';
|
||||
|
||||
interface SimpleCardProps extends Omit<CardProps, 'title'> {
|
||||
title?: ReactNode;
|
||||
bodyClassName?: string;
|
||||
}
|
||||
|
||||
export const SimpleCard = ({ title, children, bodyClassName, ...rest }: SimpleCardProps) => (
|
||||
<Card {...rest}>
|
||||
{title && <CardHeader role="heading">{title}</CardHeader>}
|
||||
<CardBody className={bodyClassName}>{children}</CardBody>
|
||||
</Card>
|
||||
);
|
||||
@@ -1,5 +0,0 @@
|
||||
import type { FC } from 'react';
|
||||
import type { BooleanControlProps } from './BooleanControl';
|
||||
import { BooleanControl } from './BooleanControl';
|
||||
|
||||
export const ToggleSwitch: FC<BooleanControlProps> = (props) => <BooleanControl type="switch" {...props} />;
|
||||
@@ -1,10 +1,11 @@
|
||||
import type { FC } from 'react';
|
||||
import { DropdownItem } from 'reactstrap';
|
||||
import { DropdownBtn } from '../../../shlink-frontend-kit/src';
|
||||
import type { Settings } from '../../../shlink-web-component';
|
||||
import { rangeOrIntervalToString } from '../../../shlink-web-component/utils/dates/helpers/dateIntervals';
|
||||
import { DropdownBtn } from '../DropdownBtn';
|
||||
import type { Defined } from '../types';
|
||||
|
||||
type DateInterval = Exclude<Settings['visits'], undefined>['defaultInterval'];
|
||||
type DateInterval = Defined<Settings['visits']>['defaultInterval'];
|
||||
|
||||
export interface DateIntervalSelectorProps {
|
||||
active?: DateInterval;
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
import type { FC, PropsWithChildren } from 'react';
|
||||
import type { InputType } from 'reactstrap/types/lib/Input';
|
||||
import { useDomId } from '../helpers/hooks';
|
||||
import { LabeledFormGroup } from './LabeledFormGroup';
|
||||
|
||||
export type InputFormGroupProps = PropsWithChildren<{
|
||||
value: string;
|
||||
onChange: (newValue: string) => void;
|
||||
type?: InputType;
|
||||
required?: boolean;
|
||||
placeholder?: string;
|
||||
className?: string;
|
||||
labelClassName?: string;
|
||||
}>;
|
||||
|
||||
export const InputFormGroup: FC<InputFormGroupProps> = (
|
||||
{ children, value, onChange, type, required, placeholder, className, labelClassName },
|
||||
) => {
|
||||
const id = useDomId();
|
||||
|
||||
return (
|
||||
<LabeledFormGroup label={<>{children}:</>} className={className ?? ''} labelClassName={labelClassName} id={id}>
|
||||
<input
|
||||
id={id}
|
||||
className="form-control"
|
||||
type={type ?? 'text'}
|
||||
value={value}
|
||||
required={required ?? true}
|
||||
placeholder={placeholder}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
/>
|
||||
</LabeledFormGroup>
|
||||
);
|
||||
};
|
||||
@@ -1,19 +0,0 @@
|
||||
import type { FC, PropsWithChildren, ReactNode } from 'react';
|
||||
|
||||
type LabeledFormGroupProps = PropsWithChildren<{
|
||||
label: ReactNode;
|
||||
noMargin?: boolean;
|
||||
className?: string;
|
||||
labelClassName?: string;
|
||||
id?: string;
|
||||
}>;
|
||||
|
||||
/* eslint-disable jsx-a11y/label-has-associated-control */
|
||||
export const LabeledFormGroup: FC<LabeledFormGroupProps> = (
|
||||
{ children, label, className = '', labelClassName = '', noMargin = false, id },
|
||||
) => (
|
||||
<div className={`${className} ${noMargin ? '' : 'mb-3'}`}>
|
||||
<label className={`form-label ${labelClassName}`} htmlFor={id}>{label}</label>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
@@ -1,6 +1,5 @@
|
||||
import { useRef, useState } from 'react';
|
||||
import { useLocation, useNavigate } from 'react-router-dom';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import { parseQuery } from '../../../shlink-web-component/utils/helpers/query';
|
||||
|
||||
const DEFAULT_DELAY = 2000;
|
||||
@@ -35,17 +34,3 @@ export const useParsedQuery = <T>(): T => {
|
||||
const { search } = useLocation();
|
||||
return parseQuery<T>(search);
|
||||
};
|
||||
|
||||
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 useDomId = (): string => {
|
||||
const { current: id } = useRef(`dom-${uuid()}`);
|
||||
return id;
|
||||
};
|
||||
|
||||
export const useElementRef = <T>() => useRef<T | null>(null);
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
export type OrderDir = 'ASC' | 'DESC' | undefined;
|
||||
|
||||
export interface Order<Fields> {
|
||||
field?: Fields;
|
||||
dir?: OrderDir;
|
||||
}
|
||||
|
||||
export const determineOrderDir = <T extends string = string>(
|
||||
currentField: T,
|
||||
newField?: T,
|
||||
currentOrderDir?: OrderDir,
|
||||
): OrderDir => {
|
||||
if (currentField !== newField) {
|
||||
return 'ASC';
|
||||
}
|
||||
|
||||
const newOrderMap: Record<'ASC' | 'DESC', OrderDir> = {
|
||||
ASC: 'DESC',
|
||||
DESC: undefined,
|
||||
};
|
||||
|
||||
return currentOrderDir ? newOrderMap[currentOrderDir] : 'ASC';
|
||||
};
|
||||
|
||||
export const sortList = <List>(list: List[], { field, dir }: Order<keyof List>) => (
|
||||
!field || !dir ? list : list.sort((a, b) => {
|
||||
const greaterThan = dir === 'ASC' ? 1 : -1;
|
||||
const smallerThan = dir === 'ASC' ? -1 : 1;
|
||||
|
||||
return a[field] > b[field] ? greaterThan : smallerThan;
|
||||
})
|
||||
);
|
||||
|
||||
export const orderToString = <T>(order: Order<T>): string | undefined => (
|
||||
order.dir ? `${order.field}-${order.dir}` : undefined
|
||||
);
|
||||
|
||||
export const stringToOrder = <T>(order: string): Order<T> => {
|
||||
const [field, dir] = order.split('-') as [T | undefined, OrderDir | undefined];
|
||||
return { field, dir };
|
||||
};
|
||||
1
src/utils/types.ts
Normal file
1
src/utils/types.ts
Normal file
@@ -0,0 +1 @@
|
||||
export type Defined<T> = Exclude<T, undefined>;
|
||||
Reference in New Issue
Block a user