mirror of
https://github.com/shlinkio/shlink-web-client.git
synced 2026-03-18 21:43:49 +00:00
Improved QR code component
This commit is contained in:
@@ -1,19 +1,87 @@
|
|||||||
import { Modal, ModalBody, ModalHeader } from 'reactstrap';
|
import { useMemo, useState } from 'react';
|
||||||
|
import { DropdownItem, FormGroup, Modal, ModalBody, ModalHeader, Row } from 'reactstrap';
|
||||||
import { ExternalLink } from 'react-external-link';
|
import { ExternalLink } from 'react-external-link';
|
||||||
|
import CopyToClipboard from 'react-copy-to-clipboard';
|
||||||
|
import { faCopy as copyIcon } from '@fortawesome/free-regular-svg-icons';
|
||||||
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
import { ShortUrlModalProps } from '../data';
|
import { ShortUrlModalProps } from '../data';
|
||||||
|
import { ReachableServer } from '../../servers/data';
|
||||||
|
import { versionMatch } from '../../utils/helpers/version';
|
||||||
|
import { DropdownBtn } from '../../utils/DropdownBtn';
|
||||||
import './QrCodeModal.scss';
|
import './QrCodeModal.scss';
|
||||||
|
|
||||||
const QrCodeModal = ({ shortUrl: { shortUrl }, toggle, isOpen }: ShortUrlModalProps) => (
|
interface QrCodeModalConnectProps extends ShortUrlModalProps {
|
||||||
<Modal isOpen={isOpen} toggle={toggle} centered>
|
selectedServer: ReachableServer;
|
||||||
<ModalHeader toggle={toggle}>
|
}
|
||||||
QR code for <ExternalLink href={shortUrl}>{shortUrl}</ExternalLink>
|
|
||||||
</ModalHeader>
|
type QrCodeFormat = 'svg' | 'png';
|
||||||
<ModalBody>
|
|
||||||
<div className="text-center">
|
const buildQrCodeUrl = (shortUrl: string, size: number, format: QrCodeFormat, version: string): string => {
|
||||||
<img src={`${shortUrl}/qr-code`} className="qr-code-modal__img" alt="QR code" />
|
const useSizeInPath = !versionMatch(version, { minVersion: '2.5.0' });
|
||||||
</div>
|
const svgIsSupported = versionMatch(version, { minVersion: '2.4.0' });
|
||||||
</ModalBody>
|
const sizeFragment = useSizeInPath ? `/${size}?` : `?size=${size}&`;
|
||||||
</Modal>
|
const formatFragment = !svgIsSupported ? '' : `format=${format}`;
|
||||||
);
|
|
||||||
|
return `${shortUrl}/qr-code${sizeFragment}${formatFragment}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const QrCodeModal = ({ shortUrl: { shortUrl }, toggle, isOpen, selectedServer }: QrCodeModalConnectProps) => {
|
||||||
|
const [ size, setSize ] = useState(300);
|
||||||
|
const [ format, setFormat ] = useState<QrCodeFormat>('png');
|
||||||
|
const qrCodeUrl = useMemo(
|
||||||
|
() => buildQrCodeUrl(shortUrl, size, format, selectedServer.version),
|
||||||
|
[ shortUrl, size, format, selectedServer ],
|
||||||
|
);
|
||||||
|
const modalSize = useMemo(() => {
|
||||||
|
if (size < 500) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return size < 800 ? 'lg' : 'xl';
|
||||||
|
}, [ size ]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal isOpen={isOpen} toggle={toggle} centered size={modalSize}>
|
||||||
|
<ModalHeader toggle={toggle}>
|
||||||
|
QR code for <ExternalLink href={shortUrl}>{shortUrl}</ExternalLink>
|
||||||
|
</ModalHeader>
|
||||||
|
<ModalBody>
|
||||||
|
<Row className="mb-2">
|
||||||
|
<div className="col-md-6">
|
||||||
|
<FormGroup>
|
||||||
|
<label className="mb-0">Size: {size}px</label>
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
className="form-control-range"
|
||||||
|
value={size}
|
||||||
|
step={10}
|
||||||
|
min={50}
|
||||||
|
max={1000}
|
||||||
|
onChange={(e) => setSize(Number(e.target.value))}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
</div>
|
||||||
|
<div className="col-md-6">
|
||||||
|
<DropdownBtn text={`Format (${format})`}>
|
||||||
|
<DropdownItem active={format === 'png'} onClick={() => setFormat('png')}>PNG</DropdownItem>
|
||||||
|
<DropdownItem active={format === 'svg'} onClick={() => setFormat('svg')}>SVG</DropdownItem>
|
||||||
|
</DropdownBtn>
|
||||||
|
</div>
|
||||||
|
</Row>
|
||||||
|
<div className="text-center">
|
||||||
|
<div className="mb-3">
|
||||||
|
<div>QR code URL:</div>
|
||||||
|
<ExternalLink className="indivisible" href={qrCodeUrl} />
|
||||||
|
<CopyToClipboard text={qrCodeUrl}>
|
||||||
|
<FontAwesomeIcon icon={copyIcon} className="ml-2" />
|
||||||
|
</CopyToClipboard>
|
||||||
|
</div>
|
||||||
|
<img src={qrCodeUrl} className="qr-code-modal__img" alt="QR code" />
|
||||||
|
<div className="mt-2">{size}x{size}</div>
|
||||||
|
</div>
|
||||||
|
</ModalBody>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export default QrCodeModal;
|
export default QrCodeModal;
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ import { useToggle } from '../../utils/helpers/hooks';
|
|||||||
import { ShortUrl, ShortUrlModalProps } from '../data';
|
import { ShortUrl, ShortUrlModalProps } from '../data';
|
||||||
import { Versions } from '../../utils/helpers/version';
|
import { Versions } from '../../utils/helpers/version';
|
||||||
import { SelectedServer } from '../../servers/data';
|
import { SelectedServer } from '../../servers/data';
|
||||||
import QrCodeModal from './QrCodeModal';
|
|
||||||
import VisitStatsLink from './VisitStatsLink';
|
import VisitStatsLink from './VisitStatsLink';
|
||||||
import './ShortUrlsRowMenu.scss';
|
import './ShortUrlsRowMenu.scss';
|
||||||
|
|
||||||
@@ -29,6 +28,7 @@ const ShortUrlsRowMenu = (
|
|||||||
EditTagsModal: ShortUrlModal,
|
EditTagsModal: ShortUrlModal,
|
||||||
EditMetaModal: ShortUrlModal,
|
EditMetaModal: ShortUrlModal,
|
||||||
EditShortUrlModal: ShortUrlModal,
|
EditShortUrlModal: ShortUrlModal,
|
||||||
|
QrCodeModal: ShortUrlModal,
|
||||||
ForServerVersion: FC<Versions>,
|
ForServerVersion: FC<Versions>,
|
||||||
) => ({ shortUrl, selectedServer }: ShortUrlsRowMenuProps) => {
|
) => ({ shortUrl, selectedServer }: ShortUrlsRowMenuProps) => {
|
||||||
const [ isOpen, toggle ] = useToggle();
|
const [ isOpen, toggle ] = useToggle();
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import { resetShortUrlParams } from '../reducers/shortUrlsListParams';
|
|||||||
import { editShortUrl } from '../reducers/shortUrlEdition';
|
import { editShortUrl } from '../reducers/shortUrlEdition';
|
||||||
import { ConnectDecorator } from '../../container/types';
|
import { ConnectDecorator } from '../../container/types';
|
||||||
import { ShortUrlsTable } from '../ShortUrlsTable';
|
import { ShortUrlsTable } from '../ShortUrlsTable';
|
||||||
|
import QrCodeModal from '../helpers/QrCodeModal';
|
||||||
|
|
||||||
const provideServices = (bottle: Bottle, connect: ConnectDecorator) => {
|
const provideServices = (bottle: Bottle, connect: ConnectDecorator) => {
|
||||||
// Components
|
// Components
|
||||||
@@ -40,6 +41,7 @@ const provideServices = (bottle: Bottle, connect: ConnectDecorator) => {
|
|||||||
'EditTagsModal',
|
'EditTagsModal',
|
||||||
'EditMetaModal',
|
'EditMetaModal',
|
||||||
'EditShortUrlModal',
|
'EditShortUrlModal',
|
||||||
|
'QrCodeModal',
|
||||||
'ForServerVersion',
|
'ForServerVersion',
|
||||||
);
|
);
|
||||||
bottle.serviceFactory('CreateShortUrlResult', CreateShortUrlResult, 'useStateFlagTimeout');
|
bottle.serviceFactory('CreateShortUrlResult', CreateShortUrlResult, 'useStateFlagTimeout');
|
||||||
@@ -69,6 +71,9 @@ const provideServices = (bottle: Bottle, connect: ConnectDecorator) => {
|
|||||||
bottle.serviceFactory('EditShortUrlModal', () => EditShortUrlModal);
|
bottle.serviceFactory('EditShortUrlModal', () => EditShortUrlModal);
|
||||||
bottle.decorator('EditShortUrlModal', connect([ 'shortUrlEdition' ], [ 'editShortUrl' ]));
|
bottle.decorator('EditShortUrlModal', connect([ 'shortUrlEdition' ], [ 'editShortUrl' ]));
|
||||||
|
|
||||||
|
bottle.serviceFactory('QrCodeModal', () => QrCodeModal);
|
||||||
|
bottle.decorator('QrCodeModal', connect([ 'selectedServer' ]));
|
||||||
|
|
||||||
// Services
|
// Services
|
||||||
bottle.serviceFactory('SearchBar', SearchBar, 'ColorGenerator');
|
bottle.serviceFactory('SearchBar', SearchBar, 'ColorGenerator');
|
||||||
bottle.decorator('SearchBar', connect([ 'shortUrlsListParams' ], [ 'listShortUrls' ]));
|
bottle.decorator('SearchBar', connect([ 'shortUrlsListParams' ], [ 'listShortUrls' ]));
|
||||||
|
|||||||
@@ -1,26 +1,41 @@
|
|||||||
import { shallow, ShallowWrapper } from 'enzyme';
|
import { shallow, ShallowWrapper } from 'enzyme';
|
||||||
import { ExternalLink } from 'react-external-link';
|
import { ExternalLink } from 'react-external-link';
|
||||||
|
import { ModalHeader } from 'reactstrap';
|
||||||
import { Mock } from 'ts-mockery';
|
import { Mock } from 'ts-mockery';
|
||||||
import QrCodeModal from '../../../src/short-urls/helpers/QrCodeModal';
|
import QrCodeModal from '../../../src/short-urls/helpers/QrCodeModal';
|
||||||
import { ShortUrl } from '../../../src/short-urls/data';
|
import { ShortUrl } from '../../../src/short-urls/data';
|
||||||
|
import { ReachableServer } from '../../../src/servers/data';
|
||||||
|
|
||||||
describe('<QrCodeModal />', () => {
|
describe('<QrCodeModal />', () => {
|
||||||
let wrapper: ShallowWrapper;
|
let wrapper: ShallowWrapper;
|
||||||
const shortUrl = 'https://doma.in/abc123';
|
const shortUrl = 'https://doma.in/abc123';
|
||||||
|
const createWrapper = (version = '2.5.0') => {
|
||||||
|
const selectedServer = Mock.of<ReachableServer>({ version });
|
||||||
|
|
||||||
beforeEach(() => {
|
wrapper = shallow(
|
||||||
wrapper = shallow(<QrCodeModal shortUrl={Mock.of<ShortUrl>({ shortUrl })} isOpen={true} toggle={() => {}} />);
|
<QrCodeModal
|
||||||
});
|
shortUrl={Mock.of<ShortUrl>({ shortUrl })}
|
||||||
afterEach(() => wrapper.unmount());
|
isOpen={true}
|
||||||
|
toggle={() => {}}
|
||||||
|
selectedServer={selectedServer}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
it('shows an external link to the URL', () => {
|
return wrapper;
|
||||||
const externalLink = wrapper.find(ExternalLink);
|
};
|
||||||
|
|
||||||
|
afterEach(() => wrapper?.unmount());
|
||||||
|
|
||||||
|
it('shows an external link to the URL in the header', () => {
|
||||||
|
const wrapper = createWrapper();
|
||||||
|
const externalLink = wrapper.find(ModalHeader).find(ExternalLink);
|
||||||
|
|
||||||
expect(externalLink).toHaveLength(1);
|
expect(externalLink).toHaveLength(1);
|
||||||
expect(externalLink.prop('href')).toEqual(shortUrl);
|
expect(externalLink.prop('href')).toEqual(shortUrl);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('displays an image with the QR code of the URL', () => {
|
it('displays an image with the QR code of the URL', () => {
|
||||||
|
const wrapper = createWrapper();
|
||||||
const img = wrapper.find('img');
|
const img = wrapper.find('img');
|
||||||
|
|
||||||
expect(img).toHaveLength(1);
|
expect(img).toHaveLength(1);
|
||||||
|
|||||||
Reference in New Issue
Block a user