import type { IconProp } from '@fortawesome/fontawesome-svg-core'; import { faAndroid, faApple } from '@fortawesome/free-brands-svg-icons'; import { faDesktop } from '@fortawesome/free-solid-svg-icons'; import classNames from 'classnames'; import { parseISO } from 'date-fns'; import { isEmpty, pipe, replace, trim } from 'ramda'; import type { ChangeEvent, FC } from 'react'; import { useEffect, useState } from 'react'; import { Button, FormGroup, Input, Row } from 'reactstrap'; import type { InputType } from 'reactstrap/types/lib/Input'; import { Checkbox } from '../../src/utils/Checkbox'; import type { DateTimeInputProps } from '../../src/utils/dates/DateTimeInput'; import { DateTimeInput } from '../../src/utils/dates/DateTimeInput'; import { formatIsoDate } from '../../src/utils/helpers/date'; import { IconInput } from '../../src/utils/IconInput'; import { SimpleCard } from '../../src/utils/SimpleCard'; import { handleEventPreventingDefault, hasValue } from '../../src/utils/utils'; import type { DomainSelectorProps } from '../domains/DomainSelector'; import type { TagsSelectorProps } from '../tags/helpers/TagsSelector'; import { useFeature } from '../utils/features'; import type { DeviceLongUrls, ShortUrlData } from './data'; import { ShortUrlFormCheckboxGroup } from './helpers/ShortUrlFormCheckboxGroup'; import { UseExistingIfFoundInfoIcon } from './UseExistingIfFoundInfoIcon'; import './ShortUrlForm.scss'; export type Mode = 'create' | 'create-basic' | 'edit'; type DateFields = 'validSince' | 'validUntil'; type NonDateFields = 'longUrl' | 'customSlug' | 'shortCodeLength' | 'domain' | 'maxVisits' | 'title'; export interface ShortUrlFormProps { mode: Mode; saving: boolean; initialState: ShortUrlData; onSave: (shortUrlData: ShortUrlData) => Promise; } const normalizeTag = pipe(trim, replace(/ /g, '-')); const toDate = (date?: string | Date): Date | undefined => (typeof date === 'string' ? parseISO(date) : date); export const ShortUrlForm = ( TagsSelector: FC, DomainSelector: FC, ): FC => ({ mode, saving, onSave, initialState }) => { const [shortUrlData, setShortUrlData] = useState(initialState); const reset = () => setShortUrlData(initialState); const supportsDeviceLongUrls = useFeature('deviceLongUrls'); const isEdit = mode === 'edit'; const isBasicMode = mode === 'create-basic'; const changeTags = (tags: string[]) => setShortUrlData({ ...shortUrlData, tags: tags.map(normalizeTag) }); const setResettableValue = (value: string, initialValue?: any) => { if (hasValue(value)) { return value; } // If an initial value was provided for this when the input is "emptied", explicitly set it to null so that the // value gets removed. Otherwise, set undefined so that it gets ignored. return hasValue(initialValue) ? null : undefined; }; const submit = handleEventPreventingDefault(async () => onSave({ ...shortUrlData, validSince: formatIsoDate(shortUrlData.validSince) ?? null, validUntil: formatIsoDate(shortUrlData.validUntil) ?? null, maxVisits: !hasValue(shortUrlData.maxVisits) ? null : Number(shortUrlData.maxVisits), }).then(() => !isEdit && reset()).catch(() => {})); useEffect(() => { setShortUrlData(initialState); }, [initialState]); // TODO Consider extracting these functions to local components const renderOptionalInput = ( id: NonDateFields, placeholder: string, type: InputType = 'text', props: any = {}, fromGroupProps = {}, ) => ( setShortUrlData({ ...shortUrlData, [id]: e.target.value }))} {...props} /> ); const renderDeviceLongUrlInput = (id: keyof DeviceLongUrls, placeholder: string, icon: IconProp) => ( setShortUrlData({ ...shortUrlData, deviceLongUrls: { ...(shortUrlData.deviceLongUrls ?? {}), [id]: setResettableValue(e.target.value, initialState.deviceLongUrls?.[id]), }, })} /> ); const renderDateInput = (id: DateFields, placeholder: string, props: Partial = {}) => ( setShortUrlData({ ...shortUrlData, [id]: date })} {...props} /> ); const basicComponents = ( <> setShortUrlData({ ...shortUrlData, longUrl: e.target.value })} /> {isBasicMode && renderOptionalInput('customSlug', 'Custom slug', 'text', { bsSize: 'lg' }, { className: 'col-lg-6' })}
); return (
{isBasicMode && basicComponents} {!isBasicMode && ( <>
{basicComponents}
{supportsDeviceLongUrls && (
{renderDeviceLongUrlInput('android', 'Android-specific redirection', faAndroid)} {renderDeviceLongUrlInput('ios', 'iOS-specific redirection', faApple)} {renderDeviceLongUrlInput('desktop', 'Desktop-specific redirection', faDesktop)}
)}
{renderOptionalInput('title', 'Title', 'text', { onChange: ({ target }: ChangeEvent) => setShortUrlData({ ...shortUrlData, title: setResettableValue(target.value, initialState.title), }), })} {!isEdit && ( <>
{renderOptionalInput('customSlug', 'Custom slug', 'text', { disabled: hasValue(shortUrlData.shortCodeLength), })}
{renderOptionalInput('shortCodeLength', 'Short code length', 'number', { min: 4, disabled: hasValue(shortUrlData.customSlug), })}
setShortUrlData({ ...shortUrlData, domain })} /> )}
{renderOptionalInput('maxVisits', 'Maximum number of visits allowed', 'number', { min: 1 })}
{renderDateInput('validSince', 'Enabled since...', { maxDate: shortUrlData.validUntil ? toDate(shortUrlData.validUntil) : undefined })}
{renderDateInput('validUntil', 'Enabled until...', { minDate: shortUrlData.validSince ? toDate(shortUrlData.validSince) : undefined })}
setShortUrlData({ ...shortUrlData, validateUrl })} > Validate URL {!isEdit && (

setShortUrlData({ ...shortUrlData, findIfExists })} > Use existing URL if found

)}
setShortUrlData({ ...shortUrlData, crawlable })} > Make it crawlable setShortUrlData({ ...shortUrlData, forwardQuery })} > Forward query params on redirect
)}
); };