Skip to content

Commit b817bae

Browse files
authored
Merge pull request #454 from acelaya-forks/feature/qr-code-defaults
Make QR codes start with default options from server
2 parents 85fd275 + f59b06a commit b817bae

14 files changed

+268
-122
lines changed

Diff for: CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
1212
* [#307](https://github.com/shlinkio/shlink-web-component/issues/307) Add new setting to disable short URL deletions confirmation.
1313
* [#435](https://github.com/shlinkio/shlink-web-component/issues/435) Allow toggling between displaying raw user agent and parsed browser/OS in visits table.
1414
* [#197](https://github.com/shlinkio/shlink-web-component/issues/197) Allow line charts to be expanded to the full size of the viewport, both in individual visits views, and when comparing visits.
15+
* [#382](https://github.com/shlinkio/shlink-web-component/issues/382) Initialize QR code modal with all params unset, so that they fall back to the server defaults. Additionally, allow them to be unset if desired.
1516

1617
### Changed
1718
* Update to `@shlinkio/eslint-config-js-coding-standard` 3.0, and migrate to ESLint flat config.

Diff for: src/short-urls/helpers/QrCodeModal.scss

-4
This file was deleted.

Diff for: src/short-urls/helpers/QrCodeModal.tsx

+47-44
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { faFileDownload as downloadIcon } from '@fortawesome/free-solid-svg-icons';
22
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
3-
import { useMemo, useState } from 'react';
3+
import type { SyntheticEvent } from 'react';
4+
import { useCallback, useMemo, useState } from 'react';
45
import { ExternalLink } from 'react-external-link';
56
import { Button, FormGroup, Modal, ModalBody, ModalHeader, Row } from 'reactstrap';
67
import type { FCWithDeps } from '../../container/utils';
@@ -10,9 +11,9 @@ import type { QrCodeFormat, QrErrorCorrection } from '../../utils/helpers/qrCode
1011
import { buildQrCodeUrl } from '../../utils/helpers/qrCodes';
1112
import type { ImageDownloader } from '../../utils/services/ImageDownloader';
1213
import type { ShortUrlModalProps } from '../data';
14+
import { QrDimensionControl } from './qr-codes/QrDimensionControl';
1315
import { QrErrorCorrectionDropdown } from './qr-codes/QrErrorCorrectionDropdown';
1416
import { QrFormatDropdown } from './qr-codes/QrFormatDropdown';
15-
import './QrCodeModal.scss';
1617

1718
type QrCodeModalDeps = {
1819
ImageDownloader: ImageDownloader
@@ -22,22 +23,25 @@ const QrCodeModal: FCWithDeps<ShortUrlModalProps, QrCodeModalDeps> = (
2223
{ shortUrl: { shortUrl, shortCode }, toggle, isOpen },
2324
) => {
2425
const { ImageDownloader: imageDownloader } = useDependencies(QrCodeModal);
25-
const [size, setSize] = useState(300);
26-
const [margin, setMargin] = useState(0);
27-
const [format, setFormat] = useState<QrCodeFormat>('png');
28-
const [errorCorrection, setErrorCorrection] = useState<QrErrorCorrection>('L');
26+
const [size, setSize] = useState<number>();
27+
const [margin, setMargin] = useState<number>();
28+
const [format, setFormat] = useState<QrCodeFormat>();
29+
const [errorCorrection, setErrorCorrection] = useState<QrErrorCorrection>();
2930
const qrCodeUrl = useMemo(
3031
() => buildQrCodeUrl(shortUrl, { size, format, margin, errorCorrection }),
3132
[shortUrl, size, format, margin, errorCorrection],
3233
);
33-
const totalSize = useMemo(() => size + margin, [size, margin]);
34-
const modalSize = useMemo(() => {
35-
if (totalSize < 500) {
36-
return undefined;
37-
}
34+
const [modalSize, setModalSize] = useState<'lg' | 'xl'>();
35+
const onImageLoad = useCallback((e: SyntheticEvent<HTMLImageElement>) => {
36+
const image = e.target as HTMLImageElement;
37+
const { naturalWidth } = image;
3838

39-
return totalSize < 800 ? 'lg' : 'xl';
40-
}, [totalSize]);
39+
if (naturalWidth < 500) {
40+
setModalSize(undefined);
41+
} else {
42+
setModalSize(naturalWidth < 800 ? 'lg' : 'xl');
43+
}
44+
}, []);
4145

4246
return (
4347
<Modal isOpen={isOpen} toggle={toggle} centered size={modalSize}>
@@ -46,45 +50,44 @@ const QrCodeModal: FCWithDeps<ShortUrlModalProps, QrCodeModalDeps> = (
4650
</ModalHeader>
4751
<ModalBody>
4852
<Row>
49-
<FormGroup className="d-grid col-md-6">
50-
<label htmlFor="sizeControl">Size: {size}px</label>
51-
<input
52-
id="sizeControl"
53-
type="range"
54-
className="form-control-range"
55-
value={size}
56-
step={10}
57-
min={50}
58-
max={1000}
59-
onChange={(e) => setSize(Number(e.target.value))}
60-
/>
61-
</FormGroup>
62-
<FormGroup className="d-grid col-md-6">
63-
<label htmlFor="marginControl">Margin: {margin}px</label>
64-
<input
65-
id="marginControl"
66-
type="range"
67-
className="form-control-range"
68-
value={margin}
69-
step={1}
70-
min={0}
71-
max={100}
72-
onChange={(e) => setMargin(Number(e.target.value))}
73-
/>
74-
</FormGroup>
75-
<FormGroup className="d-grid col-md-6">
76-
<QrFormatDropdown format={format} setFormat={setFormat} />
53+
<QrDimensionControl
54+
className="col-sm-6"
55+
name="size"
56+
value={size}
57+
step={10}
58+
min={50}
59+
max={1000}
60+
initial={300}
61+
onChange={setSize}
62+
/>
63+
<QrDimensionControl
64+
className="col-sm-6"
65+
name="margin"
66+
value={margin}
67+
step={1}
68+
min={0}
69+
max={100}
70+
onChange={setMargin}
71+
/>
72+
<FormGroup className="d-grid col-sm-6">
73+
<QrFormatDropdown format={format} onChange={setFormat} />
7774
</FormGroup>
78-
<FormGroup className="col-md-6">
79-
<QrErrorCorrectionDropdown errorCorrection={errorCorrection} setErrorCorrection={setErrorCorrection} />
75+
<FormGroup className="col-sm-6">
76+
<QrErrorCorrectionDropdown errorCorrection={errorCorrection} onChange={setErrorCorrection} />
8077
</FormGroup>
8178
</Row>
8279
<div className="text-center">
8380
<div className="mb-3">
8481
<ExternalLink href={qrCodeUrl} />
8582
<CopyToClipboardIcon text={qrCodeUrl} />
8683
</div>
87-
<img src={qrCodeUrl} className="qr-code-modal__img" alt="QR code" />
84+
<img
85+
src={qrCodeUrl}
86+
alt="QR code"
87+
className="shadow-lg"
88+
style={{ maxWidth: '100%' }}
89+
onLoad={onImageLoad}
90+
/>
8891
<div className="mt-3">
8992
<Button
9093
block
+65
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import { faArrowRotateLeft } from '@fortawesome/free-solid-svg-icons';
2+
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
3+
import type { FC } from 'react';
4+
import { useId } from 'react';
5+
import { Button, FormGroup } from 'reactstrap';
6+
7+
export type QrCodeDimensionControlProps = {
8+
name: string;
9+
value?: number;
10+
step?: number;
11+
min?: number;
12+
max?: number;
13+
initial?: number;
14+
onChange: (newValue?: number) => void;
15+
className?: string;
16+
};
17+
18+
export const QrDimensionControl: FC<QrCodeDimensionControlProps> = (
19+
{ name, value, step, min, max, onChange, className, initial = min },
20+
) => {
21+
const id = useId();
22+
23+
return (
24+
<FormGroup className={className}>
25+
{value === undefined && (
26+
<Button
27+
outline
28+
color="link"
29+
className="text-start fst-italic w-100"
30+
style={{ color: 'var(--input-text-color)', borderColor: 'var(--border-color)' }}
31+
onClick={() => onChange(initial)}
32+
>
33+
Customize {name}
34+
</Button>
35+
)}
36+
{value !== undefined && (
37+
<div className="d-flex gap-3">
38+
<div className="d-flex flex-column flex-grow-1">
39+
<label htmlFor={id} className="text-capitalize">{name}: {value}px</label>
40+
<input
41+
id={id}
42+
type="range"
43+
className="form-control-range"
44+
value={value}
45+
step={step}
46+
min={min}
47+
max={max}
48+
onChange={(e) => onChange(Number(e.target.value))}
49+
/>
50+
</div>
51+
<Button
52+
aria-label={`Default ${name}`}
53+
title={`Default ${name}`}
54+
outline
55+
color="link"
56+
onClick={() => onChange(undefined)}
57+
style={{ color: 'var(--input-text-color)', borderColor: 'var(--border-color)' }}
58+
>
59+
<FontAwesomeIcon icon={faArrowRotateLeft} />
60+
</Button>
61+
</div>
62+
)}
63+
</FormGroup>
64+
);
65+
};

Diff for: src/short-urls/helpers/qr-codes/QrErrorCorrectionDropdown.tsx

+10-8
Original file line numberDiff line numberDiff line change
@@ -4,24 +4,26 @@ import { DropdownItem } from 'reactstrap';
44
import type { QrErrorCorrection } from '../../../utils/helpers/qrCodes';
55

66
interface QrErrorCorrectionDropdownProps {
7-
errorCorrection: QrErrorCorrection;
8-
setErrorCorrection: (errorCorrection: QrErrorCorrection) => void;
7+
errorCorrection?: QrErrorCorrection;
8+
onChange: (errorCorrection?: QrErrorCorrection) => void;
99
}
1010

1111
export const QrErrorCorrectionDropdown: FC<QrErrorCorrectionDropdownProps> = (
12-
{ errorCorrection, setErrorCorrection },
12+
{ errorCorrection, onChange },
1313
) => (
14-
<DropdownBtn text={`Error correction (${errorCorrection})`}>
15-
<DropdownItem active={errorCorrection === 'L'} onClick={() => setErrorCorrection('L')}>
14+
<DropdownBtn text={errorCorrection ? `Error correction (${errorCorrection})` : <i>Default error correction</i>}>
15+
<DropdownItem active={!errorCorrection} onClick={() => onChange(undefined)}>Default</DropdownItem>
16+
<DropdownItem divider tag="hr" />
17+
<DropdownItem active={errorCorrection === 'L'} onClick={() => onChange('L')}>
1618
<b>L</b>ow
1719
</DropdownItem>
18-
<DropdownItem active={errorCorrection === 'M'} onClick={() => setErrorCorrection('M')}>
20+
<DropdownItem active={errorCorrection === 'M'} onClick={() => onChange('M')}>
1921
<b>M</b>edium
2022
</DropdownItem>
21-
<DropdownItem active={errorCorrection === 'Q'} onClick={() => setErrorCorrection('Q')}>
23+
<DropdownItem active={errorCorrection === 'Q'} onClick={() => onChange('Q')}>
2224
<b>Q</b>uartile
2325
</DropdownItem>
24-
<DropdownItem active={errorCorrection === 'H'} onClick={() => setErrorCorrection('H')}>
26+
<DropdownItem active={errorCorrection === 'H'} onClick={() => onChange('H')}>
2527
<b>H</b>igh
2628
</DropdownItem>
2729
</DropdownBtn>

Diff for: src/short-urls/helpers/qr-codes/QrFormatDropdown.tsx

+8-6
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,15 @@ import { DropdownItem } from 'reactstrap';
44
import type { QrCodeFormat } from '../../../utils/helpers/qrCodes';
55

66
interface QrFormatDropdownProps {
7-
format: QrCodeFormat;
8-
setFormat: (format: QrCodeFormat) => void;
7+
format?: QrCodeFormat;
8+
onChange: (format?: QrCodeFormat) => void;
99
}
1010

11-
export const QrFormatDropdown: FC<QrFormatDropdownProps> = ({ format, setFormat }) => (
12-
<DropdownBtn text={`Format (${format})`}>
13-
<DropdownItem active={format === 'png'} onClick={() => setFormat('png')}>PNG</DropdownItem>
14-
<DropdownItem active={format === 'svg'} onClick={() => setFormat('svg')}>SVG</DropdownItem>
11+
export const QrFormatDropdown: FC<QrFormatDropdownProps> = ({ format, onChange }) => (
12+
<DropdownBtn text={format ? `Format (${format})` : <i>Default format</i>}>
13+
<DropdownItem active={!format} onClick={() => onChange(undefined)}>Default</DropdownItem>
14+
<DropdownItem divider tag="hr" />
15+
<DropdownItem active={format === 'png'} onClick={() => onChange('png')}>PNG</DropdownItem>
16+
<DropdownItem active={format === 'svg'} onClick={() => onChange('svg')}>SVG</DropdownItem>
1517
</DropdownBtn>
1618
);

Diff for: src/utils/helpers/qrCodes.ts

+6-9
Original file line numberDiff line numberDiff line change
@@ -5,18 +5,15 @@ export type QrCodeFormat = 'svg' | 'png';
55
export type QrErrorCorrection = 'L' | 'M' | 'Q' | 'H';
66

77
export interface QrCodeOptions {
8-
size: number;
9-
format: QrCodeFormat;
10-
margin: number;
11-
errorCorrection: QrErrorCorrection;
8+
size?: number;
9+
format?: QrCodeFormat;
10+
margin?: number;
11+
errorCorrection?: QrErrorCorrection;
1212
}
1313

14-
export const buildQrCodeUrl = (shortUrl: string, { margin, ...options }: QrCodeOptions): string => {
14+
export const buildQrCodeUrl = (shortUrl: string, options: QrCodeOptions): string => {
1515
const baseUrl = `${shortUrl}/qr-code`;
16-
const query = stringifyQueryParams({
17-
...options,
18-
margin: margin > 0 ? margin : undefined,
19-
});
16+
const query = stringifyQueryParams({ ...options });
2017

2118
return `${baseUrl}${!query ? '' : `?${query}`}`;
2219
};

Diff for: src/visits/helpers/OpenMapModalBtn.tsx

+4-6
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import { faMapMarkedAlt as mapIcon } from '@fortawesome/free-solid-svg-icons';
22
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
3-
import { useDomId, useToggle } from '@shlinkio/shlink-frontend-kit';
3+
import { useToggle } from '@shlinkio/shlink-frontend-kit';
44
import { useCallback, useState } from 'react';
5-
import { Button, Dropdown, DropdownItem, DropdownMenu, DropdownToggle, UncontrolledTooltip } from 'reactstrap';
5+
import { Button, Dropdown, DropdownItem, DropdownMenu, DropdownToggle } from 'reactstrap';
66
import type { CityStats } from '../types';
77
import { MapModal } from './MapModal';
88

@@ -16,7 +16,6 @@ export const OpenMapModalBtn = ({ modalTitle, activeCities, locations = [] }: Op
1616
const [mapIsOpened, , openMap, closeMap] = useToggle();
1717
const [dropdownIsOpened, toggleDropdown] = useToggle();
1818
const [locationsToShow, setLocationsToShow] = useState<CityStats[]>([]);
19-
const id = useDomId();
2019

2120
const openMapWithCities = useCallback((filterCallback?: (city: CityStats) => boolean) => {
2221
setLocationsToShow(!filterCallback ? locations : locations.filter(filterCallback));
@@ -29,16 +28,16 @@ export const OpenMapModalBtn = ({ modalTitle, activeCities, locations = [] }: Op
2928
<Button
3029
color="link"
3130
className="p-0"
32-
id={id}
3331
onClick={() => openMapWithCities()}
3432
aria-label="Show in map"
33+
title="Show in map"
3534
>
3635
<FontAwesomeIcon icon={mapIcon} />
3736
</Button>
3837
)}
3938
{activeCities && (
4039
<Dropdown isOpen={dropdownIsOpened} toggle={toggleDropdown}>
41-
<DropdownToggle color="link" className="p-0" id={id}>
40+
<DropdownToggle color="link" className="p-0" title="Show in map">
4241
<FontAwesomeIcon icon={mapIcon} />
4342
</DropdownToggle>
4443
<DropdownMenu end>
@@ -49,7 +48,6 @@ export const OpenMapModalBtn = ({ modalTitle, activeCities, locations = [] }: Op
4948
</DropdownMenu>
5049
</Dropdown>
5150
)}
52-
<UncontrolledTooltip placement="left" target={id}>Show in map</UncontrolledTooltip>
5351
<MapModal toggle={closeMap} isOpen={mapIsOpened} title={modalTitle} locations={locationsToShow} />
5452
</>
5553
);

0 commit comments

Comments
 (0)