Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
113 changes: 74 additions & 39 deletions front/.eslintrc.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,50 @@
// Files that still import moment, exempt from the no-moment rule below until
// they are migrated. Shrinks to nothing as the migration proceeds.
const momentAllowlist = require('./internals/eslint/momentAllowlist');

const restrictedImportPaths = [
{
name: '@tanstack/react-router',
importNames: ['Link', 'useNavigate', 'NavLink'],
message:
"Import the Link or useNavigate from utils/cl-router instead of directly from '@tanstack/react-router'",
},
{
name: 'react-intl',
importNames: ['FormattedMessage', 'injectIntl', 'useIntl'],
message:
"Import FormattedMessage, injectIntl and useIntl from 'utils/cl-intl' instead of directly from 'react-intl'",
},
{
name: 'history',
message:
"Import history from utils/cl-router/cl-history instead of directly from 'history'",
},
{
name: 'lodash',
message: "Import lodash functions from 'lodash-es' instead of 'lodash'",
},
{
name: '@testing-library/react',
message:
"Import React testing library exports from 'utils/testUtils/rtl' instead",
},
{
name: '@tippyjs/react',
message:
"Import Tooltip from component library instead of directly from '@tippyjs/react'",
},
];

const noMomentMessage =
'moment is being removed. Format dates with useFormatDate() in components, ' +
'or utils/dateFormat directly elsewhere. See app/utils/dateFormat.README.md.';

const restrictedMomentPaths = [
{ name: 'moment', message: noMomentMessage },
{ name: 'moment-timezone', message: noMomentMessage },
];

module.exports = {
env: {
browser: true,
Expand Down Expand Up @@ -133,45 +180,7 @@ module.exports = {
'no-multiple-empty-lines': 'off',
'no-new-wrappers': 'error',
'no-param-reassign': 'error',
'no-restricted-imports': [
'error',
{
paths: [
{
name: '@tanstack/react-router',
importNames: ['Link', 'useNavigate', 'NavLink'],
message:
"Import the Link or useNavigate from utils/cl-router instead of directly from '@tanstack/react-router'",
},
{
name: 'react-intl',
importNames: ['FormattedMessage', 'injectIntl', 'useIntl'],
message:
"Import FormattedMessage, injectIntl and useIntl from 'utils/cl-intl' instead of directly from 'react-intl'",
},
{
name: 'history',
message:
"Import history from utils/cl-router/cl-history instead of directly from 'history'",
},
{
name: 'lodash',
message:
"Import lodash functions from 'lodash-es' instead of 'lodash'",
},
{
name: '@testing-library/react',
message:
"Import React testing library exports from 'utils/testUtils/rtl' instead",
},
{
name: '@tippyjs/react',
message:
"Import Tooltip from component library instead of directly from '@tippyjs/react'",
},
],
},
],
'no-restricted-imports': ['error', { paths: restrictedImportPaths }],
'no-trailing-spaces': 'off',
'no-underscore-dangle': 'off',
'no-var': 'error',
Expand Down Expand Up @@ -232,6 +241,32 @@ module.exports = {
],
'@typescript-eslint/no-unnecessary-condition': 'error',
},
overrides: [
{
// Block new moment imports everywhere except the files still waiting to
// be migrated. Set to 'error' rather than 'warn' on purpose: the
// allowlist means this cannot break existing code, and a warning would
// let new moment imports through CI and grow the pile we are shrinking.
files: ['app/**/*.ts', 'app/**/*.tsx'],
excludedFiles: momentAllowlist,
rules: {
'no-restricted-imports': [
'error',
{ paths: [...restrictedImportPaths, ...restrictedMomentPaths] },
],
// no-restricted-imports only sees static imports. moment's locale files
// are pulled in with `await import('moment/dist/locale/xx')`, so without
// this the whole lazy-loading path would slip past the rule.
'no-restricted-syntax': [
'error',
{
selector: 'ImportExpression[source.value=/^moment/]',
message: noMomentMessage,
},
],
},
},
],
ignorePatterns: [
'.rollup.config.cjs',
'.eslintrc.js',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { Box, Title, Text } from '@citizenlab/cl2-component-library';
import { IEventData } from 'api/events/types';
import useAuthUser from 'api/me/useAuthUser';

import useLocale from 'hooks/useLocale';
import useLocalize from 'hooks/useLocalize';

import EventSharingButtons from 'containers/EventsShowPage/components/EventSharingButtons';
Expand All @@ -25,11 +26,12 @@ interface Props {
}

const ConfirmationModal = ({ opened, event, onClose }: Props) => {
const locale = useLocale();
const { data: user } = useAuthUser();
const { formatMessage } = useIntl();
const localize = useLocalize();

const eventDateTime = getEventDateString(event);
const eventDateTime = getEventDateString(event, locale);

return (
<Modal
Expand Down
32 changes: 22 additions & 10 deletions front/app/components/EventCards/DateBlocks/index.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,17 @@
import React, { memo } from 'react';

import { media } from '@citizenlab/cl2-component-library';
import moment from 'moment';
import styled from 'styled-components';

import useLocale from 'hooks/useLocale';

import {
formatDayOfMonth,
formatMonthShort,
formatYear,
getViewerZone,
} from 'utils/dateFormat';

import DateBlockSingleYear from './DateBlockSingleYear';
import DateBlocksMultiYear from './DateBlocksMultiYear';
import { EventDateBlockWrapper } from './styling';
Expand All @@ -22,20 +30,24 @@ const EventDateBlocks = styled.div`
`;

interface Props {
startAtMoment: moment.Moment;
endAtMoment: moment.Moment;
startAt: string;
endAt: string;
isMultiDayEvent: boolean;
showOnlyStartDate?: boolean;
}

export default memo<Props>(
({ startAtMoment, endAtMoment, isMultiDayEvent, showOnlyStartDate }) => {
const startAtDay = startAtMoment.format('DD');
const endAtDay = endAtMoment.format('DD');
const startAtMonth = startAtMoment.format('MMM');
const endAtMonth = endAtMoment.format('MMM');
const startAtYear = startAtMoment.format('YYYY');
const endAtYear = endAtMoment.format('YYYY');
({ startAt, endAt, isMultiDayEvent, showOnlyStartDate }) => {
const locale = useLocale();
// Event dates are shown on the viewer's clock, matching the rest of the
// event UI.
const inViewerZone = { timeZone: getViewerZone() };
const startAtDay = formatDayOfMonth(startAt, locale, inViewerZone);
const endAtDay = formatDayOfMonth(endAt, locale, inViewerZone);
const startAtMonth = formatMonthShort(startAt, locale, inViewerZone);
const endAtMonth = formatMonthShort(endAt, locale, inViewerZone);
const startAtYear = formatYear(startAt, inViewerZone);
const endAtYear = formatYear(endAt, inViewerZone);
const isMultiYearEvent = !showOnlyStartDate && startAtYear !== endAtYear;

return (
Expand Down
17 changes: 8 additions & 9 deletions front/app/components/EventCards/EventInformation/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,12 @@ import {
Text,
colors,
} from '@citizenlab/cl2-component-library';
import moment from 'moment-timezone';
import { isPast } from 'date-fns';
import styled, { useTheme } from 'styled-components';

import { IEventData } from 'api/events/types';

import useLocale from 'hooks/useLocale';
import useLocalize from 'hooks/useLocalize';

import EventAttendanceButton from 'components/EventAttendanceButton';
Expand All @@ -21,7 +22,7 @@ import ButtonWithLink from 'components/UI/ButtonWithLink';

import { useIntl } from 'utils/cl-intl';
import Link, { typedStyled } from 'utils/cl-router/Link';
import { getEventDateString, userTimezone } from 'utils/dateUtils';
import { getEventDateString } from 'utils/dateUtils';

import DateBlocks from '../DateBlocks';
import messages from '../messages';
Expand Down Expand Up @@ -54,19 +55,17 @@ interface Props {
}

const EventInformation = ({ event }: Props) => {
const locale = useLocale();
const { formatMessage } = useIntl();
const theme = useTheme();
const localize = useLocalize();
const registrantCountMessage = useRegistrantCountMessage(event);
const ariaId = useId();

const startAtMoment = moment.tz(event.attributes.start_at, userTimezone);
const endAtMoment = moment.tz(event.attributes.end_at, userTimezone);

const isPastEvent = moment().isAfter(endAtMoment);
const isPastEvent = isPast(new Date(event.attributes.end_at));
const address1 = event.attributes.address_1;
const onlineLink = event.attributes.online_link;
const eventDateTime = getEventDateString(event);
const eventDateTime = getEventDateString(event, locale);

return (
<EventInformationContainer data-testid="EventInformation">
Expand Down Expand Up @@ -95,8 +94,8 @@ const EventInformation = ({ event }: Props) => {
</EventTitleLink>

<DateBlocks
startAtMoment={startAtMoment}
endAtMoment={endAtMoment}
startAt={event.attributes.start_at}
endAt={event.attributes.end_at}
isMultiDayEvent={false}
showOnlyStartDate={true}
/>
Expand Down
9 changes: 6 additions & 3 deletions front/app/components/EventPreviews/EventPreviewCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,17 @@ import {
Text,
useBreakpoint,
} from '@citizenlab/cl2-component-library';
import moment from 'moment';
import styled, { useTheme } from 'styled-components';

import { IEventData } from 'api/events/types';

import useLocale from 'hooks/useLocale';
import useLocalize from 'hooks/useLocalize';

import DayAndMonth from 'components/EventCards/DateBlocks/DayAndMonth';

import { formatDayOfMonth, formatMonthShort } from 'utils/dateFormat';

const Container = styled(Box)`
${defaultCardStyle};
flex-shrink: 0;
Expand Down Expand Up @@ -57,6 +59,7 @@ type EventPreviewCardProps = {
};

const EventPreviewCard = ({ event }: EventPreviewCardProps) => {
const locale = useLocale();
const localize = useLocalize();
const isMobile = useBreakpoint('phone');

Expand All @@ -78,8 +81,8 @@ const EventPreviewCard = ({ event }: EventPreviewCardProps) => {
<Box display="flex" flexDirection="column" alignItems="stretch">
<EventDate mr={theme.isRtl ? '8px' : '0px'}>
<DayAndMonth
day={moment(event.attributes.start_at).format('DD')}
month={moment(event.attributes.start_at).format('MMM')}
day={formatDayOfMonth(event.attributes.start_at, locale)}
month={formatMonthShort(event.attributes.start_at, locale)}
/>
</EventDate>
</Box>
Expand Down
41 changes: 26 additions & 15 deletions front/app/components/ScreenReadableEventDate/index.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
import React from 'react';

import moment from 'moment-timezone';

import { IEventData } from 'api/events/types';

import useLocale from 'hooks/useLocale';

import { ScreenReaderOnly } from 'utils/a11y';
import { useIntl } from 'utils/cl-intl';
import { userTimezone } from 'utils/dateUtils';
import {
formatLongDate,
formatTime,
formatTimeZoneAbbreviation,
getViewerZone,
isSameDayInZone,
} from 'utils/dateFormat';

import messages from './messages';

Expand All @@ -22,31 +28,36 @@ interface Props {
* easy for a user using a screen reader to understand the date.
*/
const ScreenReadableEventDate = ({ event }: Props) => {
const locale = useLocale();
const { formatMessage } = useIntl();
const startAtMoment = moment.tz(event.attributes.start_at, userTimezone);
const endAtMoment = moment.tz(event.attributes.end_at, userTimezone);
const tzLabel = startAtMoment.format('z');
const isEventMultipleDays =
startAtMoment.dayOfYear() !== endAtMoment.dayOfYear();
const { start_at, end_at } = event.attributes;
// Event times read in the VIEWER's zone, with the zone named explicitly.
const inViewerZone = { timeZone: getViewerZone() };
const tzLabel = formatTimeZoneAbbreviation(start_at, locale, inViewerZone);
const isEventMultipleDays = !isSameDayInZone(
start_at,
end_at,
inViewerZone.timeZone
);

return (
<ScreenReaderOnly>
{isEventMultipleDays ? (
<p>
{formatMessage(messages.multiDayScreenReaderDate, {
startDate: startAtMoment.format('MMMM Do, YYYY'),
startTime: startAtMoment.format('LT'),
endDate: endAtMoment.format('MMMM Do, YYYY'),
endTime: endAtMoment.format('LT'),
startDate: formatLongDate(start_at, locale, inViewerZone),
startTime: formatTime(start_at, locale, inViewerZone),
endDate: formatLongDate(end_at, locale, inViewerZone),
endTime: formatTime(end_at, locale, inViewerZone),
timezone: tzLabel,
})}
</p>
) : (
<p>
{formatMessage(messages.singleDayScreenReaderDate, {
eventDate: startAtMoment.format('MMMM Do, YYYY'),
startTime: startAtMoment.format('LT'),
endTime: endAtMoment.format('LT'),
eventDate: formatLongDate(start_at, locale, inViewerZone),
startTime: formatTime(start_at, locale, inViewerZone),
endTime: formatTime(end_at, locale, inViewerZone),
timezone: tzLabel,
})}
</p>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import React, { useMemo, useState } from 'react';

import { colors, Box, Text } from '@citizenlab/cl2-component-library';
import moment from 'moment-timezone';
import 'react-day-picker/style.css';
import { transparentize } from 'polished';
import { DayPicker, PropsBase } from 'react-day-picker';
Expand All @@ -15,6 +14,7 @@ import TimeInput from 'components/admin/TimeSelection/TimeInput';
import Warning from 'components/UI/Warning';

import { useIntl } from 'utils/cl-intl';
import { formatUtcOffset } from 'utils/dateFormat';
import { userTimezone } from 'utils/dateUtils';

import { getLocale } from '../../_shared/locales';
Expand Down Expand Up @@ -231,7 +231,7 @@ const Calendar = ({
}: Props) => {
const { data: tenant } = useAppConfiguration();
const timeZone = tenant?.data.attributes.settings.core.timezone;
const gmtOffset = timeZone ? moment().tz(timeZone).format('Z') : '';
const gmtOffset = timeZone ? formatUtcOffset(Date.now(), { timeZone }) : '';

const { formatMessage } = useIntl();

Expand Down
Loading