diff --git a/src/components/DateTimeInput.tsx b/src/components/DateTimeInput.tsx index cbe78c5..ad6fa5f 100644 --- a/src/components/DateTimeInput.tsx +++ b/src/components/DateTimeInput.tsx @@ -12,15 +12,16 @@ interface DateTimeInputProps { className?: string; onChange: ChangeEventHandler; type: 'date' | 'time'; + readOnly?: boolean; } -export const DateTimeInput: VFC = ({ id, value, icon, className, onChange, type }) => ( +export const DateTimeInput: VFC = ({ id, value, icon, className, onChange, type, readOnly }) => ( - + ); diff --git a/src/components/TimestampPicker.tsx b/src/components/TimestampPicker.tsx index db7887f..8ce8c23 100644 --- a/src/components/TimestampPicker.tsx +++ b/src/components/TimestampPicker.tsx @@ -1,7 +1,7 @@ import { DateTimeInput } from 'components/DateTimeInput'; import { TFunction } from 'i18next'; import styles from 'modules/TimestampPicker.module.scss'; -import { ChangeEventHandler, useCallback, useMemo, VFC } from 'react'; +import React, { ChangeEventHandler, FC, useCallback, useMemo } from 'react'; import Select from 'react-select'; import { StylesConfig } from 'react-select/src/styles'; import { ThemeConfig } from 'react-select/src/theme'; @@ -69,6 +69,7 @@ const customStyles: StylesConfig = { interface PropTypes { changeTimezone: (tz: null | string) => void; dateString: string; + fixedTimestamp: boolean; handleDateChange: (value: string | null) => void; handleTimeChange: (value: string | null) => void; t: TFunction; @@ -77,15 +78,17 @@ interface PropTypes { timezoneNames: TimezoneOptionType[]; } -export const TimestampPicker: VFC = ({ +export const TimestampPicker: FC = ({ changeTimezone, dateString, + fixedTimestamp, handleDateChange: onDateChange, handleTimeChange: onTimeChange, t, timeString, timezone, timezoneNames, + children, }) => { const handleTimezoneChange = useCallback( (selected: TimezoneOptionType | null) => { @@ -116,10 +119,18 @@ export const TimestampPicker: VFC = ({ id={dateInputId} icon="calendar" onChange={handleDateChange} + readOnly={fixedTimestamp} /> - + @@ -138,9 +149,13 @@ export const TimestampPicker: VFC = ({ theme={customTheme} styles={customStyles} isClearable + isDisabled={fixedTimestamp} /> + + {children} + ); diff --git a/src/components/TimestampsTable.tsx b/src/components/TimestampsTable.tsx index 21f937b..f57fc4a 100644 --- a/src/components/TimestampsTable.tsx +++ b/src/components/TimestampsTable.tsx @@ -71,11 +71,12 @@ const CopySyntax: VoidFunctionComponent<{ syntax: string; className?: string }> interface PropTypes { timestamp: Moment | null; + timeInSeconds: string; locale: string; t: TFunction; } -export const TimestampsTable: VFC = ({ t, locale, timestamp }) => { +export const TimestampsTable: VFC = ({ t, locale, timestamp, timeInSeconds }) => { const [now, setNow] = useState(() => moment()); useEffect(() => { @@ -89,7 +90,6 @@ export const TimestampsTable: VFC = ({ t, locale, timestamp }) => { return moment(value).locale(locale); }, [timestamp, locale]); - const timeInSeconds = useMemo(() => String(timestamp?.unix() || '0'), [timestamp]); const rows = useMemo(() => { const shortDate: TimeValue = { example: localizedTs.format('L'), diff --git a/src/fontawesome.ts b/src/fontawesome.ts index bbca8e6..d4105dc 100644 --- a/src/fontawesome.ts +++ b/src/fontawesome.ts @@ -8,7 +8,9 @@ import { faCode, faEye, faGlobe, + faLock, faTimes, + faTimesCircle, faUserClock, } from '@fortawesome/free-solid-svg-icons'; @@ -18,4 +20,17 @@ config.autoAddCss = false; const brandIcons = [faGithub, faDiscord, faOsi]; // List of used icons - amend if new icons are needed -library.add(...brandIcons, faClipboard, faClock, fasCalendar, farCalendar, faGlobe, faTimes, faEye, faUserClock, faCode); +library.add( + ...brandIcons, + faClipboard, + faClock, + fasCalendar, + farCalendar, + faGlobe, + faTimes, + faEye, + faUserClock, + faCode, + faTimesCircle, + faLock, +); diff --git a/src/pages/index.tsx b/src/pages/index.tsx index 748368c..8397814 100644 --- a/src/pages/index.tsx +++ b/src/pages/index.tsx @@ -1,13 +1,17 @@ +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { AppContainer } from 'components/AppContainer'; import { CustomIcon } from 'components/CustomIcon'; import { Layout } from 'components/Layout'; import { TimestampPicker } from 'components/TimestampPicker'; import { TimestampsTable } from 'components/TimestampsTable'; -import { throttle } from 'lodash'; +import { parseInt, throttle } from 'lodash'; import moment, { Moment } from 'moment-timezone'; import { GetStaticProps } from 'next'; import { SSRConfig, useTranslation } from 'next-i18next'; -import { useCallback, useEffect, useMemo, useState, VFC } from 'react'; +import Link from 'next/link'; +import { useRouter } from 'next/router'; +import React, { useCallback, useEffect, useMemo, useState, VFC } from 'react'; +import { Button, FormGroup } from 'reactstrap'; import { SITE_TITLE } from 'src/config'; import { useLocale } from 'src/util/common'; import { typedServerSideTranslations } from 'src/util/i18n-server'; @@ -17,7 +21,9 @@ interface IndexPageProps { tzNames: string[]; } -const IndexPage: VFC = ({ tzNames }) => { +const TS_QUERY_PARAM = 't'; + +export const IndexPage: VFC = ({ tzNames }) => { const { t, i18n: { language }, @@ -27,7 +33,19 @@ const IndexPage: VFC = ({ tzNames }) => { const [timezone, setTimezone] = useState(() => timezoneNames[0].value); const [timeString, setTimeString] = useState(''); const [dateString, setDateString] = useState(''); + const router = useRouter(); + const timestampQuery = router.query[TS_QUERY_PARAM]; + const initialTimestamp = useMemo(() => { + if (typeof timestampQuery === 'string') { + const timestampNumber = parseInt(timestampQuery, 10); + if (!isNaN(timestampNumber) && isFinite(timestampNumber)) { + return timestampNumber; + } + } + return null; + }, [timestampQuery]); const [timestamp, setTimestamp] = useState(null); + const timestampInSeconds = useMemo(() => String(timestamp?.unix() || '0'), [timestamp]); const handleTimezoneChange = useMemo( () => @@ -49,11 +67,20 @@ const IndexPage: VFC = ({ tzNames }) => { }, []); useEffect(() => { - const clientMoment = moment().seconds(0).milliseconds(0); + let clientMoment: Moment | undefined; + let clientTimezone: string | null = null; + if (typeof initialTimestamp === 'number') { + const initialDate = moment.tz(initialTimestamp * 1000, 'GMT'); + if (initialDate.isValid()) { + clientTimezone = 'GMT'; + clientMoment = initialDate; + } + } + if (!clientMoment) clientMoment = moment().seconds(0).milliseconds(0); setTimeString(clientMoment.format(isoTimeFormat)); setDateString(clientMoment.format(isoDateFormat)); - handleTimezoneChange(null); - }, [handleTimezoneChange]); + handleTimezoneChange(clientTimezone); + }, [handleTimezoneChange, initialTimestamp]); useEffect(() => { if (!dateString || !timeString) return; @@ -74,6 +101,8 @@ const IndexPage: VFC = ({ tzNames }) => { return originalText; }, [locale, t]); + const fixedTimestamp = initialTimestamp !== null; + return ( @@ -92,8 +121,17 @@ const IndexPage: VFC = ({ tzNames }) => { handleTimeChange={handleTimeChange} timezone={timezone} timezoneNames={timezoneNames} - /> - + fixedTimestamp={fixedTimestamp} + > + + + + + + + ); @@ -101,9 +139,20 @@ const IndexPage: VFC = ({ tzNames }) => { export default IndexPage; -export const getStaticProps: GetStaticProps = async ({ locale }) => ({ - props: { - tzNames: getSortedNormalizedTimezoneNames(), - ...(await typedServerSideTranslations(locale, ['common'])), - }, -}); +export const getStaticProps: GetStaticProps = async ({ locale, params }) => { + const timestamp = params?.timestamp; + let initialTimestamp: number | null = null; + if (typeof timestamp === 'string') { + const timestampNumber = parseInt(timestamp, 10); + if (!isNaN(timestampNumber) && isFinite(timestampNumber)) { + initialTimestamp = timestampNumber; + } + } + return { + props: { + initialTimestamp, + tzNames: getSortedNormalizedTimezoneNames(), + ...(await typedServerSideTranslations(locale, ['common'])), + }, + }; +}; diff --git a/src/scss/modules/TimestampPicker.module.scss b/src/scss/modules/TimestampPicker.module.scss index bff49f3..59928de 100644 --- a/src/scss/modules/TimestampPicker.module.scss +++ b/src/scss/modules/TimestampPicker.module.scss @@ -3,105 +3,17 @@ @import '~bootstrap/scss/mixins'; @import '~bootstrap/scss/variables'; -$rdt-background: #111; -$rdt-cell-background: $input-bg; -$rdt-border: invert(#f9f9f9); -$rdt-active: $discord; -$rdt-disabled-color: invert(#999999); - .datepicker { - :global { - // react-datetime - .rdtPicker { - background: $rdt-background; - box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); - border: 1px solid $rdt-border; - } - - .rdtPicker .rdtSwitch { - cursor: pointer !important; - } - - .rdtPicker td.rdtDay:hover, - .rdtPicker td.rdtHour:hover, - .rdtPicker td.rdtMinute:hover, - .rdtPicker td.rdtSecond:hover, - .rdtPicker .rdtSwitch:hover, - .rdtPicker .rdtTimeToggle:hover { - background: $rdt-cell-background; - } - - .rdtPicker td.rdtOld, - .rdtPicker td.rdtNew { - color: $rdt-disabled-color; - } - - .rdtPicker td.rdtToday:before { - border-left: 7px solid transparent; - border-bottom: 7px solid $rdt-active; - border-top-color: rgba(0, 0, 0, 0.2); - } - - .rdtPicker td.rdtActive, - .rdtPicker td.rdtActive:hover { - background-color: $rdt-active; - text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); - } - - .rdtPicker td.rdtActive.rdtToday:before { - border-bottom-color: #fff; - } - - .rdtPicker td.rdtDisabled, - .rdtPicker td.rdtDisabled:hover { - color: $rdt-disabled-color; - } - - .rdtPicker td span.rdtOld { - color: $rdt-disabled-color; - } - - .rdtPicker td span.rdtDisabled, - .rdtPicker td span.rdtDisabled:hover { - color: $rdt-disabled-color; - } - - .rdtPicker th { - border-bottom: 1px solid $rdt-border; - } - - .rdtPicker th.rdtDisabled, - .rdtPicker th.rdtDisabled:hover { - color: $rdt-disabled-color; - } - - .rdtPicker thead tr:first-of-type th:hover { - background: $rdt-cell-background; - } - - .rdtPicker tfoot { - border-top: 1px solid $rdt-border; - } - - .rdtPicker button:hover { - background-color: $rdt-cell-background; - } - - td.rdtMonth:hover, - td.rdtYear:hover { - background: $rdt-cell-background; - } - - .rdtCounter .rdtBtn:hover { - background: $rdt-cell-background; - } - } - margin-bottom: 1rem; .form-label { width: 100%; } + + input[disabled] { + color: rgba($input-color, 0.75); + pointer-events: none; + } } :root[dir='rtl'] {