Skip to content

Commit 478679c

Browse files
authored
Merge pull request #516 from acelaya-forks/feature/geolocation-rules
Geolocation redirect conditions
2 parents b95493f + fe86272 commit 478679c

File tree

10 files changed

+381
-53
lines changed

10 files changed

+381
-53
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
77
## [Unreleased]
88
### Added
99
* [#491](https://github.com/shlinkio/shlink-web-component/issues/491) Add support for colors in QR code configurator.
10+
* [#515](https://github.com/shlinkio/shlink-web-component/issues/515) Add support for geolocation redirect conditions when using Shlink 4.3 or newer.
1011

1112
### Changed
1213
* *Nothing*

package-lock.json

Lines changed: 5 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@
4747
"@fortawesome/react-fontawesome": "^0.2.0",
4848
"@reduxjs/toolkit": "^2.0.1",
4949
"@shlinkio/shlink-frontend-kit": "^0.6.0",
50-
"@shlinkio/shlink-js-sdk": "^1.2.0",
50+
"@shlinkio/shlink-js-sdk": "^1.3.0",
5151
"react": "^18.2.0",
5252
"react-dom": "^18.2.0",
5353
"react-redux": "^9.0.1",

src/redirect-rules/helpers/RedirectRuleCard.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,8 @@ export const RedirectRuleCard: FC<RedirectRuleCardProps> = (
6767
<>Query string contains {condition.matchKey}={condition.matchValue}</>
6868
)}
6969
{condition.type === 'ip-address' && <>IP address matches {condition.matchValue}</>}
70+
{condition.type === 'geolocation-country-code' && <>Country code is {condition.matchValue}</>}
71+
{condition.type === 'geolocation-city-name' && <>City name is {condition.matchValue}</>}
7072
</div>
7173
))}
7274
</div>

src/redirect-rules/helpers/RedirectRuleModal.tsx

Lines changed: 49 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,15 @@ import type { FC, FormEvent } from 'react';
1010
import { useMemo } from 'react';
1111
import { useCallback, useId, useState } from 'react';
1212
import { Button, Input, Modal, ModalBody, ModalFooter, ModalHeader, Row } from 'reactstrap';
13+
import { countryCodes } from '../../utils/country-codes';
1314
import { useFeature } from '../../utils/features';
1415
import './RedirectRuleModal.scss';
1516

1617
const deviceNames = {
1718
android: 'Android',
1819
ios: 'iOS',
1920
desktop: 'Desktop',
20-
} satisfies Record<string, string>;
21+
} as const satisfies Record<string, string>;
2122

2223
type DeviceType = keyof typeof deviceNames;
2324

@@ -116,12 +117,40 @@ const QueryParamControls: FC<{
116117
);
117118
};
118119

119-
const IpAddressControls: FC<{ ipAddress?: string; onIpAddressChange: (lang: string) => void; }> = (
120+
const IpAddressControls: FC<{ ipAddress?: string; onIpAddressChange: (ipAddress: string) => void; }> = (
120121
{ ipAddress, onIpAddressChange },
121122
) => (
122123
<PlainValueControls value={ipAddress} onValueChange={onIpAddressChange} label="IP address" placeholder="192.168.1.10" />
123124
);
124125

126+
const CountryCodeControls: FC<{ countryCode?: string; onCountryCodeChange: (countryCode: string) => void }> = ({
127+
countryCode,
128+
onCountryCodeChange,
129+
}) => {
130+
const countryCodeId = useId();
131+
return (
132+
<div>
133+
<label htmlFor={countryCodeId}>Country:</label>
134+
<select
135+
id={countryCodeId}
136+
className="form-select"
137+
value={countryCode}
138+
onChange={(e) => onCountryCodeChange(e.target.value)}
139+
required
140+
>
141+
{!countryCode && <option value="">- Select country -</option>}
142+
{Object.entries(countryCodes).map(([code, name]) => <option key={code} value={code}>{name}</option>)}
143+
</select>
144+
</div>
145+
);
146+
};
147+
148+
const CityNameControls: FC<{ cityName?: string; onCityNameChange: (cityName: string) => void; }> = (
149+
{ cityName, onCityNameChange },
150+
) => (
151+
<PlainValueControls value={cityName} onValueChange={onCityNameChange} label="City name" placeholder="New York" />
152+
);
153+
125154
const Condition: FC<{
126155
condition: ShlinkRedirectCondition;
127156
onConditionChange: (condition: ShlinkRedirectCondition) => void;
@@ -142,21 +171,25 @@ const Condition: FC<{
142171
[condition, onConditionChange],
143172
);
144173
const supportsIpRedirectCondition = useFeature('ipRedirectCondition');
174+
const supportsGeolocationRedirectCondition = useFeature('geolocationRedirectCondition');
145175
const conditionNames = useMemo((): Partial<Record<ShlinkRedirectConditionType, string>> => {
146-
const commonConditionNames: Partial<Record<ShlinkRedirectConditionType, string>> = {
176+
const conditionNames: Partial<Record<ShlinkRedirectConditionType, string>> = {
147177
device: 'Device type',
148178
language: 'Language',
149179
'query-param': 'Query param',
150180
};
151-
if (!supportsIpRedirectCondition) {
152-
return commonConditionNames;
181+
182+
if (supportsIpRedirectCondition) {
183+
conditionNames['ip-address'] = 'IP address';
153184
}
154185

155-
return {
156-
...commonConditionNames,
157-
'ip-address': 'IP address',
158-
};
159-
}, [supportsIpRedirectCondition]);
186+
if (supportsGeolocationRedirectCondition) {
187+
conditionNames['geolocation-country-code'] = 'Country (geolocation)';
188+
conditionNames['geolocation-city-name'] = 'City name (geolocation)';
189+
}
190+
191+
return conditionNames;
192+
}, [supportsGeolocationRedirectCondition, supportsIpRedirectCondition]);
160193

161194
return (
162195
<div className="redirect-rule-modal__condition rounded p-3 h-100 d-flex flex-column gap-2 position-relative">
@@ -200,6 +233,12 @@ const Condition: FC<{
200233
{condition.type === 'ip-address' && (
201234
<IpAddressControls ipAddress={condition.matchValue} onIpAddressChange={setConditionValue} />
202235
)}
236+
{condition.type === 'geolocation-country-code' && (
237+
<CountryCodeControls countryCode={condition.matchValue} onCountryCodeChange={setConditionValue} />
238+
)}
239+
{condition.type === 'geolocation-city-name' && (
240+
<CityNameControls cityName={condition.matchValue} onCityNameChange={setConditionValue} />
241+
)}
203242
</div>
204243
);
205244
};

src/short-urls/ShortUrlsTable.tsx

Lines changed: 34 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { clsx } from 'clsx';
2-
import type { ReactNode } from 'react';
2+
import type { FC, ReactNode } from 'react';
33
import type { FCWithDeps } from '../container/utils';
44
import { componentFactory, useDependencies } from '../container/utils';
55
import { UnstyledButton } from '../utils/components/UnstyledButton';
@@ -20,6 +20,38 @@ type ShortUrlsTableDeps = {
2020
ShortUrlsRow: ShortUrlsRowType;
2121
};
2222

23+
type ShortUrlsTableBodyProps = ShortUrlsTableDeps & Pick<ShortUrlsTableProps, 'shortUrlsList' | 'onTagClick'>;
24+
25+
const ShortUrlsTableBody: FC<ShortUrlsTableBodyProps> = ({ shortUrlsList, onTagClick, ShortUrlsRow }) => {
26+
const { error, loading, shortUrls } = shortUrlsList;
27+
28+
if (error) {
29+
return (
30+
<tr>
31+
<td colSpan={6} className="text-center table-danger text-dark">
32+
Something went wrong while loading short URLs :(
33+
</td>
34+
</tr>
35+
);
36+
}
37+
38+
if (loading) {
39+
return <tr><td colSpan={6} className="text-center">Loading...</td></tr>;
40+
}
41+
42+
if (!loading && (!shortUrls || shortUrls.data.length === 0)) {
43+
return <tr><td colSpan={6} className="text-center">No results found</td></tr>;
44+
}
45+
46+
return shortUrls?.data.map((shortUrl) => (
47+
<ShortUrlsRow
48+
key={shortUrl.shortUrl}
49+
shortUrl={shortUrl}
50+
onTagClick={onTagClick}
51+
/>
52+
));
53+
};
54+
2355
const ShortUrlsTable: FCWithDeps<ShortUrlsTableProps, ShortUrlsTableDeps> = ({
2456
orderByColumn,
2557
renderOrderIcon,
@@ -28,39 +60,10 @@ const ShortUrlsTable: FCWithDeps<ShortUrlsTableProps, ShortUrlsTableDeps> = ({
2860
className,
2961
}: ShortUrlsTableProps) => {
3062
const { ShortUrlsRow } = useDependencies(ShortUrlsTable);
31-
const { error, loading, shortUrls } = shortUrlsList;
3263
const actionableFieldClasses = clsx({ 'short-urls-table__header-cell--with-action': !!orderByColumn });
3364
const orderableColumnsClasses = clsx('short-urls-table__header-cell', actionableFieldClasses);
3465
const tableClasses = clsx('table table-hover responsive-table short-urls-table', className);
3566

36-
const renderShortUrls = () => {
37-
if (error) {
38-
return (
39-
<tr>
40-
<td colSpan={6} className="text-center table-danger text-dark">
41-
Something went wrong while loading short URLs :(
42-
</td>
43-
</tr>
44-
);
45-
}
46-
47-
if (loading) {
48-
return <tr><td colSpan={6} className="text-center">Loading...</td></tr>;
49-
}
50-
51-
if (!loading && (!shortUrls || shortUrls.data.length === 0)) {
52-
return <tr><td colSpan={6} className="text-center">No results found</td></tr>;
53-
}
54-
55-
return shortUrls?.data.map((shortUrl) => (
56-
<ShortUrlsRow
57-
key={shortUrl.shortUrl}
58-
shortUrl={shortUrl}
59-
onTagClick={onTagClick}
60-
/>
61-
));
62-
};
63-
6467
return (
6568
<table className={tableClasses}>
6669
<thead className="responsive-table__header short-urls-table__header">
@@ -88,7 +91,7 @@ const ShortUrlsTable: FCWithDeps<ShortUrlsTableProps, ShortUrlsTableDeps> = ({
8891
</tr>
8992
</thead>
9093
<tbody>
91-
{renderShortUrls()}
94+
<ShortUrlsTableBody ShortUrlsRow={ShortUrlsRow} shortUrlsList={shortUrlsList} onTagClick={onTagClick} />
9295
</tbody>
9396
</table>
9497
);

0 commit comments

Comments
 (0)