Added ShortUrlStatus concept

This commit is contained in:
Alejandro Celaya 2022-12-18 19:26:30 +01:00
parent 99485cc6d8
commit d1a1b7426e
6 changed files with 105 additions and 40 deletions

View file

@ -65,6 +65,7 @@ export const ShortUrlsTable = (ShortUrlsRow: ShortUrlsRowType) => ({
<th className={orderableColumnsClasses} onClick={orderByColumn?.('dateCreated')}>
Created at {renderOrderIcon?.('dateCreated')}
</th>
<th className="short-urls-table__header-cell" />
<th className={orderableColumnsClasses} onClick={orderByColumn?.('shortCode')}>
Short URL {renderOrderIcon?.('shortCode')}
</th>

View file

@ -31,7 +31,9 @@ export interface ShortUrl {
shortUrl: string;
longUrl: string;
dateCreated: string;
visitsCount: number;
/** @deprecated */
visitsCount: number; // Deprecated since Shlink 3.4.0
visitsSummary?: ShortUrlVisitsSummary; // Optional only before Shlink 3.4.0
meta: Required<Nullable<ShortUrlMeta>>;
tags: string[];
domain: string | null;
@ -46,6 +48,12 @@ export interface ShortUrlMeta {
maxVisits?: number;
}
export interface ShortUrlVisitsSummary {
total: number;
nonBots: number;
bots: number;
}
export interface ShortUrlModalProps {
shortUrl: ShortUrl;
isOpen: boolean;

View file

@ -1,21 +0,0 @@
import { FC, useRef } from 'react';
import { UncontrolledTooltip } from 'reactstrap';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faLinkSlash } from '@fortawesome/free-solid-svg-icons';
import { mutableRefToElementRef } from '../../utils/helpers/components';
export const DisabledLabel: FC = () => {
const tooltipRef = useRef<HTMLElement | undefined>();
return (
<>
<span style={{ cursor: 'help' }} ref={mutableRefToElementRef(tooltipRef)} className="badge text-bg-danger">
<FontAwesomeIcon icon={faLinkSlash} className="me-1" />
Disabled
</span>
<UncontrolledTooltip target={(() => tooltipRef.current) as any} placement="left">
This short URL cannot be currently visited because of some of its limits.
</UncontrolledTooltip>
</>
);
};

View file

@ -0,0 +1,91 @@
import { FC, ReactNode, useRef } from 'react';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { IconDefinition } from '@fortawesome/fontawesome-common-types';
import { faLinkSlash, faCalendarXmark, faCheck } from '@fortawesome/free-solid-svg-icons';
import { UncontrolledTooltip } from 'reactstrap';
import { isBefore } from 'date-fns';
import { mutableRefToElementRef } from '../../utils/helpers/components';
import { ShortUrl } from '../data';
import { formatHumanFriendly, now, parseISO } from '../../utils/helpers/date';
interface ShortUrlStatusProps {
shortUrl: ShortUrl;
}
interface StatusResult {
icon: IconDefinition;
className: string;
description?: ReactNode;
}
const resolveShortUrlStatus = (shortUrl: ShortUrl): StatusResult => {
const { meta, visitsCount, visitsSummary } = shortUrl;
const { maxVisits, validSince, validUntil } = meta;
const totalVisits = visitsSummary?.total ?? visitsCount;
if (maxVisits && totalVisits >= maxVisits) {
return {
icon: faLinkSlash,
className: 'text-danger',
description: (
<>
This short URL cannot be currently visited because it has reached the maximum
amount of <b>{maxVisits}</b> visit{maxVisits > 1 ? 's' : ''}.
</>
),
};
}
if (validUntil && isBefore(parseISO(validUntil), now())) {
return {
icon: faCalendarXmark,
className: 'text-danger',
description: (
<>
This short URL cannot be visited
since <b className="indivisible">{formatHumanFriendly(parseISO(validUntil))}</b>.
</>
),
};
}
if (validSince && isBefore(now(), parseISO(validSince))) {
return {
icon: faCalendarXmark,
className: 'text-warning',
description: (
<>
This short URL will start working
on <b className="indivisible">{formatHumanFriendly(parseISO(validSince))}</b>.
</>
),
};
}
return {
icon: faCheck,
className: 'text-primary',
};
};
export const ShortUrlStatus: FC<ShortUrlStatusProps> = ({ shortUrl }) => {
const tooltipRef = useRef<HTMLElement | undefined>();
const { icon, className, description } = resolveShortUrlStatus(shortUrl);
return (
<>
<span
style={{ cursor: !description ? undefined : 'help' }}
ref={mutableRefToElementRef(tooltipRef)}
className={className}
>
<FontAwesomeIcon icon={icon} />
</span>
{description && (
<UncontrolledTooltip target={(() => tooltipRef.current) as any} placement="right">
{description}
</UncontrolledTooltip>
)}
</>
);
};

View file

@ -9,8 +9,7 @@ import { Time } from '../../utils/dates/Time';
import { ShortUrlVisitsCount } from './ShortUrlVisitsCount';
import { ShortUrlsRowMenuType } from './ShortUrlsRowMenu';
import { Tags } from './Tags';
import { shortUrlIsDisabled } from './index';
import { DisabledLabel } from './DisabledLabel';
import { ShortUrlStatus } from './ShortUrlStatus';
import './ShortUrlsRow.scss';
interface ShortUrlsRowProps {
@ -27,7 +26,6 @@ export const ShortUrlsRow = (
const [copiedToClipboard, setCopiedToClipboard] = useTimeoutToggle();
const [active, setActive] = useTimeoutToggle(false, 500);
const isFirstRun = useRef(true);
const isDisabled = shortUrlIsDisabled(shortUrl);
useEffect(() => {
!isFirstRun.current && setActive();
@ -39,6 +37,9 @@ export const ShortUrlsRow = (
<td className="indivisible short-urls-row__cell responsive-table__cell" data-th="Created at">
<Time date={shortUrl.dateCreated} />
</td>
<td className="responsive-table__cell short-urls-row__cell" data-th="Status">
<ShortUrlStatus shortUrl={shortUrl} />
</td>
<td className="responsive-table__cell short-urls-row__cell" data-th="Short URL">
<span className="position-relative short-urls-row__cell--indivisible">
<span className="short-urls-row__short-url-wrapper">
@ -54,11 +55,6 @@ export const ShortUrlsRow = (
className="responsive-table__cell short-urls-row__cell short-urls-row__cell--break"
data-th={`${shortUrl.title ? 'Title' : 'Long URL'}`}
>
{isDisabled && (
<div className="float-end ms-2">
<DisabledLabel />
</div>
)}
<ExternalLink href={shortUrl.longUrl}>{shortUrl.title ?? shortUrl.longUrl}</ExternalLink>
</td>
{shortUrl.title && (

View file

@ -3,8 +3,6 @@ import { ShortUrl, ShortUrlData } from '../data';
import { OptionalString } from '../../utils/utils';
import { DEFAULT_DOMAIN } from '../../visits/reducers/domainVisits';
import { ShortUrlCreationSettings } from '../../settings/reducers/settings';
import { isBefore, parseISO } from 'date-fns';
import { now } from '../../utils/helpers/date';
export const shortUrlMatches = (shortUrl: ShortUrl, shortCode: string, domain: OptionalString): boolean => {
if (isNil(domain)) {
@ -22,14 +20,6 @@ export const domainMatches = (shortUrl: ShortUrl, domain: string): boolean => {
return shortUrl.domain === domain;
};
export const shortUrlIsDisabled = (shortUrl: ShortUrl): boolean => (
!!shortUrl.meta.maxVisits && shortUrl.visitsCount >= shortUrl.meta.maxVisits
) || (
!!shortUrl.meta.validSince && isBefore(now(), parseISO(shortUrl.meta.validSince))
) || (
!!shortUrl.meta.validUntil && isBefore(parseISO(shortUrl.meta.validUntil), now())
);
export const shortUrlDataFromShortUrl = (shortUrl?: ShortUrl, settings?: ShortUrlCreationSettings): ShortUrlData => {
const validateUrl = settings?.validateUrls ?? false;