Replace moment with date-fns

This commit is contained in:
ponas committed 2026-07-29 01:43:11 +02:00
1 parent 5fe86aefea
commit 60ce585d01
8 files changed
+70 -25

No files matched your search

-2
View File
@@ -80,8 +80,6 @@ history, code and binary blobs related to React Native functionality filtered ou
token, so drop redux for [TanStack Query](https://tanstack.com/query/latest) for token, so drop redux for [TanStack Query](https://tanstack.com/query/latest) for
fetching/caching, with the token in React context + AsyncStorage — replaces fetching/caching, with the token in React context + AsyncStorage — replaces
`src/reducers.ts`, the fetch thunks in `src/api.ts` and the `isFetching*`/error flags `src/reducers.ts`, the fetch thunks in `src/api.ts` and the `isFetching*`/error flags
- replace [moment](https://momentjs.com/docs/#/-project-status/) (maintenance mode) with
`date-fns`
- follow-ups from the [Expo SDK 54 upgrade](https://expo.dev/changelog/sdk-54): - follow-ups from the [Expo SDK 54 upgrade](https://expo.dev/changelog/sdk-54):
- decide on Sentry: `@sentry/react-native` is installed but never initialized (see the - decide on Sentry: `@sentry/react-native` is installed but never initialized (see the
commented-out init in `index.ts`), and its config plugin (now applied via `app.json`) commented-out init in `index.ts`), and its config plugin (now applied via `app.json`)
+4
View File
@@ -1,5 +1,9 @@
/* eslint-disable no-undef */ /* eslint-disable no-undef */
// Deliberately not Europe/Oslo: event times must render as Oslo wall-clock no
// matter the device timezone, so tests assert Oslo strings under a foreign TZ.
process.env.TZ = 'America/New_York';
process.env.EXPO_PUBLIC_EVENT_API_URL = 'https://events.example.test/api/v1/events'; process.env.EXPO_PUBLIC_EVENT_API_URL = 'https://events.example.test/api/v1/events';
process.env.EXPO_PUBLIC_DUSKEN_API_URL = 'https://dusken.example.test'; process.env.EXPO_PUBLIC_DUSKEN_API_URL = 'https://dusken.example.test';
process.env.EXPO_PUBLIC_DUSKEN_FORGOT_PASSWORD_URL = 'https://dusken.example.test/auth/password_reset/'; process.env.EXPO_PUBLIC_DUSKEN_FORGOT_PASSWORD_URL = 'https://dusken.example.test/auth/password_reset/';
+18 -10
View File
@@ -9,6 +9,7 @@
"version": "2.0.1", "version": "2.0.1",
"dependencies": { "dependencies": {
"@apollo/client": "^3.14.1", "@apollo/client": "^3.14.1",
"@date-fns/tz": "^1.5.0",
"@expo/vector-icons": "^15.0.3", "@expo/vector-icons": "^15.0.3",
"@react-native-async-storage/async-storage": "2.2.0", "@react-native-async-storage/async-storage": "2.2.0",
"@react-navigation/bottom-tabs": "^7.4.7", "@react-navigation/bottom-tabs": "^7.4.7",
@@ -17,6 +18,7 @@
"@sentry/react-native": "~7.11.0", "@sentry/react-native": "~7.11.0",
"@shopify/react-native-skia": "2.6.2", "@shopify/react-native-skia": "2.6.2",
"@stripe/stripe-react-native": "0.64.0", "@stripe/stripe-react-native": "0.64.0",
"date-fns": "^4.4.0",
"email-validator": "^2.0.4", "email-validator": "^2.0.4",
"expo": "^56.0.0", "expo": "^56.0.0",
"expo-asset": "~56.0.21", "expo-asset": "~56.0.21",
@@ -27,7 +29,6 @@
"expo-splash-screen": "~56.0.14", "expo-splash-screen": "~56.0.14",
"expo-status-bar": "~56.0.4", "expo-status-bar": "~56.0.4",
"graphql": "^16.9.0", "graphql": "^16.9.0",
"moment": "^2.30.1",
"react": "19.2.3", "react": "19.2.3",
"react-hook-form": "^7.52.1", "react-hook-form": "^7.52.1",
"react-native": "0.85.3", "react-native": "0.85.3",
@@ -1405,6 +1406,12 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/@date-fns/tz": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/@date-fns/tz/-/tz-1.5.0.tgz",
"integrity": "sha512-lwYN/vDPeNRULcepoE/LO2Pgx+7/RV+S9ARfbc9lr2DtGkOD7pAiruHvbR1RX3Qyf6ja47EWJDMsNK5vK08DJg==",
"license": "MIT"
},
"node_modules/@egjs/hammerjs": { "node_modules/@egjs/hammerjs": {
"version": "2.0.17", "version": "2.0.17",
"resolved": "https://registry.npmjs.org/@egjs/hammerjs/-/hammerjs-2.0.17.tgz", "resolved": "https://registry.npmjs.org/@egjs/hammerjs/-/hammerjs-2.0.17.tgz",
@@ -6260,6 +6267,16 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/date-fns": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.4.0.tgz",
"integrity": "sha512-+1UMbeh68lH1SegH83CGWwpb6OHHbpSgr3+s5Eww5M4CAgswBpoWS0AjTOfEJ33HiYKz1hdj/KTFprzXHmq/6w==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/kossnocorp"
}
},
"node_modules/debug": { "node_modules/debug": {
"version": "4.4.3", "version": "4.4.3",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
@@ -12222,15 +12239,6 @@
"node": ">=10" "node": ">=10"
} }
}, },
"node_modules/moment": {
"version": "2.30.1",
"resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz",
"integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==",
"license": "MIT",
"engines": {
"node": "*"
}
},
"node_modules/ms": { "node_modules/ms": {
"version": "2.1.3", "version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+2 -1
View File
@@ -16,6 +16,7 @@
}, },
"dependencies": { "dependencies": {
"@apollo/client": "^3.14.1", "@apollo/client": "^3.14.1",
"@date-fns/tz": "^1.5.0",
"@expo/vector-icons": "^15.0.3", "@expo/vector-icons": "^15.0.3",
"@react-native-async-storage/async-storage": "2.2.0", "@react-native-async-storage/async-storage": "2.2.0",
"@react-navigation/bottom-tabs": "^7.4.7", "@react-navigation/bottom-tabs": "^7.4.7",
@@ -24,6 +25,7 @@
"@sentry/react-native": "~7.11.0", "@sentry/react-native": "~7.11.0",
"@shopify/react-native-skia": "2.6.2", "@shopify/react-native-skia": "2.6.2",
"@stripe/stripe-react-native": "0.64.0", "@stripe/stripe-react-native": "0.64.0",
"date-fns": "^4.4.0",
"email-validator": "^2.0.4", "email-validator": "^2.0.4",
"expo": "^56.0.0", "expo": "^56.0.0",
"expo-asset": "~56.0.21", "expo-asset": "~56.0.21",
@@ -34,7 +36,6 @@
"expo-splash-screen": "~56.0.14", "expo-splash-screen": "~56.0.14",
"expo-status-bar": "~56.0.4", "expo-status-bar": "~56.0.4",
"graphql": "^16.9.0", "graphql": "^16.9.0",
"moment": "^2.30.1",
"react": "19.2.3", "react": "19.2.3",
"react-hook-form": "^7.52.1", "react-hook-form": "^7.52.1",
"react-native": "0.85.3", "react-native": "0.85.3",
+40
View File
@@ -30,6 +30,46 @@ const event = {
}, },
}; };
it('formats the next occurrence as a Norwegian date with the venue', async () => {
mockFetch.mockResolvedValue({ json: async () => ({ events: [event] }) });
const { findByText } = renderWithProviders(<EventList />);
expect(await findByText('lø. 1. aug. 2026 kl. 19:00 i Storsalen')).toBeTruthy();
});
it('formats weekday and month abbreviations across the year', async () => {
const events = [
{ ...event, id: '2', nextOccurrence: { ...event.nextOccurrence, start: '2026-12-30T18:00:00+01:00' } },
{ ...event, id: '3', nextOccurrence: { ...event.nextOccurrence, start: '2027-05-17T13:00:00+02:00' } },
];
mockFetch.mockResolvedValue({ json: async () => ({ events }) });
const { findByText } = renderWithProviders(<EventList />);
expect(await findByText('on. 30. des. 2026 kl. 18:00 i Storsalen')).toBeTruthy();
expect(await findByText('ma. 17. mai 2027 kl. 13:00 i Storsalen')).toBeTruthy();
});
it('renders venue-local (Oslo) time whatever the API offset and device timezone', async () => {
// 17:00 UTC = 19:00 in Oslo (CEST); the suite runs under TZ=America/New_York
const events = [{ ...event, nextOccurrence: { ...event.nextOccurrence, start: '2026-08-01T17:00:00Z' } }];
mockFetch.mockResolvedValue({ json: async () => ({ events }) });
const { findByText } = renderWithProviders(<EventList />);
expect(await findByText('lø. 1. aug. 2026 kl. 19:00 i Storsalen')).toBeTruthy();
});
it('prefixes the time with Neste: when the event has more occurrences', async () => {
mockFetch.mockResolvedValue({ json: async () => ({ events: [{ ...event, futureOccurrencesCount: 3 }] }) });
const { findByText } = renderWithProviders(<EventList />);
expect(await findByText('Neste: lø. 1. aug. 2026 kl. 19:00 i Storsalen')).toBeTruthy();
expect(await findByText('Se flere tider (2)')).toBeTruthy();
});
it('renders events from the API', async () => { it('renders events from the API', async () => {
mockFetch.mockResolvedValue({ json: async () => ({ events: [event] }) }); mockFetch.mockResolvedValue({ json: async () => ({ events: [event] }) });
+6 -5
View File
@@ -1,8 +1,9 @@
import { TZDate } from '@date-fns/tz';
import { SimpleLineIcons as Icon } from '@expo/vector-icons'; import { SimpleLineIcons as Icon } from '@expo/vector-icons';
import moment from 'moment'; import { format, parseISO } from 'date-fns';
import { nb } from 'date-fns/locale';
import React from 'react'; import React from 'react';
import { StyleSheet, View, Platform, FlatList, TouchableOpacity, Text, Linking } from 'react-native'; import { StyleSheet, View, Platform, FlatList, TouchableOpacity, Text, Linking } from 'react-native';
import 'moment/locale/nb';
import Button from '@/components/Button'; import Button from '@/components/Button';
import theme from '@/theme'; import theme from '@/theme';
@@ -10,10 +11,10 @@ import { EventType } from '@/types/data';
import useEventList from './useEventList'; import useEventList from './useEventList';
moment.locale('nb'); // Venue-local (Europe/Oslo) wall-clock time regardless of device timezone,
// e.g. "lø. 1. aug. 2026 kl. 19:00"
const formatTime = (time: string) => { const formatTime = (time: string) => {
return moment(time).format('llll'); return format(new TZDate(parseISO(time), 'Europe/Oslo'), "EEEEEE. d. MMM y 'kl.' HH:mm", { locale: nb });
}; };
const Header = () => <View style={styles.listHeader} />; const Header = () => <View style={styles.listHeader} />;
-4
View File
@@ -1,12 +1,8 @@
import moment from 'moment';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import 'moment/locale/nb';
import type { EventType } from '@/types/data'; import type { EventType } from '@/types/data';
import { fetchWithTimeout } from '@/utils'; import { fetchWithTimeout } from '@/utils';
moment.locale('nb');
/* Ref: https://medium.com/react-native-development/how-to-use-the-flatlist-component-react-native-basics-92c482816fe6 */ /* Ref: https://medium.com/react-native-development/how-to-use-the-flatlist-component-react-native-basics-92c482816fe6 */
const useEventList = () => { const useEventList = () => {
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
-3
View File
@@ -1,3 +0,0 @@
// moment ships its locale files as plain JS with no type declarations. TypeScript 6
// (Expo SDK 56) rejects side-effect imports it cannot type, so declare them as untyped.
declare module 'moment/locale/*';