Skip to content

Commit

Permalink
Enable linking to specific unix timestamp via t query parameter
Browse files Browse the repository at this point in the history
  • Loading branch information
DJDavid98 committed Nov 18, 2021
1 parent 208068a commit bf74f5e
Show file tree
Hide file tree
Showing 6 changed files with 107 additions and 115 deletions.
5 changes: 3 additions & 2 deletions src/components/DateTimeInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,16 @@ interface DateTimeInputProps {
className?: string;
onChange: ChangeEventHandler<HTMLInputElement>;
type: 'date' | 'time';
readOnly?: boolean;
}

export const DateTimeInput: VFC<DateTimeInputProps> = ({ id, value, icon, className, onChange, type }) => (
export const DateTimeInput: VFC<DateTimeInputProps> = ({ id, value, icon, className, onChange, type, readOnly }) => (
<InputGroup className={classNames(styles.dateInputGroup, className)}>
<InputGroupAddon addonType="prepend">
<InputGroupText tag="label" htmlFor={id} className={styles.inputAddon}>
<FontAwesomeIcon icon={icon} fixedWidth />
</InputGroupText>
</InputGroupAddon>
<Input type={type} bsSize="lg" id={id} value={value} onChange={onChange} />
<Input type={type} bsSize="lg" id={id} value={value} onChange={onChange} disabled={readOnly} tabIndex={readOnly ? -1 : undefined} />
</InputGroup>
);
21 changes: 18 additions & 3 deletions src/components/TimestampPicker.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -69,6 +69,7 @@ const customStyles: StylesConfig<TimezoneOptionType, false> = {
interface PropTypes {
changeTimezone: (tz: null | string) => void;
dateString: string;
fixedTimestamp: boolean;
handleDateChange: (value: string | null) => void;
handleTimeChange: (value: string | null) => void;
t: TFunction;
Expand All @@ -77,15 +78,17 @@ interface PropTypes {
timezoneNames: TimezoneOptionType[];
}

export const TimestampPicker: VFC<PropTypes> = ({
export const TimestampPicker: FC<PropTypes> = ({
changeTimezone,
dateString,
fixedTimestamp,
handleDateChange: onDateChange,
handleTimeChange: onTimeChange,
t,
timeString,
timezone,
timezoneNames,
children,
}) => {
const handleTimezoneChange = useCallback(
(selected: TimezoneOptionType | null) => {
Expand Down Expand Up @@ -116,10 +119,18 @@ export const TimestampPicker: VFC<PropTypes> = ({
id={dateInputId}
icon="calendar"
onChange={handleDateChange}
readOnly={fixedTimestamp}
/>
</Col>
<Col xl={6}>
<DateTimeInput type="time" value={timeString} id={timeInputId} icon="clock" onChange={handleTimeChange} />
<DateTimeInput
type="time"
value={timeString}
id={timeInputId}
icon="clock"
onChange={handleTimeChange}
readOnly={fixedTimestamp}
/>
</Col>
</Row>
</FormGroup>
Expand All @@ -138,9 +149,13 @@ export const TimestampPicker: VFC<PropTypes> = ({
theme={customTheme}
styles={customStyles}
isClearable
isDisabled={fixedTimestamp}
/>
</FormGroup>
</Col>
<Col xs="auto" className="d-flex flex-row align-items-end">
{children}
</Col>
</Row>
</div>
);
Expand Down
4 changes: 2 additions & 2 deletions src/components/TimestampsTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<PropTypes> = ({ t, locale, timestamp }) => {
export const TimestampsTable: VFC<PropTypes> = ({ t, locale, timestamp, timeInSeconds }) => {
const [now, setNow] = useState(() => moment());

useEffect(() => {
Expand All @@ -89,7 +90,6 @@ export const TimestampsTable: VFC<PropTypes> = ({ t, locale, timestamp }) => {
return moment(value).locale(locale);
}, [timestamp, locale]);

const timeInSeconds = useMemo(() => String(timestamp?.unix() || '0'), [timestamp]);
const rows = useMemo<TimeValue[]>(() => {
const shortDate: TimeValue = {
example: localizedTs.format('L'),
Expand Down
17 changes: 16 additions & 1 deletion src/fontawesome.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ import {
faCode,
faEye,
faGlobe,
faLock,
faTimes,
faTimesCircle,
faUserClock,
} from '@fortawesome/free-solid-svg-icons';

Expand All @@ -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,
);
77 changes: 63 additions & 14 deletions src/pages/index.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -17,7 +21,9 @@ interface IndexPageProps {
tzNames: string[];
}

const IndexPage: VFC<IndexPageProps> = ({ tzNames }) => {
const TS_QUERY_PARAM = 't';

export const IndexPage: VFC<IndexPageProps> = ({ tzNames }) => {
const {
t,
i18n: { language },
Expand All @@ -27,7 +33,19 @@ const IndexPage: VFC<IndexPageProps> = ({ tzNames }) => {
const [timezone, setTimezone] = useState<string>(() => timezoneNames[0].value);
const [timeString, setTimeString] = useState<string>('');
const [dateString, setDateString] = useState<string>('');
const router = useRouter();
const timestampQuery = router.query[TS_QUERY_PARAM];
const initialTimestamp = useMemo<number | null>(() => {
if (typeof timestampQuery === 'string') {
const timestampNumber = parseInt(timestampQuery, 10);
if (!isNaN(timestampNumber) && isFinite(timestampNumber)) {
return timestampNumber;
}
}
return null;
}, [timestampQuery]);
const [timestamp, setTimestamp] = useState<Moment | null>(null);
const timestampInSeconds = useMemo(() => String(timestamp?.unix() || '0'), [timestamp]);

const handleTimezoneChange = useMemo(
() =>
Expand All @@ -49,11 +67,20 @@ const IndexPage: VFC<IndexPageProps> = ({ 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;
Expand All @@ -74,6 +101,8 @@ const IndexPage: VFC<IndexPageProps> = ({ tzNames }) => {
return originalText;
}, [locale, t]);

const fixedTimestamp = initialTimestamp !== null;

return (
<Layout>
<AppContainer bg="discord">
Expand All @@ -92,18 +121,38 @@ const IndexPage: VFC<IndexPageProps> = ({ tzNames }) => {
handleTimeChange={handleTimeChange}
timezone={timezone}
timezoneNames={timezoneNames}
/>
<TimestampsTable {...commonProps} timestamp={timestamp} />
fixedTimestamp={fixedTimestamp}
>
<FormGroup>
<Link href={fixedTimestamp ? '/' : `/?${TS_QUERY_PARAM}=${timestampInSeconds}`} passHref>
<Button tag="a" size="lg" color={fixedTimestamp ? 'danger' : 'info'}>
<FontAwesomeIcon icon={fixedTimestamp ? 'times-circle' : 'lock'} />
</Button>
</Link>
</FormGroup>
</TimestampPicker>
<TimestampsTable {...commonProps} timestamp={timestamp} timeInSeconds={timestampInSeconds} />
</AppContainer>
</Layout>
);
};

export default IndexPage;

export const getStaticProps: GetStaticProps<IndexPageProps & SSRConfig> = async ({ locale }) => ({
props: {
tzNames: getSortedNormalizedTimezoneNames(),
...(await typedServerSideTranslations(locale, ['common'])),
},
});
export const getStaticProps: GetStaticProps<IndexPageProps & SSRConfig> = 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'])),
},
};
};
98 changes: 5 additions & 93 deletions src/scss/modules/TimestampPicker.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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'] {
Expand Down

0 comments on commit bf74f5e

Please sign in to comment.