diff --git a/package-lock.json b/package-lock.json index 0db9b965..3d55c790 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,7 +15,8 @@ "@fortawesome/react-fontawesome": "^0.2.0", "@json2csv/plainjs": "^7.0.1", "@reduxjs/toolkit": "^1.9.5", - "bootstrap": "^5.3.1", + "@shlinkio/shlink-frontend-kit": "^0.1.2", + "bootstrap": "5.2.3", "bottlejs": "^2.0.1", "bowser": "^2.11.0", "chart.js": "^4.3.3", @@ -3108,6 +3109,25 @@ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "dev": true }, + "node_modules/@shlinkio/shlink-frontend-kit": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@shlinkio/shlink-frontend-kit/-/shlink-frontend-kit-0.1.2.tgz", + "integrity": "sha512-7PppdcD7Urv5wJYxXR1X9KIJC9MbQXReG6qtusjSeUdyjeliux/6LclHlZteWwT5JuVvdV+FPVpbxl+D4wLXsw==", + "dependencies": { + "classnames": "^2.3.2", + "qs": "^6.11.2", + "uuid": "^9.0.0" + }, + "peerDependencies": { + "@fortawesome/fontawesome-free": "^6.4.2", + "@fortawesome/free-solid-svg-icons": "^6.4.2", + "@fortawesome/react-fontawesome": "^0.2.0", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "react-router-dom": "^6.14.2", + "reactstrap": "^9.2.0" + } + }, "node_modules/@shlinkio/stylelint-config-css-coding-standard": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@shlinkio/stylelint-config-css-coding-standard/-/stylelint-config-css-coding-standard-1.1.1.tgz", @@ -4436,9 +4456,9 @@ "license": "MIT" }, "node_modules/bootstrap": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.1.tgz", - "integrity": "sha512-jzwza3Yagduci2x0rr9MeFSORjcHpt0lRZukZPZQJT1Dth5qzV7XcgGqYzi39KGAVYR8QEDVoO0ubFKOxzMG+g==", + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.2.3.tgz", + "integrity": "sha512-cEKPM+fwb3cT8NzQZYEu4HilJ3anCrWqh3CHAok1p9jXqMPsPTBhU25fBckEJHJ/p+tTxTFTsFQGM+gaHpi3QQ==", "funding": [ { "type": "github", @@ -4450,7 +4470,7 @@ } ], "peerDependencies": { - "@popperjs/core": "^2.11.8" + "@popperjs/core": "^2.11.6" } }, "node_modules/bottlejs": { @@ -13306,6 +13326,16 @@ } } }, + "@shlinkio/shlink-frontend-kit": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@shlinkio/shlink-frontend-kit/-/shlink-frontend-kit-0.1.2.tgz", + "integrity": "sha512-7PppdcD7Urv5wJYxXR1X9KIJC9MbQXReG6qtusjSeUdyjeliux/6LclHlZteWwT5JuVvdV+FPVpbxl+D4wLXsw==", + "requires": { + "classnames": "^2.3.2", + "qs": "^6.11.2", + "uuid": "^9.0.0" + } + }, "@shlinkio/stylelint-config-css-coding-standard": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@shlinkio/stylelint-config-css-coding-standard/-/stylelint-config-css-coding-standard-1.1.1.tgz", @@ -14249,9 +14279,9 @@ "version": "3.7.2" }, "bootstrap": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.1.tgz", - "integrity": "sha512-jzwza3Yagduci2x0rr9MeFSORjcHpt0lRZukZPZQJT1Dth5qzV7XcgGqYzi39KGAVYR8QEDVoO0ubFKOxzMG+g==", + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.2.3.tgz", + "integrity": "sha512-cEKPM+fwb3cT8NzQZYEu4HilJ3anCrWqh3CHAok1p9jXqMPsPTBhU25fBckEJHJ/p+tTxTFTsFQGM+gaHpi3QQ==", "requires": {} }, "bottlejs": { diff --git a/package.json b/package.json index defad6d8..053ad2ff 100644 --- a/package.json +++ b/package.json @@ -7,8 +7,8 @@ "license": "MIT", "scripts": { "lint": "npm run lint:css && npm run lint:js", - "lint:css": "stylelint src/*.scss src/**/*.scss shlink-web-component/*.scss shlink-web-component/**/*.scss shlink-frontend-kit/*.scss shlink-frontend-kit/**/*.scss", - "lint:js": "eslint --ext .js,.ts,.tsx src shlink-web-component shlink-frontend-kit test", + "lint:css": "stylelint src/*.scss src/**/*.scss shlink-web-component/*.scss shlink-web-component/**/*.scss", + "lint:js": "eslint --ext .js,.ts,.tsx src shlink-web-component test", "lint:fix": "npm run lint:css:fix && npm run lint:js:fix", "lint:css:fix": "npm run lint:css -- --fix", "lint:js:fix": "npm run lint:js -- --fix", @@ -31,7 +31,8 @@ "@fortawesome/react-fontawesome": "^0.2.0", "@json2csv/plainjs": "^7.0.1", "@reduxjs/toolkit": "^1.9.5", - "bootstrap": "^5.3.1", + "@shlinkio/shlink-frontend-kit": "^0.1.2", + "bootstrap": "5.2.3", "bottlejs": "^2.0.1", "bowser": "^2.11.0", "chart.js": "^4.3.3", diff --git a/shlink-frontend-kit/src/base.scss b/shlink-frontend-kit/src/base.scss deleted file mode 100644 index 6a8d8c9c..00000000 --- a/shlink-frontend-kit/src/base.scss +++ /dev/null @@ -1,27 +0,0 @@ -// Breakpoints -$xsMax: 575px; -$smMin: 576px; -$smMax: 767px; -$mdMin: 768px; -$mdMax: 991px; -$lgMin: 992px; -$lgMax: 1199px; -$xlgMin: 1200px; -$responsiveTableBreakpoint: $mdMax; - -// Colors -$mainColor: #4696e5; -$lightColor: #f5f6fe; -$lightGrey: #eeeeee; -$dangerColor: #dc3545; -$mediumGrey: #dee2e6; -$textPlaceholder: #6c757d; - -// Misc -$headerHeight: 56px; -$asideMenuWidth: 260px; -$footer-height: 2.3rem; -$footer-margin: .8rem; - -// Bootstrap overwrites -$primary: $mainColor; diff --git a/shlink-frontend-kit/src/block/Message.tsx b/shlink-frontend-kit/src/block/Message.tsx deleted file mode 100644 index 2d9d9928..00000000 --- a/shlink-frontend-kit/src/block/Message.tsx +++ /dev/null @@ -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 = { - error: 'border-danger', - default: '', - }; - - return map[type]; -}; -const getTextClassForType = (type: MessageType) => { - const map: Record = { - 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 = ( - { className, children, loading = false, type = 'default', fullWidth = false }, -) => { - const classes = classNames({ - 'col-md-12': fullWidth, - 'col-md-10 offset-md-1': !fullWidth, - }); - - return ( - -
- -

- {loading && } - {loading && {children ?? 'Loading...'}} - {!loading && children} -

-
-
-
- ); -}; diff --git a/shlink-frontend-kit/src/block/Result.tsx b/shlink-frontend-kit/src/block/Result.tsx deleted file mode 100644 index 12c380a1..00000000 --- a/shlink-frontend-kit/src/block/Result.tsx +++ /dev/null @@ -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 = ({ children, type, className, small = false }) => ( - -
- - {children} - -
-
-); diff --git a/shlink-frontend-kit/src/block/SimpleCard.tsx b/shlink-frontend-kit/src/block/SimpleCard.tsx deleted file mode 100644 index eea80173..00000000 --- a/shlink-frontend-kit/src/block/SimpleCard.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import type { ReactNode } from 'react'; -import type { CardProps } from 'reactstrap'; -import { Card, CardBody, CardHeader } from 'reactstrap'; - -export type SimpleCardProps = Omit & { - title?: ReactNode; - bodyClassName?: string; -}; - -export const SimpleCard = ({ title, children, bodyClassName, ...rest }: SimpleCardProps) => ( - - {title && {title}} - {children} - -); diff --git a/shlink-frontend-kit/src/block/index.ts b/shlink-frontend-kit/src/block/index.ts deleted file mode 100644 index b075331a..00000000 --- a/shlink-frontend-kit/src/block/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from './Message'; -export * from './Result'; -export * from './SimpleCard'; diff --git a/shlink-frontend-kit/src/form/BooleanControl.tsx b/shlink-frontend-kit/src/form/BooleanControl.tsx deleted file mode 100644 index 1beb4039..00000000 --- a/shlink-frontend-kit/src/form/BooleanControl.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import classNames from 'classnames'; -import { identity } from 'ramda'; -import type { ChangeEvent, FC, PropsWithChildren } from 'react'; -import { useDomId } from '../hooks'; - -export type BooleanControlProps = PropsWithChildren<{ - checked?: boolean; - onChange?: (checked: boolean, e: ChangeEvent) => void; - className?: string; - inline?: boolean; -}>; - -type BooleanControlWithTypeProps = BooleanControlProps & { - type: 'switch' | 'checkbox'; -}; - -export const BooleanControl: FC = ( - { checked = false, onChange = identity, className, children, type, inline = false }, -) => { - const id = useDomId(); - const onChecked = (e: ChangeEvent) => onChange(e.target.checked, e); - const typeClasses = { - 'form-switch': type === 'switch', - 'form-checkbox': type === 'checkbox', - }; - const style = inline ? { display: 'inline-block' } : {}; - - return ( - - - - - ); -}; diff --git a/shlink-frontend-kit/src/form/Checkbox.tsx b/shlink-frontend-kit/src/form/Checkbox.tsx deleted file mode 100644 index fc61a7e1..00000000 --- a/shlink-frontend-kit/src/form/Checkbox.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import type { FC } from 'react'; -import type { BooleanControlProps } from './BooleanControl'; -import { BooleanControl } from './BooleanControl'; - -export const Checkbox: FC = (props) => ; diff --git a/shlink-frontend-kit/src/form/InputFormGroup.tsx b/shlink-frontend-kit/src/form/InputFormGroup.tsx deleted file mode 100644 index e4e1ffe4..00000000 --- a/shlink-frontend-kit/src/form/InputFormGroup.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import type { FC, PropsWithChildren } from 'react'; -import type { InputType } from 'reactstrap/types/lib/Input'; -import { useDomId } from '../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 = ( - { children, value, onChange, type, required, placeholder, className, labelClassName }, -) => { - const id = useDomId(); - - return ( - {children}:} className={className ?? ''} labelClassName={labelClassName} id={id}> - onChange(e.target.value)} - /> - - ); -}; diff --git a/shlink-frontend-kit/src/form/LabeledFormGroup.tsx b/shlink-frontend-kit/src/form/LabeledFormGroup.tsx deleted file mode 100644 index 24ed4044..00000000 --- a/shlink-frontend-kit/src/form/LabeledFormGroup.tsx +++ /dev/null @@ -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 = ( - { children, label, className = '', labelClassName = '', noMargin = false, id }, -) => ( -
- - {children} -
-); diff --git a/shlink-frontend-kit/src/form/SearchField.scss b/shlink-frontend-kit/src/form/SearchField.scss deleted file mode 100644 index cc49151b..00000000 --- a/shlink-frontend-kit/src/form/SearchField.scss +++ /dev/null @@ -1,33 +0,0 @@ -@import '../../../shlink-web-component/src/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; -} diff --git a/shlink-frontend-kit/src/form/SearchField.tsx b/shlink-frontend-kit/src/form/SearchField.tsx deleted file mode 100644 index e1d3a75c..00000000 --- a/shlink-frontend-kit/src/form/SearchField.tsx +++ /dev/null @@ -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; - -type 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 ( -
- searchTermChanged(e.target.value)} - /> - - - ); -}; diff --git a/shlink-frontend-kit/src/form/ToggleSwitch.tsx b/shlink-frontend-kit/src/form/ToggleSwitch.tsx deleted file mode 100644 index b9cc6b96..00000000 --- a/shlink-frontend-kit/src/form/ToggleSwitch.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import type { FC } from 'react'; -import type { BooleanControlProps } from './BooleanControl'; -import { BooleanControl } from './BooleanControl'; - -export const ToggleSwitch: FC = (props) => ; diff --git a/shlink-frontend-kit/src/form/index.ts b/shlink-frontend-kit/src/form/index.ts deleted file mode 100644 index 160f856d..00000000 --- a/shlink-frontend-kit/src/form/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -export * from './Checkbox'; -export * from './ToggleSwitch'; -export * from './InputFormGroup'; -export * from './LabeledFormGroup'; -export * from './SearchField'; diff --git a/shlink-frontend-kit/src/hooks/index.ts b/shlink-frontend-kit/src/hooks/index.ts deleted file mode 100644 index 0e62687a..00000000 --- a/shlink-frontend-kit/src/hooks/index.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { useRef, useState } from 'react'; -import { v4 as uuid } from 'uuid'; - -type ToggleResult = [boolean, () => void, () => void, () => void]; - -export const useToggle = (initialValue = false): ToggleResult => { - const [flag, setFlag] = useState(initialValue); - return [flag, () => setFlag(!flag), () => setFlag(true), () => setFlag(false)]; -}; - -export const useDomId = (): string => { - const { current: id } = useRef(`dom-${uuid()}`); - return id; -}; - -export const useElementRef = () => useRef(null); diff --git a/shlink-frontend-kit/src/index.scss b/shlink-frontend-kit/src/index.scss deleted file mode 100644 index 1fd1e917..00000000 --- a/shlink-frontend-kit/src/index.scss +++ /dev/null @@ -1,219 +0,0 @@ -@import './utils/ResponsiveTable'; -@import './theme/theme'; - -/* stylelint-disable no-descending-specificity */ - -a, -.btn-link { - text-decoration: none; -} - -/* stylelint-disable-next-line selector-max-pseudo-class */ -a:not(.nav-link):not(.navbar-brand):not(.page-link):not(.highlight-card):not(.btn):not(.dropdown-item):hover, -.btn-link:hover { - text-decoration: underline; -} - -.bg-main { - background-color: $mainColor !important; -} - -.bg-warning { - color: $lightTextColor; -} - -.card-body, -.card-header, -.list-group-item { - background-color: transparent; -} - -.card-footer { - background-color: var(--primary-color-alfa); -} - -.card { - box-shadow: 0 .125rem .25rem rgb(0 0 0 / .075); - background-color: var(--primary-color); - border-color: var(--border-color); -} - -.list-group { - background-color: var(--primary-color); -} - -.modal-content, -.page-link, -.page-item.disabled .page-link, -.dropdown-menu { - background-color: var(--primary-color); -} - -.modal-header, -.modal-footer, -.card-header, -.card-footer, -.table thead th, -.table th, -.table td, -.page-link, -.page-link:hover, -.page-item.disabled .page-link, -.dropdown-divider, -.dropdown-menu, -.list-group-item, -.modal-content, -hr { - border-color: var(--border-color); -} - -.table-bordered, -.table-bordered thead th, -.table-bordered thead td { - border-color: var(--table-border-color); -} - -.page-link:hover, -.page-link:focus { - background-color: var(--secondary-color); -} - -.page-item.active .page-link { - background-color: var(--brand-color); - border-color: var(--brand-color); -} - -.pagination .page-link { - cursor: pointer; -} - -.container-xl { - @media (min-width: $xlgMin) { - max-width: 1320px; - } - - @media (max-width: $smMax) { - padding-right: 0; - padding-left: 0; - } -} - -/* Deprecated. Brought from bootstrap 4 */ -.btn-block { - display: block; - width: 100%; -} - -.btn-primary, -.btn-primary:hover, -.btn-primary:active, -.btn-primary.active, -.btn-outline-primary:hover, -.btn-outline-primary:active, -.btn-outline-primary.active, { - color: #ffffff; -} - -.dropdown-item, -.dropdown-item-text { - color: var(--text-color); -} - -.dropdown-item:not(:disabled) { - cursor: pointer; -} - -.dropdown-item:focus:not(:disabled), -.dropdown-item:hover:not(:disabled), -.dropdown-item.active:not(:disabled), -.dropdown-item:active:not(:disabled) { - background-color: var(--active-color) !important; - color: var(--text-color) !important; -} - -.dropdown-item--danger.dropdown-item--danger { - color: $dangerColor; - - &:hover, - &:active, - &.active { - color: $dangerColor !important; - } -} - -.badge-main { - color: #ffffff; - background-color: var(--brand-color); -} - -.close, -.close:hover, -.table, -.table-hover > tbody > tr:hover > *, -.table-hover > tbody > tr > * { - color: var(--text-color); -} - -.btn-close { - filter: var(--btn-close-filter); -} - -.table-hover tbody tr:hover { - background-color: var(--secondary-color); -} - -.form-control, -.form-control:focus { - background-color: var(--primary-color); - border-color: var(--input-border-color); - color: var(--input-text-color); -} - -.form-control.disabled, -.form-control:disabled { - background-color: var(--input-disabled-color); - cursor: not-allowed; -} - -.card .form-control:not(:disabled), -.card .form-control:not(:disabled):hover { - background-color: var(--input-color); -} - -.table-active, -.table-active > th, -.table-active > td { - background-color: var(--table-highlight-color) !important; -} - -.navbar-brand { - @media (max-width: $smMax) { - margin: 0 auto !important; - } -} - -.indivisible { - white-space: nowrap; -} - -.pointer { - cursor: pointer; -} - -.progress-bar { - background-color: $mainColor; -} - -.btn-xs-block { - @media (max-width: $xsMax) { - width: 100%; - display: block; - } -} - -.btn-md-block { - @media (max-width: $mdMax) { - width: 100%; - display: block; - } -} diff --git a/shlink-frontend-kit/src/index.ts b/shlink-frontend-kit/src/index.ts deleted file mode 100644 index 735725b8..00000000 --- a/shlink-frontend-kit/src/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -export * from './block'; -export * from './form'; -export * from './hooks'; -export * from './navigation'; -export * from './ordering'; -export * from './theme'; -export * from './utils'; diff --git a/shlink-frontend-kit/src/navigation/DropdownBtn.scss b/shlink-frontend-kit/src/navigation/DropdownBtn.scss deleted file mode 100644 index 8d3ea060..00000000 --- a/shlink-frontend-kit/src/navigation/DropdownBtn.scss +++ /dev/null @@ -1,42 +0,0 @@ -/* stylelint-disable no-descending-specificity */ - -@import '../../../shlink-web-component/src/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; -} diff --git a/shlink-frontend-kit/src/navigation/DropdownBtn.tsx b/shlink-frontend-kit/src/navigation/DropdownBtn.tsx deleted file mode 100644 index 43f0056c..00000000 --- a/shlink-frontend-kit/src/navigation/DropdownBtn.tsx +++ /dev/null @@ -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 '../hooks'; -import './DropdownBtn.scss'; - -export type DropdownBtnProps = PropsWithChildren & { - text: ReactNode; - noCaret?: boolean; - className?: string; - dropdownClassName?: string; - inline?: boolean; - minWidth?: number; - size?: 'sm' | 'md' | 'lg'; -}>; - -export const DropdownBtn: FC = ({ - 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 ( - - - {text} - - {children} - - ); -}; diff --git a/shlink-frontend-kit/src/navigation/NavPills.scss b/shlink-frontend-kit/src/navigation/NavPills.scss deleted file mode 100644 index 4151ebf1..00000000 --- a/shlink-frontend-kit/src/navigation/NavPills.scss +++ /dev/null @@ -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; -} diff --git a/shlink-frontend-kit/src/navigation/NavPills.tsx b/shlink-frontend-kit/src/navigation/NavPills.tsx deleted file mode 100644 index 52e197bf..00000000 --- a/shlink-frontend-kit/src/navigation/NavPills.tsx +++ /dev/null @@ -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 = ({ children, ...rest }) => ( - - {children} - -); - -export const NavPills: FC = ({ children, fill = false, className = '' }) => ( - - - -); diff --git a/shlink-frontend-kit/src/navigation/RowDropdownBtn.tsx b/shlink-frontend-kit/src/navigation/RowDropdownBtn.tsx deleted file mode 100644 index 8225386d..00000000 --- a/shlink-frontend-kit/src/navigation/RowDropdownBtn.tsx +++ /dev/null @@ -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 = ({ children, minWidth }) => ( - } - size="sm" - minWidth={minWidth} - end - noCaret - inline - > - {children} - -); diff --git a/shlink-frontend-kit/src/navigation/index.ts b/shlink-frontend-kit/src/navigation/index.ts deleted file mode 100644 index 5dabcc11..00000000 --- a/shlink-frontend-kit/src/navigation/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from './DropdownBtn'; -export * from './RowDropdownBtn'; -export * from './NavPills'; diff --git a/shlink-frontend-kit/src/ordering/OrderingDropdown.scss b/shlink-frontend-kit/src/ordering/OrderingDropdown.scss deleted file mode 100644 index 253824b9..00000000 --- a/shlink-frontend-kit/src/ordering/OrderingDropdown.scss +++ /dev/null @@ -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; -} diff --git a/shlink-frontend-kit/src/ordering/OrderingDropdown.tsx b/shlink-frontend-kit/src/ordering/OrderingDropdown.tsx deleted file mode 100644 index cd87e22d..00000000 --- a/shlink-frontend-kit/src/ordering/OrderingDropdown.tsx +++ /dev/null @@ -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 './ordering'; -import { determineOrderDir } from './ordering'; -import './OrderingDropdown.scss'; - -export type OrderingDropdownProps = { - items: Record; - order: Order; - onChange: (orderField?: T, orderDir?: OrderDir) => void; - isButton?: boolean; - right?: boolean; - prefixed?: boolean; -}; - -export function OrderingDropdown( - { items, order, onChange, isButton = true, right = false, prefixed = true }: OrderingDropdownProps, -) { - const handleItemClick = (fieldKey: T) => () => { - const newOrderDir = determineOrderDir(fieldKey, order.field, order.dir); - onChange(newOrderDir ? fieldKey : undefined, newOrderDir); - }; - - return ( - - - {!isButton && <>Order by} - {isButton && !order.field && Order by...} - {isButton && order.field && <>{prefixed && 'Order by: '}{items[order.field]} - {order.dir ?? 'DESC'}} - - - {toPairs(items).map(([fieldKey, fieldValue]) => ( - - {fieldValue} - {order.field === fieldKey && ( - - )} - - ))} - - onChange()}> - Clear selection - - - - ); -} diff --git a/shlink-frontend-kit/src/ordering/index.ts b/shlink-frontend-kit/src/ordering/index.ts deleted file mode 100644 index dcc4f1a1..00000000 --- a/shlink-frontend-kit/src/ordering/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './ordering'; -export * from './OrderingDropdown'; diff --git a/shlink-frontend-kit/src/ordering/ordering.ts b/shlink-frontend-kit/src/ordering/ordering.ts deleted file mode 100644 index 77ac4ffc..00000000 --- a/shlink-frontend-kit/src/ordering/ordering.ts +++ /dev/null @@ -1,41 +0,0 @@ -export type OrderDir = 'ASC' | 'DESC' | undefined; - -export type Order = { - field?: Fields; - dir?: OrderDir; -}; - -export const determineOrderDir = ( - 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[], { field, dir }: Order) => ( - !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 = (order: Order): string | undefined => ( - order.dir ? `${order.field}-${order.dir}` : undefined -); - -export const stringToOrder = (order: string): Order => { - const [field, dir] = order.split('-') as [T | undefined, OrderDir | undefined]; - return { field, dir }; -}; diff --git a/shlink-frontend-kit/src/theme/index.ts b/shlink-frontend-kit/src/theme/index.ts deleted file mode 100644 index 970ef398..00000000 --- a/shlink-frontend-kit/src/theme/index.ts +++ /dev/null @@ -1,17 +0,0 @@ -export const MAIN_COLOR = '#4696e5'; - -export const MAIN_COLOR_ALPHA = 'rgba(70, 150, 229, 0.4)'; - -export const HIGHLIGHTED_COLOR = '#f77f28'; - -export const HIGHLIGHTED_COLOR_ALPHA = 'rgba(247, 127, 40, 0.4)'; - -export const PRIMARY_LIGHT_COLOR = 'white'; - -export const PRIMARY_DARK_COLOR = '#161b22'; - -export type Theme = 'dark' | 'light'; - -export const changeThemeInMarkup = (theme: Theme) => document.querySelector('html')?.setAttribute('data-theme', theme); - -export const isDarkThemeEnabled = (): boolean => document.querySelector('html')?.getAttribute('data-theme') === 'dark'; diff --git a/shlink-frontend-kit/src/theme/theme.scss b/shlink-frontend-kit/src/theme/theme.scss deleted file mode 100644 index dd4c20d1..00000000 --- a/shlink-frontend-kit/src/theme/theme.scss +++ /dev/null @@ -1,67 +0,0 @@ -@import '../base'; - -// Light theme colors -$lightPrimaryColor: #ffffff; -$lightPrimaryColorAlfa: rgba($lightPrimaryColor, .5); -$lightSecondaryColor: $lightColor; -$lightTextColor: #232323; -$lightBorderColor: rgb(0 0 0 / .125); -$lightTableBorderColor: $mediumGrey; -$lightActiveColor: $lightGrey; -$lightBrandColor: $mainColor; -$lightInputColor: $lightPrimaryColor; -$lightInputTextColor: #495057; -$lightDisabledInputColor: $lightColor; -$lightBorderInputColor: rgb(0 0 0 / .19); -$lightTableHighlightColor: rgb(0 0 0 / .075); - -// Dark theme colors -$darkPrimaryColor: #161b22; -$darkPrimaryColorAlfa: rgba($darkPrimaryColor, .8); -$darkSecondaryColor: #0f131a; -$darkTextColor: rgb(201 209 217); -$darkBorderColor: rgb(255 255 255 / .15); -$darkTableBorderColor: #393d43; -$darkActiveColor: $darkSecondaryColor; -$darkBrandColor: #0b2d4e; -$darkInputColor: darken($darkPrimaryColor, 2%); -$darkInputTextColor: $darkTextColor; -$darkDisabledInputColor: lighten($darkPrimaryColor, 2%); -$darkBorderInputColor: $darkBorderColor; -$darkTableHighlightColor: $darkBorderColor; - -html:not([data-theme='dark']) { - --color-scheme: initial; - --primary-color: #{$lightPrimaryColor}; - --primary-color-alfa: #{$lightPrimaryColorAlfa}; - --secondary-color: #{$lightSecondaryColor}; - --text-color: #{$lightTextColor}; - --border-color: #{$lightBorderColor}; - --active-color: #{$lightActiveColor}; - --brand-color: #{$lightBrandColor}; - --input-color: #{$lightInputColor}; - --input-disabled-color: #{$lightDisabledInputColor}; - --input-border-color: #{$lightBorderInputColor}; - --input-text-color: #{$lightInputTextColor}; - --table-border-color: #{$lightTableBorderColor}; - --table-highlight-color: #{$lightTableHighlightColor}; - --btn-close-filter: initial; -} - -html[data-theme='dark'] { - --color-scheme: dark; - --primary-color: #{$darkPrimaryColor}; - --primary-color-alfa: #{$darkPrimaryColorAlfa}; - --secondary-color: #{$darkSecondaryColor}; - --text-color: #{$darkTextColor}; - --border-color: #{$darkBorderColor}; - --active-color: #{$darkActiveColor}; - --brand-color: #{$darkBrandColor}; - --input-color: #{$darkInputColor}; - --input-disabled-color: #{$darkDisabledInputColor}; - --input-border-color: #{$darkBorderInputColor}; - --input-text-color: #{$darkInputTextColor}; - --table-border-color: #{$darkTableBorderColor}; - --table-highlight-color: #{$darkTableHighlightColor}; - --btn-close-filter: invert(1); -} diff --git a/shlink-frontend-kit/src/utils/ResponsiveTable.scss b/shlink-frontend-kit/src/utils/ResponsiveTable.scss deleted file mode 100644 index a7e90583..00000000 --- a/shlink-frontend-kit/src/utils/ResponsiveTable.scss +++ /dev/null @@ -1,59 +0,0 @@ -@import '../base'; - -.responsive-table__header { - @media (max-width: $responsiveTableBreakpoint) { - display: none; - } -} - -.responsive-table.table > :not(:first-child) { - @media (max-width: $responsiveTableBreakpoint) { - border: none; - } -} - -.responsive-table__row { - @media (max-width: $responsiveTableBreakpoint) { - display: block; - border-bottom: 1px solid var(--border-color); - border-top: 2px solid var(--border-color); - position: relative; - - &:not(:last-child) { - margin-bottom: 10px; - } - } -} - -.responsive-table__cell.responsive-table__cell { - vertical-align: middle !important; - - @media (max-width: $responsiveTableBreakpoint) { - display: block; - width: 100%; - position: relative; - padding: .5rem; - font-size: .9rem; - - &[data-th]:before { - content: attr(data-th) ': '; - font-weight: 700; - } - - &:last-child { - position: absolute; - top: 3.5px; - right: .5rem; - width: auto; - padding: 0; - border: none; - } - } -} - -.responsive-table__cell.responsive-table__cell .btn-sm { - @media (max-width: $responsiveTableBreakpoint) { - padding: 0.1rem 0.4rem; - margin-top: 0.16rem; - } -} diff --git a/shlink-frontend-kit/src/utils/index.ts b/shlink-frontend-kit/src/utils/index.ts deleted file mode 100644 index cb029acc..00000000 --- a/shlink-frontend-kit/src/utils/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -import qs from 'qs'; - -// FIXME Use URLSearchParams instead of qs package - -export const parseQuery = (search: string) => qs.parse(search, { ignoreQueryPrefix: true }) as unknown as T; - -export const stringifyQuery = (query: any): string => qs.stringify(query, { arrayFormat: 'brackets' }); diff --git a/shlink-frontend-kit/test/__helpers__/setUpTest.ts b/shlink-frontend-kit/test/__helpers__/setUpTest.ts deleted file mode 100644 index 7fccbf98..00000000 --- a/shlink-frontend-kit/test/__helpers__/setUpTest.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { render } from '@testing-library/react'; -import userEvent from '@testing-library/user-event'; -import type { ReactElement } from 'react'; - -export const renderWithEvents = (element: ReactElement) => ({ - user: userEvent.setup(), - ...render(element), -}); diff --git a/shlink-frontend-kit/test/block/Message.test.tsx b/shlink-frontend-kit/test/block/Message.test.tsx deleted file mode 100644 index 101542c8..00000000 --- a/shlink-frontend-kit/test/block/Message.test.tsx +++ /dev/null @@ -1,50 +0,0 @@ -import { render, screen } from '@testing-library/react'; -import type { PropsWithChildren } from 'react'; -import type { MessageProps } from '../../src'; -import { Message } from '../../src'; - -describe('', () => { - const setUp = (props: PropsWithChildren = {}) => render(); - - it.each([ - [true, 'col-md-12'], - [false, 'col-md-10 offset-md-1'], - [undefined, 'col-md-10 offset-md-1'], - ])('renders expected classes based on width', (fullWidth, expectedClass) => { - const { container } = setUp({ fullWidth }); - expect(container.firstChild?.firstChild).toHaveClass(expectedClass); - }); - - it.each([ - [true, 'These are the children contents'], - [false, 'These are the children contents'], - [true, undefined], - [false, undefined], - ])('renders expected content', (loading, children) => { - setUp({ loading, children }); - - expect(screen.queryAllByRole('img', { hidden: true })).toHaveLength(loading ? 1 : 0); - - if (loading) { - expect(screen.getByText(children || 'Loading...')).toHaveClass('ms-2'); - } else { - expect(screen.getByRole('heading')).toHaveTextContent(children || ''); - } - }); - - it.each([ - ['error', 'border-danger', 'text-danger'], - ['default', '', 'text-muted'], - [undefined, '', 'text-muted'], - ])('renders proper classes based on message type', (type, expectedCardClass, expectedH3Class) => { - const { container } = setUp({ type: type as 'default' | 'error' | undefined }); - - expect(container.querySelector('.card-body')).toHaveAttribute('class', expect.stringContaining(expectedCardClass)); - expect(screen.getByRole('heading')).toHaveClass(`text-center mb-0 ${expectedH3Class}`); - }); - - it.each([{ className: 'foo' }, { className: 'bar' }, {}])('renders provided classes', ({ className }) => { - const { container } = setUp({ className }); - expect(container.firstChild).toHaveClass(`g-0${className ? ` ${className}` : ''}`); - }); -}); diff --git a/shlink-frontend-kit/test/block/Result.test.tsx b/shlink-frontend-kit/test/block/Result.test.tsx deleted file mode 100644 index 2747c746..00000000 --- a/shlink-frontend-kit/test/block/Result.test.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import { render, screen } from '@testing-library/react'; -import type { ResultProps, ResultType } from '../../src'; -import { Result } from '../../src'; - -describe('', () => { - const setUp = (props: ResultProps) => render(); - - it.each([ - ['success' as ResultType, 'bg-main text-white'], - ['error' as ResultType, 'bg-danger text-white'], - ['warning' as ResultType, 'bg-warning'], - ])('renders expected classes based on type', (type, expectedClasses) => { - setUp({ type }); - expect(screen.getByRole('document')).toHaveClass(expectedClasses); - }); - - it.each([ - ['foo'], - ['bar'], - ])('renders provided classes in root element', (className) => { - const { container } = setUp({ type: 'success', className }); - expect(container.firstChild).toHaveClass(className); - }); - - it.each([{ small: true }, { small: false }])('renders small results properly', ({ small }) => { - const { container } = setUp({ type: 'success', small }); - const bigElement = container.querySelectorAll('.col-md-10'); - const smallElement = container.querySelectorAll('.col-12'); - - expect(bigElement).toHaveLength(small ? 0 : 1); - expect(smallElement).toHaveLength(small ? 1 : 0); - }); -}); diff --git a/shlink-frontend-kit/test/block/SimpleCard.test.tsx b/shlink-frontend-kit/test/block/SimpleCard.test.tsx deleted file mode 100644 index 1ca051f0..00000000 --- a/shlink-frontend-kit/test/block/SimpleCard.test.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import { render, screen } from '@testing-library/react'; -import type { SimpleCardProps } from '../../src'; -import { SimpleCard } from '../../src'; - -const setUp = ({ children, ...rest }: SimpleCardProps = {}) => render({children}); - -describe('', () => { - it('does not render title if not provided', () => { - setUp(); - expect(screen.queryByRole('heading')).not.toBeInTheDocument(); - }); - - it('renders provided title', () => { - setUp({ title: 'Cool title' }); - expect(screen.getByRole('heading')).toHaveTextContent('Cool title'); - }); - - it('renders children inside body', () => { - setUp({ children: 'Hello world' }); - expect(screen.getByText('Hello world')).toBeInTheDocument(); - }); - - it.each(['primary', 'danger', 'warning'])('passes extra props to nested card', (color) => { - const { container } = setUp({ className: 'foo', color, children: 'Hello world' }); - expect(container.firstChild).toHaveAttribute('class', `foo card bg-${color}`); - }); -}); diff --git a/shlink-frontend-kit/test/form/Checkbox.test.tsx b/shlink-frontend-kit/test/form/Checkbox.test.tsx deleted file mode 100644 index 302d32be..00000000 --- a/shlink-frontend-kit/test/form/Checkbox.test.tsx +++ /dev/null @@ -1,44 +0,0 @@ -import { render, screen } from '@testing-library/react'; -import { Checkbox } from '../../src'; -import { renderWithEvents } from '../__helpers__/setUpTest'; - -describe('', () => { - it.each([['foo'], ['bar'], ['baz']])('includes extra class names when provided', (className) => { - const { container } = render(); - expect(container.firstChild).toHaveAttribute('class', `form-check form-checkbox ${className}`); - }); - - it.each([[true], [false]])('marks input as checked if defined', (checked) => { - render(Foo); - - if (checked) { - expect(screen.getByLabelText('Foo')).toBeChecked(); - } else { - expect(screen.getByLabelText('Foo')).not.toBeChecked(); - } - }); - - it.each([['foo'], ['bar'], ['baz']])('renders provided children inside the label', (children) => { - render({children}); - expect(screen.getByText(children)).toHaveAttribute('class', 'form-check-label'); - }); - - it.each([[true], [false]])('changes checked status on input change', async (checked) => { - const onChange = vi.fn(); - const { user } = renderWithEvents(Foo); - - expect(onChange).not.toHaveBeenCalled(); - await user.click(screen.getByLabelText('Foo')); - expect(onChange).toHaveBeenCalledWith(!checked, expect.anything()); - }); - - it.each([[true], [false]])('allows setting inline rendering', (inline) => { - const { container } = render(); - - if (inline) { - expect(container.firstChild).toHaveAttribute('style', 'display: inline-block;'); - } else { - expect(container.firstChild).not.toHaveAttribute('style'); - } - }); -}); diff --git a/shlink-frontend-kit/test/navigation/DropdownBtn.test.tsx b/shlink-frontend-kit/test/navigation/DropdownBtn.test.tsx deleted file mode 100644 index d51caef5..00000000 --- a/shlink-frontend-kit/test/navigation/DropdownBtn.test.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import { screen } from '@testing-library/react'; -import type { PropsWithChildren } from 'react'; -import type { DropdownBtnProps } from '../../src'; -import { DropdownBtn } from '../../src'; -import { renderWithEvents } from '../__helpers__/setUpTest'; - -describe('', () => { - const setUp = (props: PropsWithChildren) => renderWithEvents( - , - ); - - it.each([['foo'], ['bar'], ['baz']])('displays provided text in button', (text) => { - setUp({ text }); - expect(screen.getByRole('button')).toHaveTextContent(text); - }); - - it.each([['foo'], ['bar'], ['baz']])('displays provided children in menu', async (children) => { - const { user } = setUp({ text: '', children }); - - await user.click(screen.getByRole('button')); - expect(screen.getByRole('menu')).toHaveTextContent(children); - }); - - it.each([ - [undefined, 'dropdown-btn__toggle btn-block'], - ['', 'dropdown-btn__toggle btn-block'], - ['foo', 'dropdown-btn__toggle btn-block foo'], - ['bar', 'dropdown-btn__toggle btn-block bar'], - ])('includes provided classes', (className, expectedClasses) => { - setUp({ text: '', className }); - expect(screen.getByRole('button')).toHaveClass(expectedClasses); - }); - - it.each([ - [100, 'min-width: 100px; '], - [250, 'min-width: 250px; '], - [undefined, ''], - ])('renders proper styles when minWidth is provided', async (minWidth, expectedStyle) => { - const { user } = setUp({ text: '', minWidth }); - - await user.click(screen.getByRole('button')); - expect(screen.getByRole('menu')).toHaveAttribute( - 'style', - `${expectedStyle}position: absolute; left: 0px; top: 0px; transform: translate(0px, 0px);`, - ); - }); -}); diff --git a/shlink-frontend-kit/test/navigation/NavPills.test.tsx b/shlink-frontend-kit/test/navigation/NavPills.test.tsx deleted file mode 100644 index a23b3dcb..00000000 --- a/shlink-frontend-kit/test/navigation/NavPills.test.tsx +++ /dev/null @@ -1,56 +0,0 @@ -/* eslint-disable no-console */ -import { render, screen } from '@testing-library/react'; -import { MemoryRouter } from 'react-router-dom'; -import { NavPillItem, NavPills } from '../../src'; - -describe('', () => { - let originalError: typeof console.error; - - beforeEach(() => { - originalError = console.error; - console.error = () => {}; // Suppress errors logged during this test - }); - afterEach(() => { - console.error = originalError; - }); - - it.each([ - ['Foo'], - [Hi!], - [[, Hi!]], - ])('throws error when any of the children is not a NavPillItem', (children) => { - expect.assertions(1); - expect(() => render({children})).toThrow( - 'Only NavPillItem children are allowed inside NavPills.', - ); - }); - - it.each([ - [undefined], - [true], - [false], - ])('renders provided items', (fill) => { - const { container } = render( - - - 1 - 2 - 3 - - , - ); - - const links = screen.getAllByRole('link'); - expect(links).toHaveLength(3); - links.forEach((link, index) => { - expect(link).toHaveTextContent(`${index + 1}`); - expect(link).toHaveAttribute('href', `/${index + 1}`); - }); - - if (fill) { - expect(container.querySelector('.nav')).toHaveClass('nav-fill'); - } else { - expect(container.querySelector('.nav')).not.toHaveClass('nav-fill'); - } - }); -}); diff --git a/shlink-frontend-kit/test/navigation/RowDropdownBtn.test.tsx b/shlink-frontend-kit/test/navigation/RowDropdownBtn.test.tsx deleted file mode 100644 index 62cd6de9..00000000 --- a/shlink-frontend-kit/test/navigation/RowDropdownBtn.test.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import { screen } from '@testing-library/react'; -import { fromPartial } from '@total-typescript/shoehorn'; -import type { DropdownBtnMenuProps } from '../../src'; -import { RowDropdownBtn } from '../../src'; -import { renderWithEvents } from '../__helpers__/setUpTest'; - -describe('', () => { - const setUp = (props: Partial = {}) => renderWithEvents( - ({ ...props })}> - the children - , - ); - - it('renders expected components', () => { - setUp(); - const toggle = screen.getByRole('button'); - - expect(toggle).toBeInTheDocument(); - expect(toggle).toHaveClass('btn-sm'); - expect(toggle).toHaveClass('dropdown-btn__toggle'); - expect(screen.getByRole('img', { hidden: true })).toBeInTheDocument(); - }); - - it('renders expected children', () => { - setUp(); - expect(screen.getByText('the children')).toBeInTheDocument(); - }); -}); diff --git a/shlink-frontend-kit/test/ordering/OrderingDropdown.test.tsx b/shlink-frontend-kit/test/ordering/OrderingDropdown.test.tsx deleted file mode 100644 index 9b5aedf2..00000000 --- a/shlink-frontend-kit/test/ordering/OrderingDropdown.test.tsx +++ /dev/null @@ -1,107 +0,0 @@ -import { screen } from '@testing-library/react'; -import { values } from 'ramda'; -import type { OrderDir, OrderingDropdownProps } from '../../src'; -import { OrderingDropdown } from '../../src'; -import { renderWithEvents } from '../__helpers__/setUpTest'; - -describe('', () => { - const items = { - foo: 'Foo', - bar: 'Bar', - baz: 'Hello World', - }; - const setUp = (props: Partial = {}) => renderWithEvents( - , - ); - const setUpWithDisplayedMenu = async (props: Partial = {}) => { - const result = setUp(props); - const { user } = result; - - await user.click(screen.getByRole('button')); - expect(await screen.findByRole('menu')).toBeInTheDocument(); - - return result; - }; - - it('properly renders provided list of items', async () => { - await setUpWithDisplayedMenu(); - - const dropdownItems = screen.getAllByRole('menuitem'); - - expect(dropdownItems).toHaveLength(values(items).length); - expect(dropdownItems[0]).toHaveTextContent('Foo'); - expect(dropdownItems[1]).toHaveTextContent('Bar'); - expect(dropdownItems[2]).toHaveTextContent('Hello World'); - expect(screen.getByRole('button', { name: 'Clear selection' })).toBeInTheDocument(); - }); - - it.each([ - ['foo', 0], - ['bar', 1], - ['baz', 2], - ])('properly marks selected field as active with proper icon', async (field, expectedActiveIndex) => { - await setUpWithDisplayedMenu({ order: { field, dir: 'DESC' } }); - - const dropdownItems = screen.getAllByRole('menuitem'); - - expect(dropdownItems).toHaveLength(4); - expect(screen.queryByRole('button', { name: 'Clear selection' })).not.toBeInTheDocument(); - - dropdownItems.forEach((item, index) => { - if (index === expectedActiveIndex) { - expect(item).toHaveAttribute('class', expect.stringContaining('active')); - } else { - expect(item).not.toHaveAttribute('class', expect.stringContaining('active')); - } - }); - }); - - it.each([ - [{} as any, 'foo', 'ASC'], - [{ field: 'baz', dir: 'ASC' } as any, 'foo', 'ASC'], - [{ field: 'foo', dir: 'ASC' } as any, 'foo', 'DESC'], - [{ field: 'foo', dir: 'DESC' } as any, undefined, undefined], - ])( - 'triggers change with proper params depending on clicked item and initial state', - async (initialOrder, expectedNewField, expectedNewDir) => { - const onChange = vi.fn(); - const { user } = await setUpWithDisplayedMenu({ onChange, order: initialOrder }); - - await user.click(screen.getAllByRole('menuitem')[0]); - - expect(onChange).toHaveBeenCalledTimes(1); - expect(onChange).toHaveBeenCalledWith(expectedNewField, expectedNewDir); - }, - ); - - it('clears selection when last item is clicked', async () => { - const onChange = vi.fn(); - const { user } = await setUpWithDisplayedMenu({ onChange, order: { field: 'baz', dir: 'ASC' } }); - - await user.click(screen.getAllByRole('menuitem')[3]); - - expect(onChange).toHaveBeenCalledTimes(1); - expect(onChange).toHaveBeenCalledWith(); - }); - - it.each([ - [{ isButton: false }, /Order by$/], - [{ isButton: true }, 'Order by...'], - [ - { isButton: true, order: { field: 'foo', dir: 'ASC' as OrderDir } }, - 'Order by: Foo - ASC', - ], - [ - { isButton: true, order: { field: 'baz', dir: 'DESC' as OrderDir } }, - 'Order by: Hello World - DESC', - ], - [{ isButton: true, order: { field: 'baz' } }, 'Order by: Hello World - DESC'], - [ - { isButton: true, order: { field: 'baz', dir: 'DESC' as OrderDir }, prefixed: false }, - /^Hello World - DESC/, - ], - ])('with %s props displays %s in toggle', async (props, expectedText) => { - setUp(props); - expect(screen.getByRole('button')).toHaveTextContent(expectedText); - }); -}); diff --git a/shlink-frontend-kit/test/ordering/ordering.test.ts b/shlink-frontend-kit/test/ordering/ordering.test.ts deleted file mode 100644 index 899eb8ae..00000000 --- a/shlink-frontend-kit/test/ordering/ordering.test.ts +++ /dev/null @@ -1,46 +0,0 @@ -import type { OrderDir } from '../../src'; -import { determineOrderDir, orderToString, stringToOrder } from '../../src'; - -describe('ordering', () => { - describe('determineOrderDir', () => { - it('returns ASC when current order field and selected field are different', () => { - expect(determineOrderDir('foo', 'bar')).toEqual('ASC'); - expect(determineOrderDir('bar', 'foo')).toEqual('ASC'); - }); - - it('returns ASC when no current order dir is provided', () => { - expect(determineOrderDir('foo', 'foo')).toEqual('ASC'); - expect(determineOrderDir('bar', 'bar')).toEqual('ASC'); - }); - - it('returns DESC when current order field and selected field are equal and current order dir is ASC', () => { - expect(determineOrderDir('foo', 'foo', 'ASC')).toEqual('DESC'); - expect(determineOrderDir('bar', 'bar', 'ASC')).toEqual('DESC'); - }); - - it('returns undefined when current order field and selected field are equal and current order dir is DESC', () => { - expect(determineOrderDir('foo', 'foo', 'DESC')).toBeUndefined(); - expect(determineOrderDir('bar', 'bar', 'DESC')).toBeUndefined(); - }); - }); - - describe('orderToString', () => { - it.each([ - [{}, undefined], - [{ field: 'foo' }, undefined], - [{ field: 'foo', dir: 'ASC' as OrderDir }, 'foo-ASC'], - [{ field: 'bar', dir: 'DESC' as OrderDir }, 'bar-DESC'], - ])('casts the order to string', (order, expectedResult) => { - expect(orderToString(order)).toEqual(expectedResult); - }); - }); - - describe('stringToOrder', () => { - it.each([ - ['foo-ASC', { field: 'foo', dir: 'ASC' }], - ['bar-DESC', { field: 'bar', dir: 'DESC' }], - ])('casts a string to an order objects', (order, expectedResult) => { - expect(stringToOrder(order)).toEqual(expectedResult); - }); - }); -}); diff --git a/shlink-frontend-kit/test/utils/query.test.ts b/shlink-frontend-kit/test/utils/query.test.ts deleted file mode 100644 index a22445d3..00000000 --- a/shlink-frontend-kit/test/utils/query.test.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { parseQuery, stringifyQuery } from '../../src/utils'; - -describe('query', () => { - describe('parseQuery', () => { - it.each([ - ['', {}], - ['foo=bar', { foo: 'bar' }], - ['?foo=bar', { foo: 'bar' }], - ['?foo=bar&baz=123', { foo: 'bar', baz: '123' }], - ])('parses query string as expected', (queryString, expectedResult) => { - expect(parseQuery(queryString)).toEqual(expectedResult); - }); - }); - - describe('stringifyQuery', () => { - it.each([ - [{}, ''], - [{ foo: 'bar' }, 'foo=bar'], - [{ foo: 'bar', baz: '123' }, 'foo=bar&baz=123'], - [{ bar: 'foo', list: ['one', 'two'] }, encodeURI('bar=foo&list[]=one&list[]=two')], - ])('stringifies query as expected', (queryObj, expectedResult) => { - expect(stringifyQuery(queryObj)).toEqual(expectedResult); - }); - }); -}); diff --git a/shlink-web-component/src/Main.scss b/shlink-web-component/src/Main.scss index 03d3545c..3472e14e 100644 --- a/shlink-web-component/src/Main.scss +++ b/shlink-web-component/src/Main.scss @@ -1,4 +1,4 @@ -@import '@shlinkio/shlink-frontend-kit/base'; +@import 'node_modules/@shlinkio/shlink-frontend-kit/dist/base'; .shlink-layout__swipeable { height: 100%; diff --git a/shlink-web-component/src/common/AsideMenu.scss b/shlink-web-component/src/common/AsideMenu.scss index 2ddd3d13..765bc86f 100644 --- a/shlink-web-component/src/common/AsideMenu.scss +++ b/shlink-web-component/src/common/AsideMenu.scss @@ -1,4 +1,4 @@ -@import '@shlinkio/shlink-frontend-kit/base'; +@import 'node_modules/@shlinkio/shlink-frontend-kit/dist/base'; @import '../utils/mixins/vertical-align'; .aside-menu { diff --git a/shlink-web-component/src/domains/DomainSelector.scss b/shlink-web-component/src/domains/DomainSelector.scss index bdf04ef6..e6bd0cfc 100644 --- a/shlink-web-component/src/domains/DomainSelector.scss +++ b/shlink-web-component/src/domains/DomainSelector.scss @@ -1,4 +1,4 @@ -@import '@shlinkio/shlink-frontend-kit/base'; +@import 'node_modules/@shlinkio/shlink-frontend-kit/dist/base'; @import '../utils/mixins/vertical-align'; .domains-dropdown__toggle-btn.domains-dropdown__toggle-btn, diff --git a/shlink-web-component/src/overview/helpers/HighlightCard.scss b/shlink-web-component/src/overview/helpers/HighlightCard.scss index 8e23089c..1c41f361 100644 --- a/shlink-web-component/src/overview/helpers/HighlightCard.scss +++ b/shlink-web-component/src/overview/helpers/HighlightCard.scss @@ -1,4 +1,4 @@ -@import '@shlinkio/shlink-frontend-kit/base'; +@import 'node_modules/@shlinkio/shlink-frontend-kit/dist/base'; .highlight-card.highlight-card { text-align: center; diff --git a/shlink-web-component/src/short-urls/ShortUrlForm.scss b/shlink-web-component/src/short-urls/ShortUrlForm.scss index 39d137c0..2b8bf66a 100644 --- a/shlink-web-component/src/short-urls/ShortUrlForm.scss +++ b/shlink-web-component/src/short-urls/ShortUrlForm.scss @@ -1,4 +1,4 @@ -@import '@shlinkio/shlink-frontend-kit/base'; +@import 'node_modules/@shlinkio/shlink-frontend-kit/dist/base'; .short-url-form p:last-child { margin-bottom: 0; diff --git a/shlink-web-component/src/short-urls/helpers/ShortUrlsRow.scss b/shlink-web-component/src/short-urls/helpers/ShortUrlsRow.scss index 5ecd9c9d..f4278ea1 100644 --- a/shlink-web-component/src/short-urls/helpers/ShortUrlsRow.scss +++ b/shlink-web-component/src/short-urls/helpers/ShortUrlsRow.scss @@ -1,4 +1,4 @@ -@import '@shlinkio/shlink-frontend-kit/base'; +@import 'node_modules/@shlinkio/shlink-frontend-kit/dist/base'; @import '../../utils/mixins/vertical-align'; @mixin text-ellipsis() { diff --git a/shlink-web-component/src/tags/TagsTable.scss b/shlink-web-component/src/tags/TagsTable.scss index 2a8699e4..29a1bad0 100644 --- a/shlink-web-component/src/tags/TagsTable.scss +++ b/shlink-web-component/src/tags/TagsTable.scss @@ -1,4 +1,4 @@ -@import '@shlinkio/shlink-frontend-kit/base'; +@import 'node_modules/@shlinkio/shlink-frontend-kit/dist/base'; @import '../utils/mixins/sticky-cell'; .tags-table__header-cell.tags-table__header-cell { diff --git a/shlink-web-component/src/tags/react-tag-autocomplete.scss b/shlink-web-component/src/tags/react-tag-autocomplete.scss index 880d957b..7c4baf5c 100644 --- a/shlink-web-component/src/tags/react-tag-autocomplete.scss +++ b/shlink-web-component/src/tags/react-tag-autocomplete.scss @@ -1,4 +1,4 @@ -@import '@shlinkio/shlink-frontend-kit/base'; +@import '../../../node_modules/@shlinkio/shlink-frontend-kit/dist/base'; .react-tags { position: relative; diff --git a/shlink-web-component/src/utils/components/IconInput.scss b/shlink-web-component/src/utils/components/IconInput.scss index 1fc0f083..1aac0ece 100644 --- a/shlink-web-component/src/utils/components/IconInput.scss +++ b/shlink-web-component/src/utils/components/IconInput.scss @@ -1,5 +1,5 @@ @import '../mixins/vertical-align'; -@import '@shlinkio/shlink-frontend-kit/base'; +@import 'node_modules/@shlinkio/shlink-frontend-kit/dist/base'; .icon-input-container { position: relative; diff --git a/shlink-web-component/src/utils/dates/DateInput.scss b/shlink-web-component/src/utils/dates/DateInput.scss index e096e75b..40cb106e 100644 --- a/shlink-web-component/src/utils/dates/DateInput.scss +++ b/shlink-web-component/src/utils/dates/DateInput.scss @@ -1,5 +1,5 @@ @import '../mixins/vertical-align'; -@import '@shlinkio/shlink-frontend-kit/base'; +@import 'node_modules/@shlinkio/shlink-frontend-kit/dist/base'; .react-datepicker__close-icon.react-datepicker__close-icon { @include vertical-align(); diff --git a/shlink-web-component/src/utils/mixins/sticky-cell.scss b/shlink-web-component/src/utils/mixins/sticky-cell.scss index b006b83c..447d9447 100644 --- a/shlink-web-component/src/utils/mixins/sticky-cell.scss +++ b/shlink-web-component/src/utils/mixins/sticky-cell.scss @@ -1,4 +1,4 @@ -@import '@shlinkio/shlink-frontend-kit/base'; +@import 'node_modules/@shlinkio/shlink-frontend-kit/dist/base'; @mixin sticky-cell($with-separators: true) { z-index: 1; diff --git a/shlink-web-component/src/visits/VisitsTable.scss b/shlink-web-component/src/visits/VisitsTable.scss index 6c4f4e00..0aa5736f 100644 --- a/shlink-web-component/src/visits/VisitsTable.scss +++ b/shlink-web-component/src/visits/VisitsTable.scss @@ -1,4 +1,4 @@ -@import '@shlinkio/shlink-frontend-kit/base'; +@import 'node_modules/@shlinkio/shlink-frontend-kit/dist/base'; @import '../utils/mixins/sticky-cell'; .visits-table { diff --git a/shlink-web-component/src/visits/charts/DoughnutChartLegend.scss b/shlink-web-component/src/visits/charts/DoughnutChartLegend.scss index 0f8d4249..69eaee27 100644 --- a/shlink-web-component/src/visits/charts/DoughnutChartLegend.scss +++ b/shlink-web-component/src/visits/charts/DoughnutChartLegend.scss @@ -1,4 +1,4 @@ -@import '@shlinkio/shlink-frontend-kit/base'; +@import 'node_modules/@shlinkio/shlink-frontend-kit/dist/base'; .doughnut-chart-legend { list-style-type: none; diff --git a/shlink-web-component/src/visits/charts/LineChartCard.scss b/shlink-web-component/src/visits/charts/LineChartCard.scss index a74f33ef..9b4d7ec8 100644 --- a/shlink-web-component/src/visits/charts/LineChartCard.scss +++ b/shlink-web-component/src/visits/charts/LineChartCard.scss @@ -1,4 +1,4 @@ -@import '@shlinkio/shlink-frontend-kit/base'; +@import 'node_modules/@shlinkio/shlink-frontend-kit/dist/base'; .line-chart-card__body canvas { height: 300px !important; diff --git a/shlink-web-component/src/visits/helpers/MapModal.scss b/shlink-web-component/src/visits/helpers/MapModal.scss index 4a88ca51..9401b83a 100644 --- a/shlink-web-component/src/visits/helpers/MapModal.scss +++ b/shlink-web-component/src/visits/helpers/MapModal.scss @@ -1,4 +1,4 @@ -@import '@shlinkio/shlink-frontend-kit/base'; +@import 'node_modules/@shlinkio/shlink-frontend-kit/dist/base'; @import '../../utils/mixins/fit-with-margin'; .map-modal__modal.map-modal__modal { diff --git a/src/app/App.scss b/src/app/App.scss index 573ffcb2..168fc2a7 100644 --- a/src/app/App.scss +++ b/src/app/App.scss @@ -1,4 +1,4 @@ -@import '@shlinkio/shlink-frontend-kit/base'; +@import 'node_modules/@shlinkio/shlink-frontend-kit/dist/base'; .app-container { height: 100%; diff --git a/src/common/AppUpdateBanner.scss b/src/common/AppUpdateBanner.scss index 1791d80d..46b91841 100644 --- a/src/common/AppUpdateBanner.scss +++ b/src/common/AppUpdateBanner.scss @@ -1,4 +1,4 @@ -@import '@shlinkio/shlink-frontend-kit/base'; +@import 'node_modules/@shlinkio/shlink-frontend-kit/dist/base'; @import '../utils/mixins/horizontal-align'; .app-update-banner.app-update-banner { diff --git a/src/common/Home.scss b/src/common/Home.scss index 3a5a44b4..58798fd2 100644 --- a/src/common/Home.scss +++ b/src/common/Home.scss @@ -1,4 +1,4 @@ -@import '@shlinkio/shlink-frontend-kit/base'; +@import 'node_modules/@shlinkio/shlink-frontend-kit/dist/base'; @import '../utils/mixins/vertical-align'; $mainCardWidth: 720px; diff --git a/src/common/MainHeader.scss b/src/common/MainHeader.scss index dbd0aea6..11a899d7 100644 --- a/src/common/MainHeader.scss +++ b/src/common/MainHeader.scss @@ -1,4 +1,4 @@ -@import '@shlinkio/shlink-frontend-kit/base'; +@import 'node_modules/@shlinkio/shlink-frontend-kit/dist/base'; .main-header.main-header { color: white; diff --git a/src/common/NoMenuLayout.scss b/src/common/NoMenuLayout.scss index 19405b30..eb0a6271 100644 --- a/src/common/NoMenuLayout.scss +++ b/src/common/NoMenuLayout.scss @@ -1,4 +1,4 @@ -@import '@shlinkio/shlink-frontend-kit/base'; +@import 'node_modules/@shlinkio/shlink-frontend-kit/dist/base'; .no-menu-wrapper { padding: 15px 0 0; diff --git a/src/common/ShlinkVersionsContainer.scss b/src/common/ShlinkVersionsContainer.scss index 3a7e8faa..e6ad3b9b 100644 --- a/src/common/ShlinkVersionsContainer.scss +++ b/src/common/ShlinkVersionsContainer.scss @@ -1,4 +1,4 @@ -@import '@shlinkio/shlink-frontend-kit/base'; +@import 'node_modules/@shlinkio/shlink-frontend-kit/dist/base'; .shlink-versions-container--with-sidebar { margin-left: 0; diff --git a/src/index.scss b/src/index.scss index 9bac305e..fe2dbf0f 100644 --- a/src/index.scss +++ b/src/index.scss @@ -1,6 +1,6 @@ -@import '@shlinkio/shlink-frontend-kit/base'; // Before bootstrap stylesheet. Includes SASS var overrides +@import 'node_modules/@shlinkio/shlink-frontend-kit/dist/base'; // Before bootstrap stylesheet. Includes SASS var overrides @import 'node_modules/bootstrap/scss/bootstrap.scss'; -@import '@shlinkio/shlink-frontend-kit/index'; // After bootstrap. Includes CSS overwrites +@import 'node_modules/@shlinkio/shlink-frontend-kit/dist/index'; // After bootstrap. Includes CSS overwrites @import '@shlinkio/shlink-web-component/index'; * { diff --git a/src/servers/ServersListGroup.scss b/src/servers/ServersListGroup.scss index 71236d44..a7266a31 100644 --- a/src/servers/ServersListGroup.scss +++ b/src/servers/ServersListGroup.scss @@ -1,4 +1,4 @@ -@import '@shlinkio/shlink-frontend-kit/base'; +@import 'node_modules/@shlinkio/shlink-frontend-kit/dist/base'; @import '../utils/mixins/vertical-align'; @import '../utils/mixins/thin-scroll'; diff --git a/src/servers/helpers/ServerError.scss b/src/servers/helpers/ServerError.scss index 89fef361..6aed0332 100644 --- a/src/servers/helpers/ServerError.scss +++ b/src/servers/helpers/ServerError.scss @@ -1,4 +1,4 @@ -@import '@shlinkio/shlink-frontend-kit/base'; +@import 'node_modules/@shlinkio/shlink-frontend-kit/dist/base'; .server-error__container { text-align: center; diff --git a/tsconfig.json b/tsconfig.json index cdd9d7b6..2541b822 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -22,7 +22,6 @@ "isolatedModules": true, "baseUrl": ".", "paths": { - "@shlinkio/shlink-frontend-kit": ["shlink-frontend-kit/src"], "@shlinkio/shlink-web-component": ["shlink-web-component/src"], "@shlinkio/shlink-web-component/api-contract": ["shlink-web-component/src/api-contract"] } diff --git a/vite.config.ts b/vite.config.ts index d43f83f4..cee89cfa 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -27,7 +27,6 @@ export default defineConfig({ base: !homepage ? undefined : homepage, // Not using just homepage because empty string should be discarded resolve: { alias: { - '@shlinkio/shlink-frontend-kit': path.resolve(__dirname, './shlink-frontend-kit/src'), '@shlinkio/shlink-web-component': path.resolve(__dirname, './shlink-web-component/src'), }, },