mirror of
https://github.com/shlinkio/shlink-web-client.git
synced 2024-12-23 09:30:31 +03:00
Merge pull request #388 from acelaya-forks/feature/qr-code-margin
Feature/qr code margin
This commit is contained in:
commit
872890e674
8 changed files with 147 additions and 44 deletions
|
@ -6,7 +6,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|||
|
||||
## [Unreleased]
|
||||
### Added
|
||||
* [#379](https://github.com/shlinkio/shlink-web-client/issues/379) Improved QR code modal, including controls to customize size and format, as well as a button to copy the link to the clipboard.
|
||||
* [#379](https://github.com/shlinkio/shlink-web-client/issues/379) and [#384](https://github.com/shlinkio/shlink-web-client/issues/384) Improved QR code modal, including controls to customize size, format and margin, as well as a button to copy the link to the clipboard.
|
||||
|
||||
### Changed
|
||||
* *Nothing*
|
||||
|
|
6
package-lock.json
generated
6
package-lock.json
generated
|
@ -8056,9 +8056,9 @@
|
|||
"dev": true
|
||||
},
|
||||
"bootstrap": {
|
||||
"version": "4.5.3",
|
||||
"resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-4.5.3.tgz",
|
||||
"integrity": "sha512-o9ppKQioXGqhw8Z7mah6KdTYpNQY//tipnkxppWhPbiSWdD+1raYsnhwEZjkTHYbGee4cVQ0Rx65EhOY/HNLcQ=="
|
||||
"version": "4.6.0",
|
||||
"resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-4.6.0.tgz",
|
||||
"integrity": "sha512-Io55IuQY3kydzHtbGvQya3H+KorS/M9rSNyfCGCg9WZ4pyT/lCxIlpJgG1GXW/PswzC84Tr2fBYi+7+jFVQQBw=="
|
||||
},
|
||||
"bottlejs": {
|
||||
"version": "2.0.0",
|
||||
|
|
|
@ -26,7 +26,7 @@
|
|||
"@fortawesome/free-solid-svg-icons": "^5.15.1",
|
||||
"@fortawesome/react-fontawesome": "^0.1.12",
|
||||
"axios": "^0.21.0",
|
||||
"bootstrap": "^4.5.3",
|
||||
"bootstrap": "^4.6.0",
|
||||
"bottlejs": "^2.0.0",
|
||||
"bowser": "^2.11.0",
|
||||
"chart.js": "^2.9.4",
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
.qr-code-modal__img {
|
||||
max-width: 100%;
|
||||
box-shadow: 0 0 .25rem rgb(0 0 0 / .2);
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { useMemo, useState } from 'react';
|
||||
import { DropdownItem, FormGroup, Modal, ModalBody, ModalHeader, Row } from 'reactstrap';
|
||||
import { ExternalLink } from 'react-external-link';
|
||||
import classNames from 'classnames';
|
||||
import { ShortUrlModalProps } from '../data';
|
||||
import { ReachableServer } from '../../servers/data';
|
||||
import { versionMatch } from '../../utils/helpers/version';
|
||||
|
@ -15,22 +16,25 @@ interface QrCodeModalConnectProps extends ShortUrlModalProps {
|
|||
|
||||
const QrCodeModal = ({ shortUrl: { shortUrl }, toggle, isOpen, selectedServer }: QrCodeModalConnectProps) => {
|
||||
const [ size, setSize ] = useState(300);
|
||||
const [ margin, setMargin ] = useState(0);
|
||||
const [ format, setFormat ] = useState<QrCodeFormat>('png');
|
||||
const capabilities: QrCodeCapabilities = useMemo(() => ({
|
||||
useSizeInPath: !versionMatch(selectedServer.version, { minVersion: '2.5.0' }),
|
||||
svgIsSupported: versionMatch(selectedServer.version, { minVersion: '2.4.0' }),
|
||||
marginIsSupported: versionMatch(selectedServer.version, { minVersion: '2.6.0' }),
|
||||
}), [ selectedServer ]);
|
||||
const qrCodeUrl = useMemo(
|
||||
() => buildQrCodeUrl(shortUrl, size, format, capabilities),
|
||||
[ shortUrl, size, format, capabilities ],
|
||||
() => buildQrCodeUrl(shortUrl, { size, format, margin }, capabilities),
|
||||
[ shortUrl, size, format, margin, capabilities ],
|
||||
);
|
||||
const totalSize = useMemo(() => size + margin, [ size, margin ]);
|
||||
const modalSize = useMemo(() => {
|
||||
if (size < 500) {
|
||||
if (totalSize < 500) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return size < 800 ? 'lg' : 'xl';
|
||||
}, [ size ]);
|
||||
return totalSize < 800 ? 'lg' : 'xl';
|
||||
}, [ totalSize ]);
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen} toggle={toggle} centered size={modalSize}>
|
||||
|
@ -39,7 +43,13 @@ const QrCodeModal = ({ shortUrl: { shortUrl }, toggle, isOpen, selectedServer }:
|
|||
</ModalHeader>
|
||||
<ModalBody>
|
||||
<Row className="mb-2">
|
||||
<div className={capabilities.svgIsSupported ? 'col-md-6' : 'col-12'}>
|
||||
<div
|
||||
className={classNames({
|
||||
'col-md-4': capabilities.marginIsSupported && capabilities.svgIsSupported,
|
||||
'col-md-6': (!capabilities.marginIsSupported && capabilities.svgIsSupported) || (capabilities.marginIsSupported && !capabilities.svgIsSupported),
|
||||
'col-12': !capabilities.marginIsSupported && !capabilities.svgIsSupported,
|
||||
})}
|
||||
>
|
||||
<FormGroup>
|
||||
<label className="mb-0">Size: {size}px</label>
|
||||
<input
|
||||
|
@ -53,8 +63,24 @@ const QrCodeModal = ({ shortUrl: { shortUrl }, toggle, isOpen, selectedServer }:
|
|||
/>
|
||||
</FormGroup>
|
||||
</div>
|
||||
{capabilities.marginIsSupported && (
|
||||
<div className={capabilities.svgIsSupported ? 'col-md-4' : 'col-md-6'}>
|
||||
<FormGroup>
|
||||
<label className="mb-0">Margin: {margin}px</label>
|
||||
<input
|
||||
type="range"
|
||||
className="form-control-range"
|
||||
value={margin}
|
||||
step={1}
|
||||
min={0}
|
||||
max={100}
|
||||
onChange={(e) => setMargin(Number(e.target.value))}
|
||||
/>
|
||||
</FormGroup>
|
||||
</div>
|
||||
)}
|
||||
{capabilities.svgIsSupported && (
|
||||
<div className="col-md-6">
|
||||
<div className={capabilities.marginIsSupported ? 'col-md-4' : '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>
|
||||
|
|
|
@ -1,25 +1,31 @@
|
|||
import { always, cond } from 'ramda';
|
||||
import { isEmpty } from 'ramda';
|
||||
import { stringifyQuery } from './query';
|
||||
|
||||
export interface QrCodeCapabilities {
|
||||
useSizeInPath: boolean;
|
||||
svgIsSupported: boolean;
|
||||
marginIsSupported: boolean;
|
||||
}
|
||||
|
||||
export type QrCodeFormat = 'svg' | 'png';
|
||||
|
||||
export interface QrCodeOptions {
|
||||
size: number;
|
||||
format: QrCodeFormat;
|
||||
margin: number;
|
||||
}
|
||||
|
||||
export const buildQrCodeUrl = (
|
||||
shortUrl: string,
|
||||
size: number,
|
||||
format: QrCodeFormat,
|
||||
{ useSizeInPath, svgIsSupported }: QrCodeCapabilities,
|
||||
{ size, format, margin }: QrCodeOptions,
|
||||
{ useSizeInPath, svgIsSupported, marginIsSupported }: QrCodeCapabilities,
|
||||
): string => {
|
||||
const sizeFragment = useSizeInPath ? `/${size}` : `?size=${size}`;
|
||||
const formatFragment = !svgIsSupported ? '' : `format=${format}`;
|
||||
const joinSymbolResolver = cond([
|
||||
[ () => useSizeInPath && svgIsSupported, always('?') ],
|
||||
[ () => !useSizeInPath && svgIsSupported, always('&') ],
|
||||
]);
|
||||
const joinSymbol = joinSymbolResolver() ?? '';
|
||||
const baseUrl = `${shortUrl}/qr-code${useSizeInPath ? `/${size}` : ''}`;
|
||||
const query = stringifyQuery({
|
||||
size: useSizeInPath ? undefined : size,
|
||||
format: svgIsSupported ? format : undefined,
|
||||
margin: marginIsSupported && margin > 0 ? margin : undefined,
|
||||
});
|
||||
|
||||
return `${shortUrl}/qr-code${sizeFragment}${joinSymbol}${formatFragment}`;
|
||||
return `${baseUrl}${isEmpty(query) ? '' : `?${query}`}`;
|
||||
};
|
||||
|
|
|
@ -11,7 +11,7 @@ import { DropdownBtn } from '../../../src/utils/DropdownBtn';
|
|||
describe('<QrCodeModal />', () => {
|
||||
let wrapper: ShallowWrapper;
|
||||
const shortUrl = 'https://doma.in/abc123';
|
||||
const createWrapper = (version = '2.5.0') => {
|
||||
const createWrapper = (version = '2.6.0') => {
|
||||
const selectedServer = Mock.of<ReachableServer>({ version });
|
||||
|
||||
wrapper = shallow(
|
||||
|
@ -37,11 +37,20 @@ describe('<QrCodeModal />', () => {
|
|||
});
|
||||
|
||||
it.each([
|
||||
[ '2.3.0', '/qr-code/300' ],
|
||||
[ '2.4.0', '/qr-code/300?format=png' ],
|
||||
[ '2.5.0', '/qr-code?size=300&format=png' ],
|
||||
])('displays an image with the QR code of the URL', (version, expectedUrl) => {
|
||||
[ '2.3.0', 0, '/qr-code/300' ],
|
||||
[ '2.4.0', 0, '/qr-code/300?format=png' ],
|
||||
[ '2.4.0', 10, '/qr-code/300?format=png' ],
|
||||
[ '2.5.0', 0, '/qr-code?size=300&format=png' ],
|
||||
[ '2.6.0', 0, '/qr-code?size=300&format=png' ],
|
||||
[ '2.6.0', 10, '/qr-code?size=300&format=png&margin=10' ],
|
||||
])('displays an image with the QR code of the URL', (version, margin, expectedUrl) => {
|
||||
const wrapper = createWrapper(version);
|
||||
const formControls = wrapper.find('.form-control-range');
|
||||
|
||||
if (formControls.length > 1) {
|
||||
formControls.at(1).simulate('change', { target: { value: `${margin}` } });
|
||||
}
|
||||
|
||||
const modalBody = wrapper.find(ModalBody);
|
||||
const img = modalBody.find('img');
|
||||
const linkInBody = modalBody.find(ExternalLink);
|
||||
|
@ -53,23 +62,31 @@ describe('<QrCodeModal />', () => {
|
|||
});
|
||||
|
||||
it.each([
|
||||
[ 530, 'lg' ],
|
||||
[ 200, undefined ],
|
||||
[ 830, 'xl' ],
|
||||
])('renders expected size', (size, modalSize) => {
|
||||
[ 530, 0, 'lg' ],
|
||||
[ 200, 0, undefined ],
|
||||
[ 830, 0, 'xl' ],
|
||||
[ 430, 80, 'lg' ],
|
||||
[ 200, 50, undefined ],
|
||||
[ 720, 100, 'xl' ],
|
||||
])('renders expected size', (size, margin, modalSize) => {
|
||||
const wrapper = createWrapper();
|
||||
const sizeInput = wrapper.find('.form-control-range');
|
||||
const formControls = wrapper.find('.form-control-range');
|
||||
const sizeInput = formControls.at(0);
|
||||
const marginInput = formControls.at(1);
|
||||
|
||||
sizeInput.simulate('change', { target: { value: `${size}` } });
|
||||
marginInput.simulate('change', { target: { value: `${margin}` } });
|
||||
|
||||
expect(wrapper.find('.mt-2').text()).toEqual(`${size}x${size}`);
|
||||
expect(wrapper.find('label').text()).toEqual(`Size: ${size}px`);
|
||||
expect(wrapper.find('label').at(0).text()).toEqual(`Size: ${size}px`);
|
||||
expect(wrapper.find('label').at(1).text()).toEqual(`Margin: ${margin}px`);
|
||||
expect(wrapper.find(Modal).prop('size')).toEqual(modalSize);
|
||||
});
|
||||
|
||||
it.each([
|
||||
[ '2.3.0', 0, 'col-12' ],
|
||||
[ '2.4.0', 1, 'col-md-6' ],
|
||||
[ '2.6.0', 1, 'col-md-4' ],
|
||||
])('shows expected components based on server version', (version, expectedAmountOfDropdowns, expectedRangeClass) => {
|
||||
const wrapper = createWrapper(version);
|
||||
const dropdown = wrapper.find(DropdownBtn);
|
||||
|
|
|
@ -3,15 +3,68 @@ import { buildQrCodeUrl, QrCodeFormat } from '../../../src/utils/helpers/qrCodes
|
|||
describe('qrCodes', () => {
|
||||
describe('buildQrCodeUrl', () => {
|
||||
test.each([
|
||||
[ 'foo.com', 530, 'svg', { useSizeInPath: true, svgIsSupported: true }, 'foo.com/qr-code/530?format=svg' ],
|
||||
[ 'foo.com', 530, 'png', { useSizeInPath: true, svgIsSupported: true }, 'foo.com/qr-code/530?format=png' ],
|
||||
[ 'bar.io', 870, 'svg', { useSizeInPath: false, svgIsSupported: false }, 'bar.io/qr-code?size=870' ],
|
||||
[ 'bar.io', 200, 'png', { useSizeInPath: false, svgIsSupported: true }, 'bar.io/qr-code?size=200&format=png' ],
|
||||
[ 'bar.io', 200, 'svg', { useSizeInPath: false, svgIsSupported: true }, 'bar.io/qr-code?size=200&format=svg' ],
|
||||
[ 'foo.net', 480, 'png', { useSizeInPath: true, svgIsSupported: false }, 'foo.net/qr-code/480' ],
|
||||
[ 'foo.net', 480, 'svg', { useSizeInPath: true, svgIsSupported: false }, 'foo.net/qr-code/480' ],
|
||||
])('builds expected URL based in params', (shortUrl, size, format, capabilities, expectedUrl) => {
|
||||
expect(buildQrCodeUrl(shortUrl, size, format as QrCodeFormat, capabilities)).toEqual(expectedUrl);
|
||||
[
|
||||
'foo.com',
|
||||
{ size: 530, format: 'svg' as QrCodeFormat, margin: 0 },
|
||||
{ useSizeInPath: true, svgIsSupported: true, marginIsSupported: false },
|
||||
'foo.com/qr-code/530?format=svg',
|
||||
],
|
||||
[
|
||||
'foo.com',
|
||||
{ size: 530, format: 'png' as QrCodeFormat, margin: 0 },
|
||||
{ useSizeInPath: true, svgIsSupported: true, marginIsSupported: false },
|
||||
'foo.com/qr-code/530?format=png',
|
||||
],
|
||||
[
|
||||
'bar.io',
|
||||
{ size: 870, format: 'svg' as QrCodeFormat, margin: 0 },
|
||||
{ useSizeInPath: false, svgIsSupported: false, marginIsSupported: false },
|
||||
'bar.io/qr-code?size=870',
|
||||
],
|
||||
[
|
||||
'bar.io',
|
||||
{ size: 200, format: 'png' as QrCodeFormat, margin: 0 },
|
||||
{ useSizeInPath: false, svgIsSupported: true, marginIsSupported: false },
|
||||
'bar.io/qr-code?size=200&format=png',
|
||||
],
|
||||
[
|
||||
'bar.io',
|
||||
{ size: 200, format: 'svg' as QrCodeFormat, margin: 0 },
|
||||
{ useSizeInPath: false, svgIsSupported: true, marginIsSupported: false },
|
||||
'bar.io/qr-code?size=200&format=svg',
|
||||
],
|
||||
[
|
||||
'foo.net',
|
||||
{ size: 480, format: 'png' as QrCodeFormat, margin: 0 },
|
||||
{ useSizeInPath: true, svgIsSupported: false, marginIsSupported: false },
|
||||
'foo.net/qr-code/480',
|
||||
],
|
||||
[
|
||||
'foo.net',
|
||||
{ size: 480, format: 'svg' as QrCodeFormat, margin: 0 },
|
||||
{ useSizeInPath: true, svgIsSupported: false, marginIsSupported: false },
|
||||
'foo.net/qr-code/480',
|
||||
],
|
||||
[
|
||||
'shlink.io',
|
||||
{ size: 123, format: 'svg' as QrCodeFormat, margin: 10 },
|
||||
{ useSizeInPath: true, svgIsSupported: false, marginIsSupported: false },
|
||||
'shlink.io/qr-code/123',
|
||||
],
|
||||
[
|
||||
'shlink.io',
|
||||
{ size: 456, format: 'png' as QrCodeFormat, margin: 10 },
|
||||
{ useSizeInPath: true, svgIsSupported: true, marginIsSupported: true },
|
||||
'shlink.io/qr-code/456?format=png&margin=10',
|
||||
],
|
||||
[
|
||||
'shlink.io',
|
||||
{ size: 456, format: 'png' as QrCodeFormat, margin: 0 },
|
||||
{ useSizeInPath: true, svgIsSupported: true, marginIsSupported: true },
|
||||
'shlink.io/qr-code/456?format=png',
|
||||
],
|
||||
])('builds expected URL based in params', (shortUrl, options, capabilities, expectedUrl) => {
|
||||
expect(buildQrCodeUrl(shortUrl, options, capabilities)).toEqual(expectedUrl);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Reference in a new issue