Replace moment with date-fns
This commit is contained in:
8 files changed
+70
-25
No files matched your search
@@ -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`)
|
||||||
|
|||||||
@@ -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/';
|
||||||
|
|||||||
Generated
+18
-10
@@ -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
@@ -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",
|
||||||
|
|||||||
@@ -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] }) });
|
||||||
|
|
||||||
|
|||||||
@@ -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} />;
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
Vendored
-3
@@ -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/*';
|
|
||||||
Reference in new issue
Block a user